自分のPythonプロジェクトでPDFのテキスト抽出が必要になった
仕事でPDFからテキストを抽出する処理を書いていて、英語のPDFはうまくいくのに日本語のPDFで文字化けが頻発しました。
# よくある光景
import pymupdf
doc = pymupdf.open("契約書.pdf")
text = doc[0].get_text()
print(text)
ライブラリを切り替えてみても同じ。pdfminer、pypdf、pypdfium2…どれも日本語CIDフォントのPDFで程度の差はあれ化けます。
原因を調べていくうちにPDF仕様そのものに興味が湧いて、1,000ページ超のPDF Reference(ISO 32000)を章ごとに読み始めました。数ヶ月かかりましたが、読み終わった頃には「これは自分で実装するしかない」と思っていました。
それで作ったのがPDF Oxideです。Rustで書いて、PyO3でPythonバインディングを付けています。
なぜ日本語で文字化けするのか
PDFはテキストを直接保存していません。グリフID(字形番号)を保存して、フォントエンコーディング経由でUnicodeに変換する仕組みです。
英語フォントはこの変換が比較的単純なのですが、日本語フォント(CIDフォント)は別の経路を通ります。Adobe-Japan1というレジストリのCID-to-Unicodeマッピングが必要で、これが15,000エントリ以上あります。
多くのライブラリがこのマッピングを持っていないか、不完全にしか実装していません。さらにShift-JIS/RKSJデコーディングやIdentity-H CMapの伝搬処理も必要で、どれか一つ抜けると化けます。
初期バージョンでは日本語CIDフォントの処理にバグがあって、ある種のPDFで一部の文字だけ化ける(残りは正しい)という厄介な状態になりました。原因はCID-to-Unicodeマッピングの優先順位の問題で、ToUnicode CMがある場合とない場合で処理を分岐させる必要がありました。
PDF Oxideの対応
PDF Oxideでは80,000以上のCID-to-Unicodeマッピングを内蔵しています(Adobe-Japan1, GB1, CNS1, Korea1)。Shift-JIS/RKSJデコーディングとCFFフォントエンコーディングパーサーも実装済みです。
pip install pdf_oxide
from pdf_oxide import PdfDocument
doc = PdfDocument("日本語文書.pdf")
text = doc.extract_text(0) # 正しい日本語テキスト
md = doc.to_markdown(0, detect_headings=True) # Markdown変換
ベンチマーク
3,830の実PDFファイル(veraPDF 2,907、Mozilla pdf.js 897、DARPA SafeDocs 26)でテスト。テストコーパスもベンチマークコードもリポジトリで公開しています。
| ライブラリ | 平均 | p99 | パス率 | ライセンス |
|---|---|---|---|---|
| PDF Oxide | 0.8ms | 9ms | 100% | MIT |
| PyMuPDF | 4.6ms | 28ms | 99.3% | AGPL-3.0 |
| pypdfium2 | 4.1ms | 42ms | 99.2% | Apache-2.0 |
| pdfminer | 16.8ms | 124ms | 98.8% | MIT |
| pdfplumber | 23.2ms | 189ms | 98.8% | MIT |
| pypdf | 12.1ms | 97ms | 98.4% | BSD-3 |
v0.3.5で平均23.3msだったのが、プロファイリングを繰り返してv0.3.8で0.8msまで下がりました。一番効いたのはページツリーのO(n^2)探索をO(1)バルクキャッシュに変えたこと(168倍高速化)です。
テキスト抽出以外にもPDF作成(Markdown/HTML/画像から)、OCR(PaddleOCR + ONNX Runtime)、暗号化(AES-256)などに対応しています。MITライセンスです。
リンク
- GitHub: github.com/yfedoseev/pdf_oxide
- ドキュメント: oxide.fyi
日本語PDFでテストしていただけると助かります。文字化けやバグを見つけたらGitHub issueで教えてください。