世界を動かす技術を、日本語で。

PDFを解析したいですか?

概要

  • PDFパーサー の理想と現実の違いを解説
  • PDF仕様 に基づく解析手順の流れを説明
  • 実際のファイルで発生する よくある問題点 を具体例で紹介
  • 現実のPDF解析 で遭遇するエラーや非準拠ケースの多様性を指摘
  • 主要なPDFビューアの多くは 非準拠PDF にも対応している事実を強調

PDFパーサーの理想的な解析手順

  • ファイル先頭の バージョンヘッダーコメント 検出
  • クロスリファレンステーブル(xref) へのポインタ検出
  • 全オブジェクトのバイトオフセット 一覧取得
  • トレーラーディクショナリ の構築とカタログディクショナリへの参照取得
  • これらの手順により PDFの構造解析 が可能となる

PDFオブジェクトの基礎

  • PDFオブジェクト は有効なPDF内容(数値、文字列、辞書など)をobj/endobjで囲む
  • 例: 16 0 obj 620 endobj は16番目のオブジェクト(世代0)に数値620を格納
  • オブジェクト間参照 は「16 0 R」のような間接参照形式
  • 仕様上、特定の型のオブジェクトは 間接参照 である必要
  • オブジェクトの分割方法 は生成アプリケーション次第

クロスリファレンステーブル(xref)の探索

  • xrefテーブル は各オブジェクトの位置を示すインデックス
  • ファイル末尾にstartxrefとオフセット、%%EOFマーカー
  • 仕様では ファイルの末尾%%EOFがあるべき
  • 実際は 1024バイト以内 や異なる位置、スペルミス(startref)など多様
  • xrefオフセット の検出が困難な場合も多い

オブジェクトオフセットの取得

  • 指定オフセットに 整形済みxrefテーブル が存在するはず
  • 例: xref 7 4 ... は7番から4つのオブジェクト情報
  • 各行に バイトオフセット・世代番号・状態(n:使用中, f:未使用)
  • 複数のxrefテーブルやストリーム が/Prevで連結される場合あり

トレーラーディクショナリの特定

  • startxref直前 にトレーラーディクショナリ
  • ルートオブジェクト など重要メタデータを格納
  • 参照をたどって PDF全体の内容解釈 が可能

現実世界のPDF解析の困難

  • 仕様通りの パーサー では多くの実ファイルで失敗
  • 非準拠ファイル の多発、仕様は「社会的構築物」「雰囲気」
  • パーサーの堅牢性 が求められる理由

xrefポインタ探索の現実的な課題

  • ファイル末尾や1024バイト以内 にポインタがない
  • スペルミス や予想外のフォーマット
  • ポインタが の場合も多い
  • サンプル調査で 0.5%のファイル が不正なxref宣言

PDF内容が非ゼロオフセットで開始

  • バージョンヘッダー前にゴミデータ があると全オフセットがずれる
  • 例: 先頭10バイトのゴミでstartxrefが10バイトずれる
  • バージョンヘッダーの実オフセット も考慮して両方を試す必要
  • サンプルエラーの 約50% がこの問題

xrefテーブル内へのポインタ

  • ポインタが xrefテーブル中間 を指すケース
  • サンプルで約5件発生

xref付近の「ほぼ正しい」ポインタ

  • 1文字分ずれている、endobjマーカー内など微妙な位置
  • よくある事例

xrefオフセットは正しいがテーブル内容が不正

  • テーブル内のオブジェクトオフセット が一部または全部不正
  • サブセクションごとに異なるずれ
  • /Prevポインタの値が0(デフォルト値を誤って書き込んだ)など

xrefテーブル自体のフォーマット不良

  • linebreakなしxref5 2 ...
  • 宣言数より多いエントリ が含まれる
  • テーブル途中にゴミデータ が混入

結論と考察

  • 仕様通りの解析手順と 現実のエラー例 を比較
  • サンプル調査で 0.5%の非準拠ファイル 確認
  • PDF.js, Adobe, Sumatraなど 主要ビューア は非準拠ファイルにも対応
  • PDF仕様の一部(全1300ページ中22ページ) だけでも多様な困難
  • 堅牢なPDFパーサー開発 には多様な実装例への対応が不可欠

Hackerたちの意見

そうだね、PDFはストリーミングを考慮してなかったんだ。最後のトレーラー辞書が厄介で、ファイルが完全に読み込まれるまで解析できないんだよね。とはいえ、最初のページを表示するのに十分な情報が前にある「ストリーミング可能な」PDFもあると思うよ(でも、私はもう10年以上PDFのことを追ってないから、その点は注意してね)。

そうそう、リニアライズされたPDFがあって、これはフルファイルをダウンロードせずに最初のページを表示できるように整理されてるんだ。今はそのことを要約から省いたけど、彼らには独自の付録があるからね。

PDFを解析したいの?絶対に無理だよ。記事に書いてある理由からね。

銀行がもっとわかりやすい形式で記録を提供してくれたらいいのに。でも、それまでは仕方ないね。

ほんとだよね。その間違いをしたことがあるから、もう二度とやらないよ。

Pythonを学んだ後に挑戦した最初のプログラミングプロジェクトの一つが、D&Dのキャンペーン用に地図を自動で取得するためのPDFパーサーだったんだ。全然うまくいかなかったけど(笑)。

PDFをページごとに画像に変換して、その画像をOCRプログラムに入れるか(PDFが単一カラムの場合)、ビジョンLLMに入れるんだ(ダブルカラムや複雑なレイアウトの場合)。一部のビジョンLLMはPDF入力を直接受け付けることもできるけど、画像に変換して処理するか、他の方法でテキストを抽出しようとして失敗するかを確認する必要があるよ。OpenAIやAnthropic、Geminiは今、画像版の処理をしてくれてるから、ありがたいね。

残念ながら、これはある意味納得できるね。PDFはテキスト内の文字をフォントへのオフセットとして表現していて、しばしばフォントが不完全なんだ。だから、PDF内の'A'は古き良きASCII 65じゃないことが多い。理論的には、'A'だと教えてくれる2つのオプションシステムがあるはずなんだけど、うまくいかないこともあるから、結局はフォントを使って描画するしかないんだ。

知ってるPDF作成者がいないなら、PDFコンテンツを安全に扱うにはこれが唯一の方法だね。Type 3フォントだけでも、テキストを取り出すのが信頼できないか、無理な場合が多いし、スキャン画像が含まれてるPDFに至る前に問題が出る。今のLLMは、Tesseractみたいな以前の方法よりもかなり改善されてると思うんだけど、画像入力を与えた場合、モデルの能力をテストする方法って知ってる?

いいまとめだね。君が触れなかったけど、面白いと思ったのはインクリメンタルセーブチェーンのこと。最初のstartxrefオフセットは問題ないけど、Acrobatが連続編集で追加する/Prevリンクは次のxrefの数バイト手前を指すことがあるんだ。ほとんどのビューア(PDF.js、MuPDF、Adobe Readerの「修復」モードでも)は、objトークンを探すために強引なスキャンに戻って、新しいテーブルを再構築するから、ちゃんと動くけど、仕様に正確なパーサーは爆発しちゃう。異なるアプリケーションで何度も編集された現実のドキュメントを扱うなら、似たようなサルベージパスを構築するのはほぼ必須だね。

その通り、これはサンプルセットでよく見られた失敗状態だね。前の参照や参照チェーンのものがオフセット0を指したり、ファイルの範囲外を指したり、単に間違ってることもあった。この記事を書こうと思ったのは、私のプロジェクトPdfPigの初期解析ロジックを書き直そうとしたからなんだ。最初はJavaのPDFBoxのコードを移植したけど、もっとパフォーマンス良く書き直せるはずだと思ったんだ。新しいロジックは、単一のxrefテーブルやストリームを見逃した場合、ファイル全体を強引にスキャンすることに戻って、その回復パスのオフセットに頼ってるんだ。でも、前のコードよりかなり遅くなってしまって、変更に自信を持つのが難しい。今は1万ファイルのテストセットを通してエッジケースを特定しようとしてるところだよ。

免責事項 - Tensorlakeの創設者です。私たちは開発者向けにドキュメントパースAPIを作りました。これが、PDFを解析するためのコンピュータビジョンアプローチが現実世界でうまく機能する理由なんです。ファイル内のメタデータに頼るのは、さまざまなPDFソースに対してはスケールしません。私たちはPDFを画像に変換して、まずレイアウト理解モデルを適用し、その後にテキスト認識や表認識モデルのような専門モデルを使って、結果をまとめて精度が求められる分野で受け入れ可能な結果を得ています。

Hacker Newsで議論の続きを見る