請求書を開き、金額や日付を探してExcelへ転記する。1件なら数分でも、毎月100件あれば無視できない作業量です。
Pythonを使えば、PDFの受信、文字抽出、検算、CSV保存まで自動化できます。ただし、数行の抽出コードだけでは安全な無人運転になりません。
実際にHiroの環境で確認したところ、テキストを含む小規模PDFは3件すべてから文字列を取得できました。一方、記事生成の自動化基盤では、レビューと最終確認がそれぞれ240秒でタイムアウトしています。それでも処理を段階分けしていたため、記事保存、Notion保存、GitHubへのpushまでは完了しました。
この違いから分かるのは、PDF抽出で重要なのは「文字を読めること」だけではないという点です。失敗を検知し、危険な結果を止め、途中から再実行できる設計まで必要です。
この記事では、Python初心者でも着手できるように、PDF帳票から必要情報を抽出する仕組みを9ステップで解説します。
PythonによるPDF情報抽出の全体像
PDF抽出は、次の工程に分けると安全に運用できます。
PDF受信
↓
重複判定
↓
ページごとの種類判定
├─ テキストPDF → 文字を直接抽出
└─ 画像PDF → OCR
↓
必要項目を抽出・正規化
↓
業務ルールで検算
├─ 正常 → CSV・DB・会計システムへ保存
└─ 異常 → 隔離して通知
工程を分ける理由は、失敗箇所を特定しやすくするためです。
取引先が帳票レイアウトを変更しても、受信処理や保存処理まで作り直す必要はありません。該当する抽出ルールだけを修正できます。
最初に知っておきたいPDFの3分類
PDFは見た目が同じでも、内部構造が異なります。
| PDFの種類 | 具体例 | 主な抽出手段 | 注意点 |
|---|---|---|---|
| テキストPDF | 会計ソフトから出力した請求書 | pdfplumber、PyMuPDF | 見た目と文字の読み順が異なる場合がある |
| 画像PDF | 紙をスキャンした領収書 | OCR、pytesseract | 傾き、低解像度、印影で誤読しやすい |
| 混在PDF | 表紙は画像、明細はテキスト | ページ別判定 | ファイル全体の一律判定では取りこぼす |
PDFファイル単位ではなく、ページ単位で文字が含まれているか確認するのがポイントです。
たとえば、10ページのうち1ページだけがスキャン画像なら、そのページだけOCRへ回します。全ページにOCRをかけるより高速で、誤認識も抑えられます。
Hiro環境で確認したPDF抽出の最小検証
Hiroのローカル環境では、2026年7月12日に次のスモークテストを実施しています。
test=hiro_pdf_extract_smoke_test
date=2026-07-12
timezone=Asia/Tokyo
python=3.11.9
library=pdfplumber
samples=3
elapsed_ms=8.6
text_extracted=3/3
取得した文字列は次の3件です。
Invoice No: A-001 Amount: 12800 Due: 2026-07-31
Invoice No: A-002 Amount: 9800 Due: 2026-08-15
Invoice No: A-003 Amount: 45000 Due: 2026-08-31
3件すべてから文字列を取得し、処理時間は合計8.6ミリ秒でした。
ただし、これはメモリ上で作成した単純なテキストPDFによる動作確認です。実際の請求書、複数ページの表、スキャン画像、縦書き、文字化けを含むPDFの性能を示すベンチマークではありません。
また、正解データとの項目単位比較を行った記録ではないため、「正解率100%」とは評価していません。確認できたのは、あくまで次の2点です。
- テスト用PDFの文字列取得に3件中3件成功した
- 3件の抽出処理に合計8.6ミリ秒かかった
本番導入前には、実際に処理する帳票を使って別途正解率を測る必要があります。
自動化基盤の実運用ログから分かったこと
PDF抽出の速度とは別に、Hiroが運用する記事生成基盤では、2026年7月22日に次の処理結果が記録されています。
| 時刻 | 工程 | 結果 |
|---|---|---|
| 19:27:38 | PDF抽出記事の生成開始 | 開始 |
| 19:31:13 | 下書き生成 | 成功 |
| 19:31:13 | Geminiによるレビュー | コマンド長超過で失敗 |
| 19:36:05 | Codexによるレビュー | 240秒でタイムアウト |
| 19:41:18 | Codexによる最終確認 | 240秒でタイムアウト |
| 19:41:18 | Markdown保存 | 成功 |
| 19:41:19 | Notion保存 | 成功 |
| 19:41:23 | GitHub push | 成功 |
これはPDF抽出精度の検証結果ではありません。しかし、無人処理を設計するうえで重要な一次情報です。
レビュー工程が失敗しても、どの成果物を使うかが事前に決められていたため、後続処理を継続できました。PDF抽出でも同じように、次の動作を明確にします。
- 抽出に失敗したらOCRへ切り替える
- 検算に失敗したら後続登録を止める
- 通知に失敗しても抽出結果は保存する
- 外部システムへの保存が失敗したら再送できる状態を残す
- タイムアウト後の再実行で二重登録しない
「失敗しないシステム」を目指すのではなく、「失敗しても壊れないシステム」を作ることが重要です。
ステップ1:実物のPDFを集めて分類する
最初に、実際に処理する帳票を集めます。
きれいなサンプルPDFだけで開発すると、本番で初めて例外に遭遇します。最低でも次の違いが含まれるようにしてください。
- 発行元が異なる
- 1ページと複数ページがある
- テキスト型とスキャン型がある
- 金額に「円」「¥」「税込」などの表記差がある
- 表が複数ページにまたがる
- 訂正版や再発行版がある
- パスワード付き、破損、空白ページがある
- 印鑑や手書き文字が重なっている
最初の20件を集める場合でも、同じ会社の請求書20件より、10社から2件ずつ集めたほうがレイアウト差を確認できます。
20件は業界標準ではありません。初期調査の例であり、本番判断に必要な件数は帳票の種類と誤登録時の損失によって変わります。
ステップ2:抽出項目と停止条件を決める
コードを書く前に、抽出仕様を表にします。
| 項目 | 値の例 | 必須 | 検証条件 | 異常時の動作 |
|---|---|---|---|---|
| 請求書番号 | INV-2026-0712 | 必須 | 空欄不可、重複不可 | 処理停止 |
| 請求日 | 2026-07-12 | 必須 | 日付として変換可能 | 処理停止 |
| 請求金額 | 128,000円 | 必須 | 数値化可能、0円以上 | 隔離 |
| 支払期限 | 2026-07-31 | 必須 | 請求日以降 | 隔離 |
| 登録番号 | T1234567890123 | 任意 | T+13桁 | 警告 |
| 備考 | 7月利用分 | 任意 | 条件なし | 空欄を許可 |
特に金額、口座番号、契約番号は、誤りが業務損失につながります。
必須・任意を決めるだけでなく、「異常を検知したら自動登録を止めるのか、警告だけにするのか」まで決めてください。
ステップ3:Pythonの実行環境を作る
まずはテキストPDFを扱う最小環境を作ります。
py -3.11 -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
pip install pdfplumber pymupdf
インストール後、バージョンを記録します。
python --version
pip show pdfplumber pymupdf
ライブラリの更新によって抽出結果が変化する可能性があります。障害調査のため、Pythonとライブラリのバージョンをログへ残します。
ステップ4:ページごとにテキスト型か判定する
PyMuPDFを使って、ページ内の文字数を確認します。
from pathlib import Path
import fitz
def classify_pages(pdf_path: Path) -> list[dict]:
results = []
with fitz.open(pdf_path) as document:
for page_number, page in enumerate(document, start=1):
text = page.get_text("text").strip()
results.append(
{
"page": page_number,
"text": text,
"character_count": len(text),
"method": (
"embedded_text"
if len(text) >= 30
else "ocr_required"
),
}
)
return results
30文字は説明用の仮値です。対象PDFで文字数を測り、次の条件も含めて境界値を調整します。
請求書やInvoiceなどの必須ラベルがあるか- 空白ページではないか
- 文字化けした記号だけが並んでいないか
- 表紙だけを誤ってOCR対象にしていないか
文字数だけで正常判定しないことが重要です。
ステップ5:必要項目を抽出して正規化する
請求金額のように形式が決まった値は、まず正規表現で抽出します。
import re
import unicodedata
from decimal import Decimal
def normalize_text(value: str) -> str:
return unicodedata.normalize("NFKC", value)
def extract_amount(text: str) -> Decimal | None:
normalized = normalize_text(text)
match = re.search(
r"請求金額[::\s]*[¥¥]?\s*([\d,]+)\s*円?",
normalized,
)
if not match:
return None
return Decimal(match.group(1).replace(",", ""))
抽出方法は帳票の特徴に合わせて選びます。
- 形式が固定された番号:正規表現
- ラベルの右側や次の行にある値:ラベル近傍探索
- 位置が完全に固定された帳票:座標抽出
- 品目、数量、単価:表抽出
- スキャンされた文字:OCR
- レイアウトが多様な自由記述:AI補助
金額にはfloatではなくDecimalを使います。浮動小数点の誤差を業務データへ持ち込まないためです。
ステップ6:値と抽出根拠を一緒に保存する
抽出結果には、値だけでなく根拠も保存します。
{
"field": "invoice_total",
"value": 128000,
"page": 1,
"method": "label_regex",
"source_text": "ご請求金額 ¥128,000",
"template_id": "aozora_invoice_v1",
"validation": "passed"
}
最低限、次の情報を残します。
- 元ファイルの識別子
- ページ番号
- 抽出方式
- 元になった文字列
- 帳票テンプレート
- 検算結果
- 処理日時
- Pythonとライブラリのバージョン
後から値が違うと判明したとき、原因を追跡できます。
ただし、口座番号や個人情報をログへそのまま出してはいけません。必要に応じて末尾4桁だけ残すなど、マスキングします。
ステップ7:業務ルールで検算する
文字として読み取れたことと、業務上正しいことは別です。
次のような検算を追加します。
- 明細の合計と請求合計が一致する
- 小計、税額、合計の関係が成立する
- 支払期限が請求日より前になっていない
- 請求書番号が過去データと重複していない
- 取引先名と振込先口座が登録済みマスターと一致する
- 通貨が前回の帳票から変わっていない
- 金額が過去平均から大幅に外れていない
消費税額は端数処理によって差が出ます。1円でも違えば異常と決めつけず、取引先の切り捨て・切り上げルールを確認して許容差を設定します。
異常値を自動修正するのは危険です。原則として、異常を検知した帳票は隔離し、人間の確認対象へ回します。
ステップ8:ハッシュと状態管理で二重登録を防ぐ
ファイル名だけでは重複判定できません。同じファイルが別名で届く場合や、同名ファイルの内容が更新される場合があるためです。
SHA-256ハッシュを計算して、処理済みか判定します。
from hashlib import sha256
from pathlib import Path
def calculate_file_hash(path: Path) -> str:
return sha256(path.read_bytes()).hexdigest()
処理状態も段階ごとに保存します。
received
↓
classified
↓
extracted
↓
validated
├─ exported
└─ quarantined
外部システムへの保存時には、ファイルハッシュや請求書番号を冪等キーとして使用します。
タイムアウト後に再実行しても、同じ請求書が二重登録されないことをテストしてください。
ステップ9:定期実行・隔離・通知を接続する
最後に、Windowsタスクスケジューラやcronなどから定期実行します。
正常データと異常データを分離します。
data/
├─ inbox/
├─ processed/
├─ quarantined/
└─ logs/
通知には、エラー全文を貼るだけでなく、担当者が次に何を確認すべきかを含めます。
PDF抽出を停止しました。
ファイル: invoice_202607.pdf
テンプレート: acme_invoice_v2
失敗項目: invoice_total
ページ: 2
理由: 明細合計と請求合計が不一致
次の操作: 元PDFの合計欄と税端数処理を確認してください
外部サービスへの保存が失敗しても、抽出済みデータは消さないでください。保存工程だけを再実行できる状態にします。
専門家が確認する7つのチェックポイント
1. 項目単位ではなく帳票単位でも評価する
1帳票に20項目あり、毎回1項目だけ間違える場合、項目単位正解率は95%です。しかし、完全に正しい帳票は0件です。
自動登録の可否は、帳票単位成功率でも判断します。
2. 「読めない」と「誤って読めた」を分ける
空欄なら停止できます。一方、OCRが8を3と読んだ場合は正常な数値に見えます。
金額や口座番号は、OCR信頼度だけでなく、合計値やマスターデータとの照合が必要です。
3. 未知のレイアウトを自動承認しない
帳票テンプレートを識別できなければ、unknownとして隔離します。
最も危険なのは、別会社用の抽出ルールを適用し、偶然取得できた数値を正常扱いすることです。
4. 座標抽出へ依存しすぎない
座標抽出は高速ですが、余白やフォントの変更で壊れます。
位置よりもラベルや見出しの意味を基準にし、座標は補助条件として使います。
5. 元PDFを保存する
抽出結果だけを保存すると、後から誤りを検証できません。
保存期間、アクセス権、暗号化、削除ルールを決めたうえで元PDFを保管します。
6. タイムアウト後の状態を確認する
呼び出し元がタイムアウトしても、外部処理が完了している場合があります。
再実行する前に、保存先や処理IDを確認します。確認せずに再送すると、二重登録の原因になります。
7. 人間確認をゼロにしない
次の帳票は、完全無人化に向かない場合があります。
- 手書き文字が多い
- 画像がぼやけている
- 印鑑が文字に重なっている
- レイアウトが毎回変わる
- 法的判断や承認が必要
- 誤登録時の損失が大きい
全件を人間が確認するのではなく、危険な例外だけを確認する設計が現実的です。
よくある失敗と改善方法
文字を取得できない
主な原因: スキャンPDFをテキストPDFとして処理している。
改善方法:
- ページごとの抽出文字数を記録する
- 必須ラベルの有無を確認する
- 文字がなければOCRへ切り替える
- OCR前に傾き補正やノイズ除去を試す
金額を数値化できない
主な原因: 全角数字、カンマ、通貨記号、空白が混在している。
改善方法:
- UnicodeをNFKC形式へ正規化する
- 通貨記号と桁区切りを除去する
Decimalへ変換する- 変換できなければ自動登録を止める
表の列がずれる
主な原因: PDFにはExcelのようなセル構造がなく、文字が座標として配置されている。
改善方法:
- 見出し名から列を特定する
- 文字のX座標を補助条件にする
- ページごとに繰り返されるヘッダーを除外する
- 明細合計と請求合計で検算する
帳票改訂のたびに処理が止まる
主な原因: すべての発行元を一つのルールで処理している。
改善方法:
- テンプレートIDを発行元ごとに分ける
- テンプレートのバージョンを管理する
- 未知レイアウトを隔離する
- 隔離件数の多い順にルールを追加する
自動化したのに確認時間が減らない
主な原因: 全件を人間が再確認している。
改善方法:
- 必須項目がすべて取得できたか確認する
- 業務検算を通過した帳票だけ自動承認する
- 未知テンプレートや低信頼度だけを確認対象にする
- 人間が修正した理由を記録し、抽出ルールへ反映する
PDF抽出で追うべきKPI
| KPI | 計算方法 | 確認できること |
|---|---|---|
| 文字列取得率 | 文字取得成功PDF数 ÷ 受信PDF数 | 抽出エンジンの基本動作 |
| 項目単位正解率 | 正しかった項目数 ÷ 確認項目数 | 各フィールドの品質 |
| 帳票単位成功率 | 全項目が正しい帳票数 ÷ 確認帳票数 | 自動登録可能な割合 |
| 完全自動処理率 | 人間確認なしで完了した件数 ÷ 受信件数 | 無人化の進捗 |
| 誤自動承認率 | 誤りがあった自動承認件数 ÷ 自動承認件数 | 最重要の安全指標 |
| 隔離率 | 隔離件数 ÷ 受信件数 | 例外処理の負担 |
| 未知テンプレート率 | 未対応帳票数 ÷ 受信件数 | ルール追加の優先度 |
| 1件当たり処理時間 | 合計処理時間 ÷ 処理件数 | 性能とコスト |
| 1件当たり人手時間 | 合計確認時間 ÷ 処理件数 | 実際の削減効果 |
| 下流訂正率 | 保存後に訂正した件数 ÷ 保存件数 | 業務影響を含む品質 |
優先順位は処理速度より、誤自動承認率です。
数秒の短縮より、誤った金額を会計システムへ登録しないことのほうが重要です。
KPIには必ず対象期間と母数を添えます。
対象期間: 2026年7月
対象: A社テキスト請求書100件
手書き: なし
帳票単位成功率: 96/100
完全自動処理率: 92/100
誤自動承認率: 0/92
条件を書かなければ、翌月や別帳票と正しく比較できません。
反論:件数が少ないなら手作業のほうが早いのでは?
月に数件しかなく、レイアウトも毎回変わる場合は、手作業のほうが合理的です。
自動化が向いているのは、次の条件がそろう業務です。
- 同じ種類の帳票が繰り返し届く
- 転記する項目が決まっている
- 手作業による入力ミスが発生している
- 抽出後に照合、通知、集計などの後続処理がある
- ログ保存や監査が必要
逆に、年1回しか来ない帳票や、判断の大部分が人間の読解に依存する文書は、自動化の維持費が削減時間を上回る可能性があります。
まずは月間削減時間を試算します。
月間削減時間
= 月間処理件数
× 1件当たり手作業時間
× 自動処理できる割合
開発と保守にかかる時間を含めて判断してください。
この設計の限界
この方法でも、次の問題は残ります。
- OCRは低解像度や手書きを誤認識する
- PDFライブラリの更新で抽出結果が変わる可能性がある
- 発行元の帳票改訂には追随が必要
- 外部OCRやAIへ送信できない機密文書がある
- 合計値が一致しても、個別項目が正しいとは限らない
- 自動化によって収益や削減効果が保証されるわけではない
また、Hiro環境の8.6ミリ秒という結果は小規模なテキストPDF3件の値です。OCR、大量ページ、暗号化PDF、ネットワーク処理を含む本番性能へ一般化できません。
本番導入前チェックリスト
- 実物PDFを発行元、形式、ページ数で分類した
- 正解データを人間が作成した
- 必須項目と任意項目を分けた
- 項目ごとの停止条件を決めた
- ページごとにテキスト型と画像型を判定できる
- 未知の帳票を自動承認しない
- 値と抽出根拠を保存している
- 明細合計などの業務検算がある
- ファイルハッシュで重複を防いでいる
- 途中から再実行できる
- タイムアウト後の二重登録を防げる
- 例外PDFを隔離できる
- ログ内の機密情報をマスキングしている
- 外部サービスの規約とデータ保持条件を確認した
- 項目単位と帳票単位の両方で正解率を測った
今日できる最小実験
最初からOCRやAIを組み込む必要はありません。
次の順番で、1種類の帳票だけを検証してください。
- 同じ発行元のPDFを3件用意する
- 抽出したい項目を3つに絞る
- 人間が正解データを作る
pdfplumberまたはPyMuPDFで文字を取得する- 正規表現で項目を抽出する
- 正解データと比較する
- 処理時間と失敗理由を記録する
- 異常なPDFを隔離する
正解データは次のようなCSVで十分です。
file,invoice_number,invoice_date,total
invoice_001.pdf,A-001,2026-07-01,12800
invoice_002.pdf,A-002,2026-07-05,9800
invoice_003.pdf,A-003,2026-07-10,45000
最初の目標は「完全自動化」ではありません。
どのPDFなら安全に自動処理でき、どのPDFを人間へ戻すべきか判定できる状態を作ることです。
まとめ:PDFを読むコードではなく、例外だけを見る仕組みを作る
PythonでPDF帳票から情報を抽出する基本手順は次の9段階です。
- 実物PDFを集めて分類する
- 抽出項目と停止条件を決める
- Pythonの実行環境を固定する
- ページごとにPDFの種類を判定する
- 必要項目を抽出して正規化する
- 値と抽出根拠を保存する
- 業務ルールで検算する
- ハッシュと状態管理で重複を防ぐ
- 定期実行、隔離、通知を接続する
Hiro環境の最小検証では、単純なテキストPDF3件から合計8.6ミリ秒で文字列を取得できました。しかし、この結果だけでは本番精度を判断できません。
一方、記事生成基盤の実運用では、レビューと最終確認がタイムアウトしても、保存工程を分離していたため、Markdown保存、Notion保存、GitHub pushまで完了しました。
PDF抽出でも同じです。重要なのは、すべてを成功させることではありません。
- 誤った値を後工程へ流さない
- 失敗したPDFだけを隔離する
- 根拠を残して原因を追跡する
- タイムアウト後も安全に再実行する
- 同じ帳票を二重登録しない
まずは手元の同一形式PDFを3件選び、抽出項目を3つだけ決めてください。正解データを作り、文字列取得率、帳票単位成功率、誤自動承認率を測るところから始めましょう。