PDFをAIに読ませるパイプラインで、コストと待ち時間の大半を食っているのはOCRです。ところが実務で届くPDFの多くは、そもそも文字データが最初から埋め込まれた「テキストベース」のファイルで、OCRにかける必要がありません。pdf-inspector(firecrawl/pdf-inspector・スター約1.6万・MIT)は、この「OCRが要るのか要らないのか」を数十ミリ秒で判定し、要らない文書だけをその場でMarkdown化してしまうRust製のライブラリです。RAGの文書取り込み層をどう組むかという全体像はRAGとは?仕組み・構築・ベクトルDB選定までの2026年実装マップにまとめていますが、本記事はその最前段にあたる「前処理の振り分け」に絞って、v1.14.2を実際に動かした結果を書きます。
TextBased と判定し、334msでMarkdownまで出している30秒でわかる pdf-inspector
・やること:PDFを text_based / scanned / image_based / mixed の4種に分類し、テキストベースならOCRなしでMarkdown化する
・やらないこと:OCRそのもの。画像から文字を読む機能は無く、「このページはOCRが必要」と返すまでが担当
・実測速度:15ページのPDFで判定24.6ms、Markdown化まで96.2ms(Apple Silicon・v1.14.2)
・依存の軽さ:MLモデルなし・外部サービスなし。PDFパースの依存は lopdf 1つだけ
・注意点:既定の判定は全ページを見ない。後述の条件で「OCRが必要なページ」を取りこぼす
この記事の位置づけ:READMEの翻訳ではなく、npm・PyPIから配布物を実際に入れ、自作の検証用PDFを食わせた実測を軸にしています。READMEの主張が再現できた点も、READMEと実装がずれている点も、両方そのまま書きます。
pdf-inspectorとは——OCRに投げる前に「投げるか」を決めるOSS
pdf-inspectorは、Web上のデータをLLM向けに整形するサービスを提供するFirecrawlが2026年2月に公開したOSSです。2026年8月16日時点でGitHubスターは約1.6万、ライセンスはMIT、最新版はv1.14.2(2026年8月13日リリース)。crates.ioでの累計ダウンロードは99,590件を数えます。
このライブラリの発想は、READMEの一文に集約されています。Firecrawlは自社のパイプラインで扱うPDFのうち約54%はOCRを必要としないと述べており、その分をローカルで200ms以内に処理してOCRサービスをスキップするために作った、と説明しています。つまりpdf-inspectorは「高精度なPDF解析エンジン」ではなく、高精度なエンジンに何を渡すかを決める門番です。
4つの分類と、それぞれの意味
判定結果は4種類の文字列で返ります。実務ではこの4値がそのまま分岐条件になります。
| 判定値 | 意味 | 典型例 | 推奨アクション |
|---|---|---|---|
text_based |
全ページにテキスト層がある | 論文・仕様書・請求書のPDF出力 | そのままローカル抽出(OCR費用ゼロ) |
scanned |
テキスト層が無く画像だけ | 紙をスキャンした契約書 | 全ページOCRへ |
image_based |
画像主体でテキストがごく僅か | 図面・スライドの画像化PDF | 全ページOCRへ |
mixed |
テキストページと画像ページが混在 | 本文+スキャン添付の年次報告書 | 該当ページだけOCRへ |
実務的にいちばん価値があるのは mixed です。300ページの報告書のうち画像ページが11枚だけ、という文書に対して全ページOCRを投げるのは無駄が大きい。pdf-inspectorは pages_needing_ocr として1始まりのページ番号リストを返すので、ページ単位で振り分けられます。
MLモデルを持たないという設計判断
pdf-inspectorはニューラルネットワークを一切使いません。PDFの中身(コンテンツストリーム)を直接読み、テキスト描画命令である Tj / TJ と画像描画命令 Do の出現を数えて分類します。この割り切りが速度と導入コストに効いています。モデルの重みをダウンロードする必要がなく、GPUも不要で、Rustの単一バイナリとして動きます。
同じ「文書をAIに食わせる形に整える」領域でも、officeParser入門:Node.jsでdocx・xlsx・pptxをAST解析してRAGに繋ぐで扱ったようなOffice文書のパーサや、Google LangExtract完全ガイド:LLMで非構造テキストから構造化抽出、ソース位置も追跡のようなLLMベースの抽出器とは、担当するレイヤーが異なります。pdf-inspectorはもっと手前の、バイト列を見て振り分けるところにいます。
インストールと最小の使い方——Node・Python・CLI・ブラウザ
配布は4系統あり、すべてv1.14.2で揃っています(crates.io / npm / PyPI / npm-wasm、いずれも2026年8月13日更新)。
READMEのPythonクイックスタートは pip install maturin してソースからビルドする手順になっていますが、実際にはPyPIにビルド済みwheelが公開されているため、利用するだけならRustツールチェインは不要です。macOS(x86_64 / arm64)・manylinux(x86_64 / aarch64)・Windows向けのabi3 wheelが揃っており、次の1行で入ります。
pip install pdf-inspector
Node.jsも同様に、napi-rsのプリビルドバイナリが降ってきます。
npm install @firecrawl/pdf-inspector
最小の判定コードはこれだけです。ファイルパスを渡すと分類結果とMarkdownが一度に返ります。
import pdf_inspector
r = pdf_inspector.detect_pdf("document.pdf")
print(r.pdf_type) # text_based / scanned / image_based / mixed
print(r.confidence) # 0.0-1.0
print(r.pages_needing_ocr) # OCRが必要な1始まりページ番号
CLIも同梱されており、cargo install pdf-inspector で pdf2md と detect-pdf の2コマンドが入ります。pdf2md document.pdf --json でMarkdownとメタ情報をまとめてJSONに吐けるため、シェルスクリプトからの利用も想定されています。
バインディングごとに使える関数の数が違う
ここは導入前に確認しておく価値がありました。npmとPyPIから実際に入れて公開シンボルを数えたところ、同じRustコアなのにバインディングによって公開されているAPIの数が3倍以上違います。
| 機能 | Node.js | Python | ブラウザWASM |
|---|---|---|---|
分類・判定(classifyPdf / detectPdf) |
○ | ○ | ○ |
Markdown変換(processPdf) |
○ | ○ | ○ |
| 位置情報つきテキスト抽出 | ○ | ○ | ✕ |
| ページ単位のMarkdown抽出 | ○ | ○ | ✕ |
| 構造要素の抽出 | ○ | ○ | ✕ |
表の構造抽出(extractTablesWithStructure 系) |
○ | ✕ | ✕ |
ベクター罫線の検出(detectVectorGridInRegion) |
○ | ✕ | ✕ |
非同期版(*Async) |
○ | ✕ | ✕ |
| 公開関数の総数 | 18 | 16 | 5 |
表をセル単位で構造化して取り出したい場合、現時点ではNode.jsを選ぶしかありません。Python版にも extract_text_in_regions(座標範囲を指定してテキストを取る)はあるので、表の位置が既知なら回避はできますが、罫線から表を自動検出してセルに割るAPIはNodeのみです。
もう一点、READMEには EarlyExit / Full / Sample(n) / Pages(vec) という4つの走査戦略(ScanStrategy)の表が載っていますが、この戦略を選べるのはRustのAPIだけでした。src/python.rs・napi/src/lib.rs・wasm/src/lib.rs のいずれも PdfOptions::new() を呼ぶだけで走査戦略には触れておらず、3つのバインディングはすべて既定値のまま動きます。
実測——判定・抽出の速度とMarkdownの中身
READMEの速度主張が手元で再現するかを確認しました。検証環境はApple Silicon(M系)、pdf-inspector v1.14.2のPyPI wheel、計測は同一処理を5回実行した中央値です。
題材には「Attention Is All You Need」の公開PDF(15ページ・2.11MB)を使いました。
| 対象PDF | ページ数 | 判定 | Markdown化まで | 出力文字数 | 判定結果 |
|---|---|---|---|---|---|
| 論文(テキストベース) | 15 | 24.6ms | 96.2ms | 40,573字 | text_based(確信度1.00) |
| 混在(自作・11ページが画像) | 30 | 10.8ms | 30.7ms | 45,072字 | mixed(確信度0.73) |
| スキャン(自作・全ページ画像) | 8 | 3.5ms | 3.4ms | 0字 | scanned(確信度0.95) |
READMEが掲げる「判定10〜50ms」「テキストベースPDFをローカルで200ms未満」は、いずれも再現できました。スキャンPDFで抽出が3.4msで終わっているのは、テキスト層が無いと判定した時点で抽出処理を打ち切るためです。無駄な処理をしないという意味で正しい挙動です。
出力されたMarkdownも実用に足る品質でした。見出しはフォントサイズ比から ## / #### に振り分けられ、著者名の脚注記号やメールアドレスも落ちていません。2段組の本文が読み順どおりに直列化されている点は、素朴なテキスト抽出との明確な差です。
ブラウザWASM版では同じ論文PDFが334msで処理されました。ネイティブの96.2msに対して約3.5倍かかりますが、PDFを一切サーバーに送らずに完結する点と引き換えと考えれば妥当な数字です。機密文書の一次判定をクライアント側で済ませたい構成では選択肢になります。
実測でわかった落とし穴——「OCR不要」の答えがAPIで食い違う
ここからが本題です。READMEに書かれていない挙動を2つ見つけました。どちらもこのライブラリの主用途である「OCRの振り分け」に直接影響します。
既定の走査は全ページを見ていない
READMEの「How classification works」には、走査戦略の既定は EarlyExit(全ページを走査し、テキストの無いページで打ち切る)と書かれています。ところが実装を読むと、既定値は違いました。
src/detector.rs の DetectionConfig::default() は ScanStrategy::Sample(8) を返します。コード中のコメントには「EarlyExitは、画像だけの表紙のあとにテキストページが続くPDF(年次報告書など)に対して過剰に反応する」と、変更した理由まで書かれています。つまり実装側の判断は妥当なのですが、READMEの記述が追随していません。
Sample(8) は、先頭・末尾・その間を等間隔に割った最大8ページだけを見ます。30ページの文書なら1・5・9・13・17・21・25・30ページです。残る22ページは分類の判断材料に入りません。
text_based と判定された瞬間、OCR対象リストは空になる
問題はここから先です。ページ単位のOCRリスト(pages_needing_ocr)を組み立てる処理は、分類結果によって挙動が変わります。
・mixed と判定された場合のみ、全ページを1枚ずつ解析してリストを作る
・scanned / image_based なら、中身を見ずに「全ページ」を返す
・text_based なら、中身を見ずに空リストを返す
したがって、抜き取った8ページがすべてテキストページだった場合、文書は text_based と分類され、その時点でOCR対象リストは空で確定します。サンプリングの隙間に画像ページが潜んでいても、報告されません。
これを確認するため、30ページのPDFを作り、11ページ目と12ページ目だけを画像化しました。この2ページは Sample(8) が見る8ページのどれにも当たりません。結果は次のとおりです。
detect_pdf() は text_based、確信度 1.0(最大値)、pages_needing_ocr は 空リストを返しました。確信度が最大のまま、2ページ分の内容が静かに欠落します。実際、Markdown出力は38,322字で、11・12ページ目の中身はどこにも含まれていません。
一方、同じライブラリの extract_pages_markdown() は同じファイルに対して pages_needing_ocr = [11, 12] を返し、該当ページの needs_ocr が True になりました。こちらは全ページを走査するためです。
振り分けの正確さが要るなら detect_pdf() だけで判断しない
・速さ優先の一次判定なら detect_pdf() / classify_pdf() で十分(数十ms)
・取りこぼしが許されない振り分けでは extract_pages_markdown() を使い、返り値の pages_needing_ocr と各ページの needs_ocr を見る
・Rustから使う場合は DetectionConfig の strategy に ScanStrategy::Full を明示すれば全ページ走査になる(バインディングからは指定できない)
・確信度1.0は「サンプリングした範囲では確実」という意味であって、文書全体の保証ではない
自分の環境で同じ確認をするなら、次のコードで済みます。判定経路と全ページ走査経路の答えが一致するかを見るだけです。
import pdf_inspector as pi
path = "your.pdf"
fast = pi.detect_pdf(path)
full = pi.extract_pages_markdown(path)
print("fast:", fast.pdf_type, fast.pages_needing_ocr)
print("full:", full.pages_needing_ocr)
if set(fast.pages_needing_ocr) != set(full.pages_needing_ocr):
print("!! 不一致: 速い経路が取りこぼしています")
ページ番号が0始まりと1始まりで混在している
もう1つ、振り分けコードで事故になりやすい点があります。同じライブラリの中で、ページ番号の基点が統一されていません。8ページ全部がスキャンのPDFで確認した結果です。
| 呼び出し | 返り値 | 基点 |
|---|---|---|
detect_pdf().pages_needing_ocr |
[1, 2, 3, 4, 5, 6, 7, 8] |
1始まり |
classify_pdf().pages_needing_ocr |
[0, 1, 2, 3, 4, 5, 6, 7] |
0始まり |
extract_pages_markdown().pages の .page |
[0, 1, ...] |
0始まり |
extract_pages_markdown().pages_needing_ocr |
[11, 12] |
1始まり |
型スタブ(pdf_inspector.pyi)にはそれぞれ “1-indexed” / “0-indexed” と明記されているので仕様どおりではあります。ただしextract_pages_markdown() が返す1つのオブジェクトの中に、0始まりの page と1始まりの pages_needing_ocr が同居している点は要注意です。速度チューニングのために detect_pdf() を classify_pdf() に差し替えると、OCRへ送るページが1つずつずれます。
pdf-inspectorとMinerU型フル解析エンジンの使い分け
日本語圏でPDFのMarkdown化というとMinerU|PDFをマークダウンに変換するOSSツールのような、レイアウト検出・数式のLaTeX化・多言語OCRまで内蔵したエンジンが定番です。pdf-inspectorはこれらの競合ではなく、前段に置くものと考えるのが正確です。
| pdf-inspector | MinerU型のフル解析エンジン | |
|---|---|---|
| 主目的 | 分類とルーティング、軽量抽出 | 高精度な文書構造の復元 |
| OCR | 無し(必要ページを通知するだけ) | 内蔵 |
| MLモデル | 使わない | 使う(レイアウト検出・数式認識など) |
| スキャンPDF | 扱えない(OCRへ回す前提) | 扱える |
| 数式 | 対応しない | LaTeX化に対応 |
| 速度 | 15ページで約96ms | モデル推論のぶん桁違いに遅い |
| 導入コスト | パッケージ1つ・GPU不要 | モデル配布物とGPUを考慮 |
役割分担はシンプルです。pdf-inspectorで分類し、text_based は自前で処理して終わり、scanned / image_based / mixed の該当ページだけをMinerUやOCR APIへ送る。Firecrawlが言う「54%はOCRを必要としない」が自社データでも成り立つなら、重いエンジンの処理量をその割合ぶん減らせる計算になります。
(実測 3〜25ms)"] B -->|"text_based"| C["ローカル抽出
(実測 96ms・課金なし)"] B -->|"mixed"| D["pages_needing_ocr を取得
※全ページ走査の経路を使う"] B -->|"scanned / image_based"| E["全ページをOCRへ"] D --> F["該当ページだけOCRへ"] C --> G["Markdown をRAGへ投入"] E --> H["OCR結果を整形"] F --> H H --> G
なお mixed の分岐では、前章のとおり detect_pdf() の結果をそのまま信じず、全ページを走査する経路でページ番号を取り直すのが安全です。
ベンチマークは再現できるのか
READMEには opendataloader-bench(200件のPDFコーパス)での比較表が載っています。pdf-inspectorが総合0.875でトップ、200件の処理が0.470秒という数字です。この種の自社ベンチは、数値だけ載って再現手段が無いことが珍しくないため、公開データの中身を確認しました。
結論として、再現用のデータは実際にコミットされています。README記載の再現ブランチには3,750ファイルが入っており、内訳は次のとおりでした。
・15エンジン分の予測結果:各203ファイル(pdf-inspector・liteparse・opendataloader・pymupdf4llm・markitdown に加え、mineru・marker・docling・nutrient・unstructured なども含む)
・正解データ:ground-truth/markdown/ に200件
・評価チャート:総合・読み順・表構造・見出しなど7種のPNG
・過去実行の履歴:history/ 配下に日付別の evaluation.csv / evaluation.json
つまり第三者が予測結果と正解を突き合わせて検証できる状態にあります。この点は正直に評価してよい部分です。
一方で、READMEの表の読み方には注意が要ります。掲載されている5エンジンは、データが存在する15エンジンからの抜粋です。README自身が「モデルベースのPDF解析を行わないローカルエンジンのみを表示し、OCRは無効化した」と条件を明記しており、意図的な絞り込みであることは明示されています。裏を返せば、この表はMinerUやmarkerのようなMLベースのエンジンとの優劣を示すものではありません。両者はそもそも土俵が違い、OCRを切った状態でMLベースのエンジンを比較しても本来の性能は出ないためです。
READMEの数値のうち、裏取りできたもの・できなかったもの
・再現できた:判定10〜50ms(実測24.6ms)、テキストPDF200ms未満(実測96.2ms)
・データを確認できた:ベンチの予測結果・正解データ・チャートは公開ブランチに実在
・出典を確認できなかった:「PDFの約54%はOCR不要」という割合。Firecrawl自社パイプラインの経験値と読めるが、README内に根拠となる調査へのリンクは無い。自社データで再計測する前提の数字として扱うのが無難
導入前に確認しておくこと
実際に組み込む場合のチェックリストです。実測を踏まえた優先順で並べています。
・振り分けの正確さと速度のどちらが要るかを先に決める。速度優先なら detect_pdf()、取りこぼし回避なら extract_pages_markdown()。両者は同じファイルでも違う答えを返す
・ページ番号の基点を関数ごとに確認する。detect_pdf() は1始まり、classify_pdf() は0始まり。差し替え時にずれる
・表の構造抽出が必要ならNode.jsを選ぶ。Python・WASMには該当APIが無い
・走査戦略を制御したいならRustから使う。3つのバインディングはいずれも既定値固定
・OCRは別途用意する。pdf-inspectorは文字認識をしない
・日本語PDFは自前のサンプルで確認する。CIDフォントのToUnicode CMap解読やUTF-16BE対応は実装されているが、公開ベンチのコーパスに日本語文書がどれだけ含まれるかは明示されていない
・暗号化PDFはパスワードを渡す。PdfOptions に password フィールドがあり、デバッグ出力では [REDACTED] に伏せられる実装になっている
導入判断としては、すでにOCR APIへ全件を投げているパイプラインがあるなら、前段に挟む価値は高いと言えます。判定コストが数十msに対してOCRは秒単位かつ従量課金なので、テキストベースの文書が一定割合あれば投資回収は早い。逆に、扱う文書がほぼ全てスキャンPDFなら、このライブラリが返すのは「全ページOCRへ」という結論だけなので、得られるものは多くありません。
まとめ
pdf-inspectorは、PDF解析の精度を競うツールではなく、重い処理に何を渡すかを数十msで決めるための道具です。実測ではREADMEの速度主張がそのまま再現でき、MLモデル無し・依存1つという軽さも確認できました。ベンチマークの再現データが実際に公開されている点も、この種の自社ベンチとしては誠実な部類です。
一方で、既定の走査が全ページを見ないこと、その結果 text_based と判定された文書ではOCR対象リストが確信度1.0のまま空で返ること、ページ番号の基点が関数間で揃っていないことは、READMEからは読み取れませんでした。いずれも「OCRの振り分け」という本来の用途に直結する挙動なので、組み込む前に自分のPDFで一度突き合わせておくことを勧めます。本記事に載せた10行程度の確認コードで足ります。
参照ソース
・firecrawl/pdf-inspector — GitHub公式リポジトリ(README・src/detector.rs・pdf_inspector.pyiを参照。2026-08-16確認)
・pdf-inspector 公式デモサイト(ブラウザWASM版)(2026-08-16確認)
・opendataloader-bench 再現ブランチ(15エンジン分の予測データと正解データ。2026-08-16確認)
・pdf-inspector — crates.io(v1.14.2・累計99,590ダウンロード。2026-08-16確認)