PDF 处理系统经常把所有文件交给同一个解析器,遇到扫描件、图片型报告或混合文档时,再用 OCR 兜底。这样做会让本来带有文本层的 PDF 也进入高延迟流程。Firecrawl 开源的 pdf-inspector 把判断步骤独立出来:先识别文档类型,再决定哪些页面可以本地提取,哪些页面需要 OCR,最后把可提取内容整理为 Markdown。

这个项目采用 Rust 编写,核心依赖是 lopdf,不需要机器学习模型和外部解析服务。仓库同时提供 Rust、Python、Node.js 和浏览器 WebAssembly 接口,并带有 pdf2md、detect-pdf 两个命令行工具。对于报告、论文、发票、法律文档等原生文本 PDF,它的定位是一个本地预处理层,负责把后续 OCR 服务不需要处理的文件筛出去。
先判断 PDF 属于哪一类
pdf-inspector 返回四种类型:TextBased、Scanned、ImageBased 和 Mixed。这四种结果对应不同的处理路径。
| 类型 | 文档特征 | 常见后续动作 |
|---|---|---|
TextBased | 页面存在可提取文本,文本页比例达到阈值 | 直接进入本地文本提取 |
Scanned | 没有可提取文本,页面主要由扫描图像组成 | 将页面交给 OCR |
ImageBased | 没有合格文本,但存在图片或矢量化文字 | OCR 或专门的图像解析 |
Mixed | 文本页与图像页并存,或模板图像承担重要内容 | 只对指定页面进行 OCR |
识别依据来自 PDF 内容流里的操作符。源码会分析 Tj、TJ 等文本操作,也会检查 Do 图像操作、图片数量、文本字符数量、矢量文字以及字体编码信息。文本操作数量高,并不自动意味着页面适合直接提取:项目还会排除文本量过低、图片占主导、文字已经被矢量化,以及字体无法可靠解码的页面。
当前源码的默认检测配置是 Sample(8),最多均匀抽取 8 个页面参与初步判断;每页至少需要 3 个文本操作,文本页比例阈值为 0.6。当页面包含图片时,文本操作的有效下限会提高到至少 10 个,避免一张带少量隐藏文字的扫描图被当成原生文本页。
源码中的分类顺序也很有代表性。含有模板图像且同时存在文本的文档会被标记为 Mixed;文本页比例达到阈值时标记为 TextBased;没有合格文本但有图像或矢量文字时,结果会在 Scanned 与 ImageBased 之间区分;文本和图像同时存在时则归为 Mixed。结果里还包含置信度、页数和需要 OCR 的页面列表。
OCR 路由是页级的
扫描件识别最实用的结果是具体页码。调用方可以据此只把相应页面送入 OCR。Scanned 和 ImageBased 通常会把全部页面列入 OCR;Mixed 只列出图像页或文本质量不足的页面。
项目还会检查两类字体问题。一类是 Identity-H 或 Identity-V 字体缺少 ToUnicode 映射,另一类是只有 Type3 字体却没有可用映射。PDF 里可能存在文本操作符,但调用方拿到的字符仍然不可解码,这些页面会追加到 OCR 列表中。页级结果还可以记录 scanned、no_text、vector_text 和 suspected_garbled_text 等原因,方便上层系统记录路由依据。
这套设计适合下面的流水线:
收到 PDF
-> pdf-inspector 分类
-> TextBased 且置信度足够
-> 本地提取文本和 Markdown
-> Scanned / ImageBased / Mixed 的指定页面
-> 只把这些页面送入 OCR
-> 合并本地提取结果与 OCR 结果pdf-inspector 本身不提供 OCR 服务,也不负责替调用方选择云端供应商。它提供的是一个可以放在 OCR 前面的确定性判断层。对于包含封面、签章页、扫描附件的混合报告,这种路由比“整份 PDF 统一 OCR”更容易控制延迟和费用。
一次加载,三种处理模式
高层 Rust API 通过 process_pdf_with_options 组织处理。源码会先加载文档一次,再把同一个文档对象交给检测和提取阶段,减少重复解析。默认模式是 ProcessMode::Full,会完成分类、文本提取、布局分析和 Markdown 转换。
另外两种模式适合拆分服务职责:
ProcessMode::DetectOnly只返回分类和路由信息,适合上传入口或消息队列消费者。ProcessMode::Analyze完成文本与布局分析,但不生成最终 Markdown,适合需要自己格式化内容的系统。ProcessMode::Full返回完整 Markdown,适合直接接入知识库、搜索索引或 RAG 预处理。
检测策略也可以调整。EarlyExit 扫描时遇到明显的非文本页就提前结束,适合只关心“能否走快速文本路径”的入口;Full 扫描全部页面,适合需要更准确区分 Mixed 和 Scanned 的场景;Sample(n) 采样均匀分布的页面,适合超长文档;Pages(vec) 只检查调用方指定的 1-based 页码。
Rust 中的最小调用如下:
use pdf_inspector::process_pdf;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let result = process_pdf("document.pdf")?;
println!("Type: {:?}", result.pdf_type);
println!("Confidence: {:.0}%", result.confidence * 100.0);
println!("Pages: {}", result.page_count);
if let Some(markdown) = result.markdown {
println!("{markdown}");
}
Ok(())
}只做路由判断时,可以使用 detect_pdf:
use pdf_inspector::detect_pdf;
let info = detect_pdf("document.pdf")?;
if !info.pages_needing_ocr.is_empty() {
println!("OCR pages: {:?}", info.pages_needing_ocr);
}需要处理上传到内存中的文件时,可使用 process_pdf_mem 或 detect_pdf_mem,避免先把字节写入临时文件。
Markdown 转换处理了哪些结构
项目的提取器保留文本位置、字体、页码和链接信息,再交给布局与 Markdown 模块处理。Markdown 转换包括以下结构:
- 标题。通过字体大小层级识别 H1 至 H4,并对接近的字号做聚类。
- 列表。识别项目符号、数字列表和字母列表。
- 代码块。根据 Courier、Consolas、Monaco、Menlo、Fira Code、JetBrains Mono 等等宽字体以及关键词特征判断。
- 表格。既检查 PDF 绘图操作里的矩形边界,也根据文本对齐关系做启发式识别。
- 多栏布局。对报纸、论文和报告中的多列文本重新排列阅读顺序。
- 字体与编码。支持 CID、Type0、Identity-H、UTF-16BE、UTF-8 和 Latin-1 等编码路径,并标记无法可靠解码的页面。
- 链接与页面元素。保留 URL,过滤页码,处理脚注、标题首字下沉、上下标、断词和目录点线。
表格处理采用两条路径的原因很实际:有些 PDF 使用线条明确画出网格,有些 PDF 只通过列对齐表达表格结构。前者适合读取矩形绘图操作,后者需要比较文本项的 X 坐标、行间距和列关系。项目还提供 extract_text_with_positions,调用方可以拿到每个文本项的坐标、字号、字体、粗体和斜体等信息,做二次排版或自定义结构化抽取。
Python 接口的用法更短:
import pdf_inspector
result = pdf_inspector.process_pdf("document.pdf")
print(result.pdf_type)
print(result.confidence)
print(result.pages_needing_ocr)
print(result.markdown)如果只需要分类,可以调用 detect_pdf;如果要逐页处理,可以使用 extract_pages_markdown。逐页结果包含 needs_ocr,同时返回带表格页面、带多栏页面和需要 OCR 的页面列表,适合把本地解析与 GPU OCR 组合起来。
CLI 适合接入现有脚本
安装 Rust CLI:
cargo install pdf-inspector将 PDF 转为 Markdown:
pdf2md document.pdf机器处理时可以使用 JSON:
pdf2md document.pdf --json
pdf2md document.pdf --items-json
pdf2md document.pdf --raw
pdf2md document.pdf --compact
pdf2md document.pdf --pages
pdf2md document.pdf --select-pages 1,3,5-10--json 返回分类、页数、置信度和 Markdown;--items-json 返回带位置的 TextItem;--raw 只输出 Markdown;--compact 折叠目录点线等冗余格式,减少后续 token 消耗;--pages 插入页面分隔标记;--select-pages 允许只处理指定页面。检测命令支持:
detect-pdf document.pdf
detect-pdf document.pdf --json
detect-pdf document.pdf --analyze --json--analyze 会额外返回表格页、多栏页和 OCR 页,适合在解析前建立文档画像。
Benchmark 该怎么看
项目 README 引用了 opendataloader-bench 的 200 份 PDF 语料,比较对象都是本地解析器,测试关闭 OCR。结果表中,pdf-inspector 的整体分数为 0.875,阅读顺序 NID 为 0.915,表格 TEDS 为 0.814,标题 MHS 为 0.788,处理 200 份文档的速度为 0.470s。
| 引擎 | 整体 | 阅读顺序 NID | 表格 TEDS | 标题 MHS | 200 份文档速度 |
|---|---|---|---|---|---|
| pdf-inspector | 0.875 | 0.915 | 0.814 | 0.788 | 0.470s |
| liteparse | 0.873 | 0.913 | 0.693 | 0.811 | 0.750s |
| opendataloader | 0.831 | 0.902 | 0.489 | 0.739 | 2.569s |
| pymupdf4llm | 0.735 | 0.886 | 0.401 | 0.424 | 17.117s |
| markitdown | 0.589 | 0.844 | 0.273 | 0.000 | 16.165s |
这组结果由项目方标注为 2026 年 7 月 31 日刷新,测试机器是 Apple M4 Pro。每个引擎在同一批 200 份文档上顺序运行单进程,速度取排除预热后的五次完整语料运行中位数。它适合用来了解项目的设计目标和测试方法,实际选型仍需要用自己的文档集复测,尤其是扫描件比例、表格复杂度、加密文件和字体编码分布不同的场景。
适合放在哪里
如果系统入口需要快速判断 PDF 是否带有可靠文本层,detect_pdf 可以独立部署在上传服务或队列前置消费者中。文本型报告进入 Rust 本地提取,扫描页进入 OCR,结果再按照页码合并。知识库建设可以保留 pages_needing_ocr、pages_with_tables、pages_with_columns 和 has_encoding_issues 等元数据,在检索和质量回溯时定位问题页。
如果团队主要使用 Python,PyO3 绑定可以把 Rust 核心嵌入现有文档流水线;Node.js 绑定适合放到 JavaScript 服务;WebAssembly 版本则可以在浏览器和 Web Worker 中本地运行,减少文件上传到服务器的环节。项目采用 MIT 许可,仓库还提供可复现 benchmark 的配套脚本,开发者可以用同一份语料和评估器比较自己的构建版本。
对 PDF 解析而言,分类、提取和 OCR 可以拆成三个独立组件。pdf-inspector 把文档识别、页面级路由、布局分析与 Markdown 输出拆成了可以独立调用的接口,适合成为文档处理系统中的本地解析层。
来源: