PythonでPDFから請求書データを抽出するには、ファイルからテキストを読み取り、そのテキストから指定されたフィールドを抜き出します。2つのステップで、おそらく40行のコードがあれば、最初に試す請求書では見事に機能するでしょう。しかし、7番目のサプライヤーが請求書番号を3行上に移動させると、午前2時にパーサーが突然Noneを返し始めます。
この後半部分こそが、この記事の本当のテーマです。読み取り自体は簡単です。正確性を維持し続けることこそが、本当の仕事なのです。
主なポイント
- Pythonは数行のコードで請求書のテキストを抽出できます。実際には、何百ものサプライヤーのレイアウトに対応し、正確性を維持し続けることにコストがかかります。
- PDFはデータフォーマットではありません。印刷されたページのタイポグラフィ的な記述に過ぎないため、特定のサプライヤーのレイアウトに対して記述された正規表現は非常に脆いものとなります。
- 正規表現とサプライヤーごとのテンプレートの管理コストは、サプライヤーの数に比例して増加します。ビジョンモデルは文字列ではなくページ全体を読み取るため、そのように増加することはありません。
- どの方法でデータを抽出するにせよ、自分自身のコードで算術チェックを行う必要があります。小計と一致しない明細行の合計は、これまでで最も安上がりなバグ検出器となります。
- 人々が自社開発を諦める理由は、データ抽出そのものにあることは稀です。例外処理キュー、ベンダーのマッチング、そして会計システムとの統合が、本当の障壁なのです。
PDFフォーマットとは
PDFフォーマットは、請求書のような紙の書類を正確にデジタルで表現できる柔軟なフォーマットです。 元々、紙印刷の世界から生まれ、印刷ページをそのままデジタルに反映するように設計されています。 この柔軟性により、PDF作成者は自由に表現でき、さまざまな基準や規制にも対応できます。
一方、PDFの中にデータが閉じ込められていると、後から情報を抽出するのは手間がかかります。 フォーマットが自由で複雑になりやすく、日々大量に処理される業務用途では、構造化データとして管理しづらい場合があります。

PDFは、各文字がページのどこに配置されているかを保存します。右下の数字が合計金額であるという事実は保存しません。その関係性はあなたの頭の中にあり、この記事で紹介するすべての抽出方法は、それをエンコードする試みです。
請求書からデータを抽出する手順
請求書は一般的にPDFフォーマットでやりとりされます。 請求書は、供給者と顧客の間で、商品やサービスの対価を正式に記録するための書類です。 この請求書から必要なデータを抽出する場合、下記の手順を踏みます:
- 抽出したいデータ項目(スキーマ)を決める
- 請求書ファイルを画像からテキストに変換する
- テキストからスキーマに沿ってデータを取得する
- 抽出したデータをまとめる

請求書データのスキーマを定義する
請求書はサプライヤーごとに異なるレイアウトで作られるものの、根本的に必要な情報はほぼ同じです。一般的には「供給者」「顧客」「請求書番号」「日付」「商品リスト(数量・説明・単価・金額)」などが含まれます。 スキーマ設定の際は、最終的に会計システムやデータベースで管理しやすい形式を意識するのがポイントです。 もし汎用性の高いデータ定義が必要であれば、schema.org にInvoiceの標準フォーマットも掲載されていますので参考になります。 Parseurでは請求書用の標準データスキーマを用意しており、要件に合わせて請求書メールボックスのフィールド名を変更することで、こちらで説明しているようにユースケースに合わせてカスタマイズすることも可能です。データフォーマットが定義されたら、請求書を画像からテキストに変換できます。
例えば、請求書用のデータスキーマを下記のようなJSON(Swagger形式)で表すことが出来ます:
{
"InvoiceNumber": {
"type": "string",
"description": "The invoice number"
},
"InvoiceIssueDate": {
"type": "string",
"description": "The invoice date"
},
"Items": {
"type": "array",
"description": "The list of items in the invoice",
"items": {
"type": "object",
"properties": {
"quantity": {
"type": "number",
"description": "The quantity of the item"
},
"description": {
"type": "string",
"description": "The description of the item"
},
"unit_price": {
"type": "number",
"description": "The unit price of the item"
},
"price": {
"type": "number",
"description": "The total price of the item"
}
}
}
}
}
解析コードを書く前に、これを書き留めてください。これは、以下のすべてのメソッドが満たさなければならない契約であり、後でビジョンモデルに渡すものとなります。
請求書を画像からテキストに変換する

PDFファイルにはスキャン画像が含まれている場合があります。たとえば、社員がスマートフォンで請求書を撮影してPDFにし、経理担当者へ送るケースです。 経理担当者はこの請求書からミスなく情報を抜き出し、会計システムに手入力しなければなりません。 このような画像からデータを抽出するには、光学文字認識 (OCR)を使いテキスト化する必要があります。 有名なOCRとしてTesseractがあります。TesseractはC/C++で記述されていますが、Pythonから利用するにはPyTesseractなどのバインディングが必要です。バインディングとは、他言語(この場合Python)からライブラリ(Tesseract)を呼び出す仕組みのことです。 他にも多くのOCRエンジンがあり、その結果は使われる技術やドキュメントのスキャン品質によって大きく異なります。 Parseurは文書が画像かどうかを自動判別し、内部で自動的にテキスト化します。文書のデータがテキスト化されれば、抽出の準備は完了です。
データスキーマに従って請求書内のテキストを抽出する
PDFがテキスト(検索可能)になっていれば、pdftotextというPythonライブラリでPDFからテキストデータを取得できます。 以下は、PDFからテキストを抽出する基本的なサンプルコードです:
import pdftotext
# 請求書を読み込む
with open("invoice.pdf", "rb") as file_handle:
pdf = pdftotext.PDF(file_handle)
# すべてのページをループ処理する
for page in pdf:
print(page)
このスクリプトを convert_pdf_to_text.py として保存し実行すると、標準出力にPDFのテキストが表示されます。
テキストをファイルに書き出したい場合は下記コマンドを利用します:
$ python convert_pdf_to_text.py > invoice.txt
pdftotextはフラットな文字列を返します。これはヘッダーフィールドには適していますが、テーブルには絶望的です。明細行が必要な場合は、すべての単語の座標を保持し、テーブル自体の抽出を試みることができるpdfplumberを使用してください。
import pdfplumber
with pdfplumber.open("invoice.pdf") as pdf:
page = pdf.pages[0]
# ページ上の位置を含む単語
for word in page.extract_words():
print(word["text"], word["x0"], word["top"])
# そして明細行テーブルの抽出を試みる
table = page.extract_table()
if table:
for row in table:
print(row)
実際のサプライヤーの請求書でこれを実行すると、非常に多くの場合、tableがNoneになることがわかります。これはバグではありません。この問題が見た目よりも難しいという最初の正直なシグナルであり、これについては後で詳しく説明します。
テキスト形式の請求書が手に入ったので、以下の手法の組み合わせを使用して、そこから必要なデータを抽出できます:
- 正規表現でのパターン抽出。強力ですがフォーマット変更に弱く、表形式データには向きません。
- 動的OCRやゾーンOCRなど視覚的なテンプレート解析。強固ですが、実装にはより高度な設定が必要です。
- スキーマとともにページをビジョンモデルに渡し、人間が文書を読むのと同じように読み取らせることができます。これは過去2年間で状況を変えたアプローチであり、後ほど独自のセクションで説明します。
Python標準の re モジュールで請求書番号を抽出するサンプル:
import re
# 請求書を読み込む
with open("invoice.txt", "r") as file_handle:
invoice = file_handle.read()
# 請求書番号を抽出する
invoice_number = re.search(r"Invoice number: (\w+)", invoice).group(1)
print(invoice_number)
このスクリプトを extract.py に保存し実行すると、請求書番号が出力されます:
$ python extract.py
例として、
INV-1234
が表示されます。
サプライヤーがレイアウトを変更すると、正規表現による請求書パーサーが壊れるのはなぜか
正規表現は文字列と一致しますが、請求書は画像です。うまくいかない問題のすべてはその不一致から生じており、それは予測可能な少数のパターンで失敗します。
- ラベルが移動した。 パターンは
Invoice number:に固定されていますが、新しいテンプレートではInvoice #となっているか、同じ行ではなく次の行に値が配置されています。 extract_tableがNoneを返す。 テーブルエクストラクターは罫線を探します。ほとんどのサプライヤーの請求書は空白で列を揃えており、境界線はまったく引かれていません。- テキストが間違った順序で出力される。 ページが文字列に平坦化されると、2列のレイアウトやフローティングアドレスブロックが混在し、明細行がシャッフルされて到着します。
- テーブルが複数のページにまたがっている。 2行目から9行目は1ページ目にあり、10行目から14行目は2ページ目にあり、その間に列ヘッダーが繰り返され、小計行が明細行のように表示されます。
- 3つの数字がすべて合計のように見える。 小計、合計、請求額、および繰越残高。クレジット(マイナス請求)が含まれている請求書で最大の数値を選ぶと、間違いになります。
- スキャンが写真である。 OCRがかすれた
8を3として読み取っても、3は完全に有効な数字であるため、下流のプロセスでは何も気付きません。 - 結合されたセルと複数行の記述。 1つの製品説明が3行に折り返され、行分割ロジックによって価格のない3つのアイテムに分割されてしまいます。
これらはどれも、より優れた正規表現で修正できるものではありません。これらはすべて、文字列の問題を装ったレイアウトの問題なのです。これが、ほとんどの人が際限なく増え続けるサプライヤーごとのテンプレートを書き始めるか、アプローチを変えるかの分岐点となります。
2026年のアプローチ - ビジョンモデルとスキーマ
大きな変化は、抽出する前にページをテキストに平坦化する必要がなくなったことです。ビジョンモデルはレンダリングされた請求書を見るため、サプライヤーが請求書番号を移動させても問題にはなりません。あなたが提供するのはパターンではなく、この記事の冒頭で作成したスキーマです。
毎回同じキーを取得できるように出力を制約します。OpenAIの構造化出力のような構造化出力モードとPydanticの組み合わせが、それを実現してくれます。
from typing import List
from pydantic import BaseModel
class LineItem(BaseModel):
description: str
quantity: float
unit_price: float
amount: float
class Invoice(BaseModel):
vendor_name: str
invoice_number: str
invoice_date: str # ISO 8601
currency: str
subtotal: float
tax: float
total: float
line_items: List[LineItem]
# PDFページを画像としてレンダリングし、ビジョンモデルに送信して、
# 応答がInvoiceスキーマに一致することを要求する。
# モデルはフィールドに入力する。形を発明することはできない。
これにより、抽出問題の大部分が純粋に解決され、このアプローチがこれほど急速に広まった理由でもあります。しかし同時に、正規表現にはなかった新たな失敗モードも生み出します。請求書番号を見つけられない正規表現はNoneを返しますが、それを見つけられないAIモデルは、時としてもっともらしい番号を作り出してしまうのです。安全を保つためのルールはシンプルです。「モデルが提案し、あなたのコードが検証する」ことです。
自分でモデルを実行したくない場合は、Azure AI Document IntelligenceやAmazon Textract's AnalyzeExpenseなど、クラウドプロバイダーが提供するマネージドサービスとしても同じ機能が販売されており、どちらもヘッダーフィールドと明細行を別々に返します。この根本的なアプローチがルールベースの解析とどのように異なるのかについてはAI対ルールベースのPDFパーサーで解説し、請求書に特化した場合にどのようになるかについてはビジョンAIによる請求書処理で説明しています。
自分で書く必要があるバリデーションレイヤー
何がJSONを生成したにせよ、これらのチェックはエクストラクターの信頼度スコアではなく、独自のコードで行うべきです。これらは安価で決定的であり、金銭的損失につながるエラーを捕捉します。
小計 + 税金 + 送料 - 割引が合計と1セント(1円)の範囲内で一致する。- 明細行の金額の合計が小計と一致する。一致しない場合は、行を見落としたか、存在しない行を作り出しています。
- すべての
数量 * 単価がそれぞれの金額と等しい。 - 日付が解析可能であり、未来の日付ではない。
- そのベンダーに対して、請求書番号の支払いがまだ行われていない。重複支払いは、買掛金において最も高くつくバグです。
- ベンダー名が、ベンダーマスターのレコードと解決(一致)できる。
- 支払先の銀行口座情報が、そのベンダーに対してすでに登録されているものと一致する。ここでの変更はデータチェックではなく、不正チェックとなります。
- 通貨が実際に取引を行っているものである。
- 発注書番号が存在し、2ウェイまたは3ウェイマッチングを実行する場合、その数量と価格が一致する。
- 必須フィールドがすべて存在し、空ではない。
失敗したものはすべて、元帳ではなく人間による確認に回されます。
def validate(invoice):
errors = []
if abs(invoice.subtotal + invoice.tax - invoice.total) > 0.01:
errors.append("totals_do_not_add_up")
line_sum = sum(item.amount for item in invoice.line_items)
if invoice.line_items and abs(line_sum - invoice.subtotal) > 0.01:
errors.append("line_items_do_not_sum_to_subtotal")
if not invoice.invoice_number:
errors.append("missing_invoice_number")
return errors
10行の算術計算は、いくらプロンプトを調整するよりも、より多くの現実的な問題を捕捉します。
invoice2dataや他のライブラリについてはどうか?
invoice2dataは、多くの人が最初にたどり着く結果であり、純粋に優れたソフトウェアであるため、ここで正当に言及する価値があります。これは、サプライヤーごとに記述したYAMLテンプレートと請求書を照合するコマンドラインツールおよびPythonライブラリであり、マッチングロジックはコードに埋もれることなくバージョン管理下に置かれます。レイアウトを絶対に変更しないサプライヤーが十数社いる場合、それは長年にわたって役立つでしょう。
限界は品質ではなく、その設計にあります。サプライヤーごとに1つのテンプレートが必要ということは、サプライヤーリストとともにメンテナンスが増加し、まさに上記で挙げた理由によってテンプレートが壊れることを意味します。アクティブなレイアウトが20〜30のどこかを超えた時点で、テンプレートを保守する担当者は、手入力で請求書を打ち込んでいた担当者よりも多くの作業を行うことになります。
同じ推論は、より幅広いライブラリにも当てはまります。pdfplumber、PyMuPDF、pdftotext、pytesseractはいずれも、ページから文字と座標を取得するという実際の仕事において非常に優れています。しかし、どれが合計金額であるかを把握するように作られているものは一つもありません。私たちは、最高のPDFパーサーと最高のデータ抽出APIのまとめで、より広範なツール群を比較しています。
抽出したデータを収集する
Pythonを活用することで、指定フォルダ内の複数の請求書PDFからデータを一括して抽出できます。 たとえば請求書番号と合計金額を抜き出してCSVとして出力するには次のようにします:
import os
import re
import pdftotext
# フォルダ内のすべてのPDFファイルをループ処理する
for filename in os.listdir("invoices/"):
if not filename.endswith(".pdf"):
continue
# 請求書を読み込む
with open("invoices/" + filename, "rb") as file_handle:
pdf = pdftotext.PDF(file_handle)
# CSVの列ヘッダーを出力する
print("InvoiceNumber,TotalAmount")
# すべてのページをループ処理する
for page in pdf:
# 請求書番号を抽出する
invoice_number = re.search(r"Invoice number: (\w+)", page).group(1)
total_amount = re.search(r"Total amount: (\w+)", page).group(1)
print(invoice_number, total_amount, sep=",")
このスクリプトを extract_to_csv.py で保存・実行すると、請求書番号と合計金額が標準出力され、それをCSV形式でリダイレクトすればExcel等のスプレッドシートソフトウェアで活用できます:
$ python extract_to_csv.py > invoices.csv
ほとんどの請求書スクリプトはCSVファイルのフォルダを生成したところで終了しますが、本当の会計業務はそこから始まります。誰かがそれらをインポートし、ベンダーを照合し、会計システムが拒否した行を追いかけ、午前2時に検証に失敗した請求書の扱いを決定しなければなりません。その作業はスクリプトには現れず、トークンの請求書にも現れません。
自社構築をやめるべきタイミング
これは多くのベンダーがスキップする部分なので、最初に言っておきます。安定した少数のサプライヤーがいて、パイプラインを監視できるエンジニアがおり、データの処理を待っている人が誰もいない場合は、スクリプトを書いてください。ビジョンモデルとスキーマ、そして上記の10行のバリデーションを組み合わせれば、非常にわずかなコストで多くの成果を得ることができ、すべての部分を自分で理解することができます。
自社構築のツケは後からやってきます。そしてそれは抽出部分ではありません。誰もプロトタイプを作らない次のような部分に現れます。
- 例外処理キュー。 AP(買掛金管理)チームには、フィールドをクリックすると請求書上の該当箇所がハイライトされ、40秒ではなく4秒で修正できる画面が必要です。これはスクリプトではなく、プロダクトです。
- ベンダーマッチング。 「ACME Ltd」、「Acme Limited」、「ACME LTD.」は1つのサプライヤーですが、元帳は3つを受け入れません。
- 重複の検出。 全く同じ請求書が火曜日にメールの添付ファイルとして到着し、金曜日に明細書のPDFとして到着します。
- ステートマシン。 キュー、再試行、部分的な失敗、そして昨夜の300枚の請求書のうち、実際にどれが通過したかを把握することです。
- 監査証跡。 何が抽出され、人間が何を変更し、誰が承認し、いつ行われたのか。経理部門は通常、監査中にそれを尋ねてきます。
- JSONの後のすべて。 会計システムへのフィールドマッピング、GL(総勘定元帳)コーディング、発注書との照合、そして拒否された行の処理。
自社構築よりも購入すべき時期が来たことを示す明確なシグナルは以下の通りです。アクティブなサプライヤーのレイアウトが約20〜30を超えている。ヘッダーフィールドだけでなく明細行も必要である。少なからぬ請求書がスキャン画像として到着している。エンジニアリングチームではなく、AP(買掛金管理)チームがミスを修正する必要がある。抽出の失敗により支払いが遅延している。あるいは、最も一般的なケースとして、パーサーを保守している担当者が、他の業務を一切進められなくなっている。
Parseurはどのように処理するか
Parseurは抽出ステップだけでなく、ループ全体を実行するドキュメントパーサーです。請求書は専用のメールボックスアドレス、API、または監視対象のフォルダから届きます。ビジョンAIエンジンはPDF、スキャン画像、写真を読み取り、テキストAIエンジンはメールとテキストドキュメントを読み取ります。フィールドは名前と型が付けられて出力され、明細行は行として出力されます。記述するテンプレートはなく、サプライヤーが請求書のデザインを変更してもメンテナンスするものは何もありません。
JSONの上に得られるのは、この記事が警告してきた半分の作業です。抽出されたデータは所定の場所で確認および修正できるため、AP担当者はチケットを開くことなく誤って読み取られた合計金額を修正できます。修正は抽出エンジンにフィードバックされます。そしてデータは、直接のWebhook統合、Make、Zapier、またはMicrosoft Power Automateを通じて、あるいはAPIからのJSONとして直接、必要な場所に送信されます。
デフォルトで請求書から抽出するフィールドセットを確認したい場合は、請求書のOCRページにドキュメント化されており、より広範なワークフローについては請求書データキャプチャでカバーされています。
まとめ
Pythonを使用してPDFから請求書データを抽出することは、約100枚の請求書にとっては解決済みの問題ですが、1万枚にとっては未解決の問題です。この2つの数字の間で変わるのはコードではありません。変わるのは、どれだけのサプライヤーのレイアウトをあなたが静かに保守し続けることになるか、そしてそのうちの1つが変更されたときに誰が呼び出されるかということです。
スクリプトを書いてみてください。上記の7つの失敗モードのうち、どれに最初に直面するかを正確に知るためだけでも、一度は実行する価値があります。その後、あなたの次の6ヶ月間の時間を8番目の問題に費やすのが最善かどうかを正直に判断してください。答えが「ノー」である場合、Parseurは2016年からこの課題に取り組んでおり、あなたからそのフォルダを引き取ります。
最終更新日





