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

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を画像に変換して、まずレイアウト理解モデルを適用し、その後にテキスト認識や表認識モデルのような専門モデルを使って、結果をまとめて精度が求められる分野で受け入れ可能な結果を得ています。

つまり、PDFを画像としてレンダリングするために使っているソフトウェアにパースをアウトソースしてるってことだね。

「これが、PDFを解析するためのコンピュータビジョンアプローチが現実世界でうまく機能する理由なんです。」でも、PDFの一番の利点は、見えないデータを含められることだよね。例えば、私の履歴書には、私が働いていた会社で働いていた証明を埋め込むことができるんだ。でも、ビジョンベースのアプローチでは、それをキャッチするのは無理だよね。

「これが、PDFを解析するためのコンピュータビジョンアプローチが現実世界でうまく機能する理由なんです。」まあ、正直言うと、問題の文書はスキャンされた画像だけだから、どうしようもない場合も多いよね。そこで見た一番難しい問題は、ナラティブなタイポグラフィアートブックや、複雑なテキストと写真が混ざったデパートのカタログ、古い都市地図なんだ。

これ、OPにはほとんど関係ないね。

ここにPDF内部の専門家がいるけど、ちょっと聞きたいことがあるんだ。mupdf-glが他のソフトよりもずっと速いのはなんでだろう?(バニラのデスクトップLinuxで)大きなPDFの検索速度が、試した中で圧倒的に速いんだよね。他のソフトもmupdf-glみたいに速くできないのはどうしてなんだろう?何かヒントがあれば教えて!

ちょっと面白いね。PDFを印刷して、それをスキャンしてメールに送るなんて、普通は大バカにされることだよ。でも、実際にはそれを解析するためにやってるんだよね。わかるよ、他にも同じことしてる人がいるって聞いたことあるし。ただ、そんなことが必要だなんて、本当にイライラするよね。世の中、HTMLをそんな風に解析することはないのに!

これをやっている間に、PDFファイルの作成をやめるようにみんなに言ってもらえませんか?最終的には新しいPDFの数がゼロになるように。マネージャーたちが「コンピュータに紙を入れる方法」と決めてしまってから、このフォーマットには希望がなくなったよね。本来の出版中間フォーマットじゃなくて、デジタル化の曖昧な模倣が広まってしまった感じ。

ちょっと気になったんだけど、君の方法って最終的には最初にPDFを解析して表示するために使ったプログラムよりも、より良い解析を生み出すの?それとも、異なる入力パーサーの解析を統一することに価値があるのかな?

馬鹿げてるように聞こえるかもしれないけど、理論的にはこれが問題に対する最良のアプローチだと思う。PDFは人間が消費するための出力を生成することを意図しているはずで、フォーマットはデータを人間が(できれば)簡単に読めるように表示することに焦点を当てているように見える。ここでは、人間のアプローチを模倣する技術を使っているみたいで、理にかなっていると思う。ただ、30年以上経っても機械がPDFを読みやすくするための一貫した方法を追加できなかったのは悲しいね。何が足りなかったのか、ちょっと気になる。誰か知ってる人いる?

私も同じことをやってるけど、ドキュメント検索のためにね。ColqwenとClaudeみたいなVLMを使ってるよ。

こんなにうまくて勇気のある紹介をありがとう。最近は、PDFのASCII「Postscript」形式を一目で認識できる人はほとんどいないよね。最初のステップは、もちろんASCIIに展開して、Flate/ZIP、LZW、RLEの最初のラッパーを取り除くこと。最近、Geminiが.PDFを受け入れて、.EPUBを受け入れないことをからかったら、PDFサポートが不透明でライブラリ指向だと謝ってた。それはすごく人間らしい反応だったね。LZWラッパー形式の簡単なまとめを除けば、リニアライズや「ページXでの最初の使用」によるオブジェクトの再配置を深く掘り下げて、各ページの前に再度書き出すのはいいプロジェクトになりそう。UglyToadって、痛みが好きな人にはぴったりの名前だね。;-)

PDFパーサーを書いたことがある者として言うけど、これは確かに見た中で一番変なフォーマットの一つだと思う。私の意見では、多くはバイナリとテキストのミックスを目指したせいで変になってる。少なくとも、悪い「不正確だけど近い」xrefオフセットの奇妙なケースのいくつかは、LF/CR変換を扱うバグのあるコードが原因かもしれない。記事には書かれてないけど、新しいPDF(v1.5+)の多くは、通常のテキストのxrefテーブルすら持ってなくて、xrefテーブル自体が「xrefストリーム」の中にあるんだ。v1.6+では、オブジェクトを「オブジェクトストリーム」の中に入れるオプションもあると思う。

そうだね、これが単純なxrefテーブルを超えて、ストリームや圧縮にまで入っていかないのはちょっと驚いたよ。欲しいオブジェクトが、変なPNG圧縮のリフで使われているストリームの中にあるって気づくまでは、そんなに悪くないように思えるんだ。オフセットがxrefストリームの中にあって、それが文書に後から追加されたフレート圧縮のものだから、ファイルの最後にあるプレーンなものから始めて、どのオブジェクトのどのバージョンがどこにあるかを考えなきゃいけない。1.7のドキュメントは簡単に見つかるけど、2年前までは2.0のドキュメントは有料だったからね。

https://digitalcorpora.org/corpora/file-corpora/cc-main-2021... には、Common Crawlの単一のクロールで取得した800万のPDFファイルのセットがあるよ。

答えは明らかだと思うな。1. PDFは好きな形式で任意のメタデータを添付・含むことができる。2. だからPDFを生成するものは、同じ情報を機械に優しい形式で添付すべきだ。3. そうすれば、PDFを「解析」したい人はメタデータを参照できるようになる。実際の観点から言うと、僕の名前はGeoffなんだけど、半分の履歴書解析ツールは僕の名前を「Geo」と「ff」に分けて解釈するんだ。これはPDFにテキストが配置される方法が原因なんだよね。いろんなソースアプリケーションから起こることなんだ。

たぶん、ffが合字として表示されるからじゃないかな。

PDFを解析するのと、PDFの内容を解析するのでは大きな違いがあるよ。PDFファイルの解析は本当に地獄だけど、PDFは基本的に「特定の位置にあるもの」であって、「境界ボックス内の整形されたテキスト」ではないから、テキストを単語として解析したいなら、どの文字が一緒に属するのかを推測しなきゃいけない。履歴書のパーサーを手伝いたいなら、アクセシビリティツリーを見てみて。すべてのPDFレンダラーがアクセシブルなPDFを生成するわけじゃないけど、アクセシブルなPDFはクソみたいなAIパーサーが名前を正しく取得するのに役立つよ。ffの問題については、おそらく履歴書アナライザーがffの合字のような非ASCIIテキストに対応できていないんだと思う。PDFレンダラーにそういう合字を生成しないように影響を与えられるかもしれないけど、その分、見た目が悪くなることが多いよ。