PDFをパースしたいですか?
(eliot-jones.com)- PDFパースは明確な順序と構造に基づいて動作すべきですが、実際のファイルはこの規格に従っていないことがよくあります
- cross-reference(xref)ポインタやオフセット探索では、さまざまなエラーや不一致が発生します
- 実際には、PDFヘッダー前の不要なデータや、ポインタ・オフセットの誤った位置によって多くの問題が生じます
- PDFのxrefテーブル自体が不明確または誤った形式になっているケースも多くあります
- そのため主要なビューアは、非標準のPDFファイルまでサポートするロジックを追加実装しています
PDFパースへの理想的なアプローチ
- PDFパースは理論上、一定の手順で進みます
- ファイル先頭でバージョンヘッダーのコメントを探す
- cross-reference(xref)ポインタを探す
- すべてのオブジェクトオフセットを収集する
- trailer辞書を見つけて、全体のカタログ構造にアクセスする
PDFオブジェクトの紹介
- PDFオブジェクトは、数値、文字列、辞書などのさまざまなPDF要素を包んで保存する単位です
- 各オブジェクトは「
obj/endobj」マーカーの間に存在します - オブジェクト同士は、**間接参照(indirect reference、例:
16 0 R)**の方式で接続されます - ファイル内でのオブジェクト分割方法は自由ですが、一部のオブジェクト型は必ず間接参照でなければなりません
cross-referenceオフセットを探す
- PDFには構造上、cross-reference(xref)テーブルがあり、これはオブジェクト位置のインデックスとして機能します
- ファイル末尾には
startxref構文で特定のバイト位置がポインタとして示されます - このポインタがxref位置を指定しますが、仕様と実際のファイルには差異があります。たとえば
%EOFマーカーは本来最後の行にあるべきですが、実際のPDFでは末尾1,024バイト以内のどこにあってもおかしくありません - 実際のファイルでは、ポインタの書式ミス(
startrefなど)や改行欠落など、さまざまな変形が見つかります
オブジェクトオフセットを探す
- xrefテーブルは
xref、オブジェクト開始番号、オブジェクト数が順に続き、各オブジェクトのオフセット/生成番号/状態(nまたはf)が1行ずつ記録されます - xrefテーブルが複数ある場合や、
/Prevエントリで相互に連結されている場合もあります
trailer辞書の位置を探索する
startxrefマーカーの上部にはtrailer辞書があり、ルートオブジェクトを見つけるための必須メタデータが含まれます- ルートオブジェクトを基準に、全体構造の解釈を開始できます
実環境: 予期しない問題点
-
PDF仕様に準拠していないファイルが多く、一般的なパーサでは処理が困難です
-
cross-referenceポインタ探索でよく失敗するケース
- ポインタがファイル末尾、または最後の1,024バイト以内にない
- タイポ(
startrefなど) - 例外的な形式
-
実際のPDFサンプル3,977件の調査では、約0.5%がxref宣言エラーを持っていました
PDFコンテンツが0以外のオフセットで始まる
- ヘッダー前に**不要データ(junk)**があると、すべてのバイトオフセットがずれて
startxrefの位置も食い違います - ヘッダー位置を基準にオフセットを再計算する必要があり、両方の位置を確認しなければなりません
- 全体のエラーの約50%を占めます
xrefポインタがxrefテーブルの途中を指している
- 指定オフセットがxrefテーブル内容のちょうど中ほどを指してしまうことがあります
- 3,977件のサンプル中、約5件で見つかりました
ポインタがxrefの近くにある
- ポインタが正確ではなくても、xrefの直前または直後の空白や改行文字のずれ程度にとどまることがよくあります
ポインタは正しいがxrefオフセットが間違っている
- xref表に記録されたオフセット自体が誤っていることもあります
- 一部のオブジェクトだけが正しく、残りはオフセットエラーを持つ場合もあります
最初のポインタは正常だが以前のオフセット(/Prev)がおかしい
- PDFを修正して生成される**
/Prevポインタ**に誤った値(例:0)が保存されている例が多数あります
xrefテーブルの形式が異常である
- 改行なしで
xrefと数字が連結されていたり、宣言されたオブジェクト数より多くの項目があったり、表の途中にゴミデータが含まれていたりと、さまざまな形で現れます - こうしたケースは PdfPig などでも issue として多数報告されています
結論
- 仕様上、PDFパースは定型的な手順で処理されるべきですが、実際のファイルの多くはそうではなく、パースにはさまざまな問題が生じます
- 実用PDFビューアは、非規格PDFへの対応拡張機能を標準で備えています
- 今回の要約は、PDF仕様(全1,300ページ中22ページ分)に相当する一部分のパースだけを扱っています
5件のコメント
今回の要約内容は、PDF仕様(全1300ページ中22ページ)に該当する一部分のパースのみを扱っていました <-... 1300ページはものすごいですね...
わあ..
PDFは率直に言って、人が作った書式をできるだけ保った、人間が読みやすいフォーマットであり、機械との相性は最悪です。
共感します。正直、読みやすいのかもよく分からないですし……重すぎて不便ですよね。
Hacker Newsの意見
答えは明快
現実には、自分の名前はGeoffなのに、履歴書パーサーの半分は自分の名前を「Geo」と「ff」に分けて認識する
これはテキストがPDFに入る方式のせいで、複数のソースアプリで継続的に発生する問題
PDFのパースとPDFコンテンツのパースはまったく別物
PDFファイルをパースするのも面倒だが、PDF自体が「指定位置に何かを描画する」前提なので、境界ボックス内の明確に定義されたテキストとは違い、単語を抽出するにはどの文字が一緒に属しているか推測しなければならない
履歴書パーサーを助けたいなら、アクセシビリティツリーに注目する価値がある
すべてのPDFレンダラーがアクセシブルPDFを出力するわけではないが、アクセシブルPDFなら名前のようなものをまだ比較的正しく読める可能性がある
「ff」問題は、おそらく非ASCII文字(例: ff リガチャ)を履歴書解析器が処理できないケース
PDFレンダラーでリガチャを生成しないよう設定することはできるが、そうすると見た目が悪くなるかもしれない
「should」という言葉に多くを期待しすぎている感じ
PDFの利用が実際にはかなり敵対的なら、人はそこまで考えないように思う
履歴書をPDFで出すこと自体、中間の流通者に修正させないためでもあるし、「編集」も画像の上にボックスを引いて隠す、表をCSVではなくPDFにして解析しづらくする、などさまざまな理由がある
実際、この方法がうまく機能するケースもあり、一部のアプリではこの方式を使っている
ただし、2つの表現(本文/メタデータ)が実際には一致しない問題は残る
手書きスキャンやその他のスキャン文書は、スキャナーや一般家庭用コンピューターが完全なOCR対応をしていない場合どうするのか、という疑問がある
おそらくffがリガチャとしてレンダリングされて生じる問題
Tensorlakeの創業者
開発者向けの文書パースAPIを作った
PDFパースでComputer Vision方式が実運用でうまく動く理由はこれ
ファイル内メタデータだけに依存する方法は、多様なPDFソースではスケールしない
そこでPDFを画像に変換し、まずレイアウト認識モデルを適用し、その後テキストや表認識などの特化モデルを回してから断片を結合し、精度が必須な分野でも使える結果を得る方式を取っている
こういうやり方は一見すると滑稽だが、実際には最も現実的な解決策に思える
PDFは本質的に人間が読むためのレイアウトを表現するよう設計されたフォーマットであり、コンピューターが読むために設計されたものではなく、見栄えのよい表示に焦点を当てたフォーマット
だからこそ、人間が読むやり方を模倣するアプローチには理にかなっている感じがする
ただ、30年以上たってもPDFが機械可読性を高められなかった点は残念
何のインセンティブが足りなかったせいでこれを可能にできなかったのか気になる
これについて洞察のある人がいれば聞いてみたい
ちょっと滑稽な話
PDFを印刷してスキャンしてからメールで送るのは笑い話のように扱われるが、PDFパースでは実質的に同じことをしているようなもの
そうしたアプローチが必要だというのがもどかしい現実
世の中はHTMLをそんなふうにはパースしない
Nutrient.ioの共同創業者で、10年以上PDFを扱っている
Webブラウザーと同じで、PDFビューアーは非常に多様なPDFを受け入れなければならない
PDFがあまりに古いため、ファイル生成側が自分の使うビューアーで正しく見えさえすればいいように好き勝手に修正してしまうから
そこでうちの会社ではAI文書処理SDK(REST APIで、PDFを入力するとJSONの構造化データを返すもの)を作った
視覚的手法だけでなく、構造的な前処理/後処理の経験も活かして、純粋なビジョンベースより性能/コストの両面でよりよい結果を提供している
自分でPDF処理を悩みたくなく、本来の仕事に集中したいなら役に立つかもしれない
https://www.nutrient.io/sdk/ai-document-processing
PDF内部構造の専門家がいるついでに質問
なぜmupdf-glが(標準的なデスクトップLinux環境で)他のすべてのプログラムよりずっと速いのか気になる
大きなPDFの検索速度が明らかに優れていて、なぜ他のビューアーはここまで速くできないのかずっと不思議だった
関連する洞察があれば聞きたい
結局、PDFを画像としてレンダリングする際に使うソフトウェアへ、パース作業を外注したようなもの
かなり前から、レイアウト中心の文書コミュニケーションから離れるべきだと思っている
つまり、専門的に整えたレイアウト自体は実のところ古い慣習に近く、実際のコンテンツ理解とはほとんど関係がないと考えている
たとえば各種規制当局への提出書類は非常に分厚い文書で、レイアウト規則を満たすためにMicrosoft Wordで長時間作業することになる
こうしたレイアウト保証のためにDOCXやPDF形式で提出するが、これらのフォーマットはプログラムが自動で内容を抽出したり加工したりするには非常に不向き
LLMもこれらのファイルを読めはするが、単純な機械向けファイル(テキスト、markdown、XML、JSONなど)に比べると計算コストが大きい
代替として、いっそ「機械優先」「内容優先」の単純なフォーマット(JSON、XML、HTMLベースなど)を標準化する可能性を考えている
最小限の構造と画像埋め込み情報だけを持ち、人間が読むときはビューアーアプリで見やすく再構成すればよい
機械処理はずっと簡単になる
すでにHTML/ブラウザー、EPUBなど類似フォーマットは存在するものの、古典的な方式を置き換える必要がある時期だと思う
LLM革命がこうした方向へ導いてくれることを期待しており、将来は高価なPDFパースがレガシーパイプとしてだけ残ることを願う
まだDOCXパーサーを作ったことはないが、DOCXはXMLベースで、明示的にレイアウト指定しない限りすべてが絶対座標化されるわけではないので、JPEGが0点、PDFが15点、markdownが100点なら、DOCXはだいたい80点くらいの易しさではないかという推測
素晴らしい整理だと思うし、興味深かった追加ポイントがある
Incremental-saveチェーン: 最初のstartxrefオフセットは問題なくても、Acrobatが何度も修正するたびに繰り返し追加する
/Prevリンクが、次のxrefを数バイト手前に指してしまうことがよくあるほとんどのビューアー(PDF.js、MuPDF、Adobe Readerまで)では、objトークンをファイル全体から力技で探して新しいテーブルを再構築し、仕様準拠のパーサーは爆発する
実際の現場で複数アプリケーションにより繰り返し編集された文書を扱いたいなら、こうした復旧経路(salvage path)は必須
前の参照、またはチェーン内のどれか1つがファイル外オフセット、0オフセット、無効値などを指すことが多い
この文章を書くきっかけは、自分のプロジェクトPdfPigの初期パースロジックを作り直していたこと
最初はJavaのPDFBoxコードを移植したが、もっと速くシンプルにしたかった
新しいロジックはxrefテーブル/ストリームを1つでも見逃したらファイル全体をスキャンし、復旧経路ではそのオフセットだけを信頼する
しかし以前より確実に遅くなっており、この変更が本当に妥当なのか確信が持てない
1万ファイルのテストセットで各種のエッジケースを探索中
https://github.com/UglyToad/PdfPig/pull/1102
うまく動く前提と適切なPDFオブジェクトパーサーがあれば簡単そうに見えるが、現実は決してそうではないと思う
これはPDF地獄のような状況
PDFは仕様書ではなく社会的合意、「空気」のようなもの
もがけばもがくほど深く沈み、今や私たちはみな神の視界から遠い泥沼に住んでいる気分
この話には笑ってしまった
「PDFをパースしたいか」という問いに対しては、断じてノーと言える
理由は元記事でよく説明されている
自分の銀行がもっと読みやすい形式で資料を提供してくれたらいいのだが、それまではどうしようもない
その失敗はもうやったので、二度とやりたくない
PDFパーサーを書いた経験者として、PDFは本当に奇妙な形式だと感じる
これはバイナリとテキストの混在という生まれつきの設計が、この奇怪さを生んでいるのだと思う
少し雑で不正確なxrefオフセット問題も、LF/CR改行変換処理中のバグに由来するのだろうと推測する
記事で触れられていない点の1つとして、最近のPDF(v1.5+)は通常のプレーンテキストxrefテーブルではなく「xref stream」に格納されていることが多い
v1.6以降では、オブジェクト自体をobject streamに入れることもできる
一見問題なさそうでも、欲しいオブジェクトがストリーム内にあり、そのストリーム自体がPNG圧縮の変種を使っていたり、オフセットがflate圧縮されたxref stream内にあったりすると厄介になる
しかも複数の文書バージョンが入り混じっていて、どこからどこまでが最新なのか見極めるのも複雑
PDF 1.7の文書までは入手しやすいが、つい2年前までPDF 2.0仕様書は有料の壁の向こうにあった
PDFはストリーミングを考慮していないフォーマット
末尾にあるtrailer dictionaryのせいで、ファイル全体が読み込まれるまでパースしづらい
ただし、「ストリーミング可能なPDF」も存在し、先頭部分に必要情報があれば最初のページはすぐレンダリングできる(残りはそうとは限らない)
最近はPDFから少し離れているので、その点は差し引いてほしい
フッターがあっても、WebサイトがRange Requestをサポートし、Content-Lengthヘッダーをきちんと使っていれば、PDFもストリーミング可能
ストリーミングリーダーはHEADリクエストと、ファイル末尾数百バイトの要求でポインターとテーブルを取得し、その後に残りを受信すればよい
リアルタイム生成PDFには向かなくても、かなり古いWebサーバーでも往復遅延が1〜2回増えるだけで十分
残念ながらファイルごとのRangeベースパーサーに気を配る例は少ないが、技術的に不可能というわけではないと思う
その通り、Linearized PDFという形式があり、ファーストページをファイル全体をダウンロードせずにすばやく表示できるよう設計されている
要約ではその方式は補足説明が多くなるため省略したと伝えている
Pythonを学んで最初に挑戦したプロジェクトの1つがPDFパーサーだった
DnDキャンペーン用の地図を自動抽出したかったが、結果は失敗だった(笑)
TIFFリーダーを書いたことがある
TIFFも書くのは簡単だが読むのは難しいことで悪名高い
PDFもまったく同じ系統に入る気がする