欲しいデータが にしか無い、という状況があります。行政の資料や企業の開示資料では今でも珍しくありません。画面には整った表が見えているので、取り出せそうに感じます。
や が用意されていれば迷わずそちらを使いますが、無いことがあります。ところが PDF には、表という構造がありません。あるのは「この位置にこの文字がある」という情報だけです。人間は罫線と位置関係から表だと認識しますが、機械にとっては座標付きの文字の集まりに過ぎません。
つまり PDF からの表抽出は、必ず推測を含みます。うまくいくこともあれば、列がずれることも、セルが結合されることもあります。この前提を理解したうえで使う必要があります。
文字が入っているかを最初に確かめる
最初にやるべきは、その PDF に文字が入っているかどうかの確認です。紙をスキャンしただけの PDF は、中身が画像なので文字を取り出せません。この場合は が必要になり、難易度が一段上がります。
確認は簡単で、1ページぶんの文字を取り出してみるだけです。何も返ってこなければ画像です。この確認を飛ばして抽出処理を書くと、原因不明の空の結果に悩むことになります。
import pdfplumber
def has_text_layer(path: str, check_pages: int = 3) -> bool: """先頭の数ページに文字が入っているかを見る。 入っていなければスキャン画像なので、OCR が要る。""" with pdfplumber.open(path) as pdf: for page in pdf.pages[:check_pages]: text = (page.extract_text() or "").strip() if text: return True return False
def diagnose(path: str) -> str: if not has_text_layer(path): return "文字が入っていない。スキャン画像なので OCR が必要" return "文字が入っている。抽出を試せる"罫線があるかどうかで手法が変わる
文字が入っている場合、次の分かれ目は罫線の有無です。罫線で区切られた表なら、線の位置を手がかりに列を判定できます。罫線が無く、空白で桁を揃えているだけの表は、文字の座標から列の切れ目を推測することになります。
後者は精度が落ちます。数字の桁数がまちまちだと、同じ列の値が違う位置に来るためです。列がずれていないかを、抽出後に必ず目で確かめてください。
| PDF の状態 | 手法 | 精度 |
|---|---|---|
| 文字あり・罫線あり | 罫線を手がかりに抽出 | 比較的高い |
| 文字あり・罫線なし | 座標から列を推測 | 低い。要確認 |
| スキャン画像 | OCR を通してから抽出 | 内容次第。数字は誤認しやすい |
| 文字あり・多段組 | 段の分割から必要 | 低い |
スキャン画像から数字を読み取る場合、1 と 7、0 と 8、6 と 5 の取り違えが起きます。金額や件数を扱うなら、合計との突き合わせなど、別の方法で検算できる仕組みが無い限り実用は難しいと考えてください。
表が複数ページにまたがる場合も要注意です。ページごとに見出しが繰り返されることがあり、それをデータの行として読み込んでしまいます。逆に、2ページ目以降に見出しが無く、列の対応が分からなくなることもあります。
ページをまたいで1つの行が分割されていることもあります。人が見れば続きだと分かりますが、機械には別の行に見えます。ページ単位で抽出してから、自分で繋ぎ直すほうが制御しやすい。
抽出した結果はの形に直してから保存してください。PDF から出てきた直後の形は、元の表の見た目を引きずっているので、そのままでは集計に使えません。
抽出した結果を必ず検算する
PDF からの抽出は推測を含むので、結果が正しいかを別の手段で確かめる必要があります。最も手軽なのが、表に載っている合計行との突き合わせです。明細を足して合計と一致すれば、少なくとも数字の取り違えは起きていません。
def verify_total(rows: list[dict], total_row: dict, col: str, tol: float = 0.5) -> None: """合計行があるなら、必ず突き合わせる。 ずれていれば、抽出の段階で値を取り違えている。""" detail_sum = sum(r[col] for r in rows if r.get(col) is not None) stated = total_row.get(col)
if stated is None: raise ValueError(f"合計行に {col} が無い") if abs(detail_sum - stated) > tol: raise ValueError( f"{col} の合計が一致しない: 明細 {detail_sum} / 合計行 {stated}" )この検算は、EP.13 で扱った取得直後の検算と同じ考え方です。合計行が無い場合は、元の PDF を目で見て数か所を照合するしかありません。全部は無理でも、先頭・末尾・途中の数行を確かめるだけで、列のずれのような大きな失敗には気づけます。
そして最後に。同じデータが他の形式で配布されていないかを、もう一度探してください。PDF の隣に CSVが置かれていることは、実際によくあります。抽出処理を書く時間より、探す時間のほうがずっと短くて済みます。
PDF からの抽出は、うまくいったときでも「たまたま合っていただけ」の可能性が残ります。検算の手段を先に用意してから取りかかってください。
そして繰り返しになりますが、これは最後の手段です。同じ内容が別の形式で出ていないかを、時間をかけてでも先に探す価値があります。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。