PDF 转 Markdown:pdf-inspector 分类与 OCR 路由

RustOCRPDF

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

pdf-inspector GitHub 官方仓库封面

这个项目采用 Rust 编写,核心依赖是 lopdf,不需要机器学习模型和外部解析服务。仓库同时提供 Rust、Python、Node.js 和浏览器 WebAssembly 接口,并带有 pdf2mddetect-pdf 两个命令行工具。对于报告、论文、发票、法律文档等原生文本 PDF,它的定位是一个本地预处理层,负责把后续 OCR 服务不需要处理的文件筛出去。

先判断 PDF 属于哪一类

pdf-inspector 返回四种类型:TextBasedScannedImageBasedMixed。这四种结果对应不同的处理路径。

类型文档特征常见后续动作
TextBased页面存在可提取文本,文本页比例达到阈值直接进入本地文本提取
Scanned没有可提取文本,页面主要由扫描图像组成将页面交给 OCR
ImageBased没有合格文本,但存在图片或矢量化文字OCR 或专门的图像解析
Mixed文本页与图像页并存,或模板图像承担重要内容只对指定页面进行 OCR

识别依据来自 PDF 内容流里的操作符。源码会分析 TjTJ 等文本操作,也会检查 Do 图像操作、图片数量、文本字符数量、矢量文字以及字体编码信息。文本操作数量高,并不自动意味着页面适合直接提取:项目还会排除文本量过低、图片占主导、文字已经被矢量化,以及字体无法可靠解码的页面。

当前源码的默认检测配置是 Sample(8),最多均匀抽取 8 个页面参与初步判断;每页至少需要 3 个文本操作,文本页比例阈值为 0.6。当页面包含图片时,文本操作的有效下限会提高到至少 10 个,避免一张带少量隐藏文字的扫描图被当成原生文本页。

源码中的分类顺序也很有代表性。含有模板图像且同时存在文本的文档会被标记为 Mixed;文本页比例达到阈值时标记为 TextBased;没有合格文本但有图像或矢量文字时,结果会在 ScannedImageBased 之间区分;文本和图像同时存在时则归为 Mixed。结果里还包含置信度、页数和需要 OCR 的页面列表。

OCR 路由是页级的

扫描件识别最实用的结果是具体页码。调用方可以据此只把相应页面送入 OCR。ScannedImageBased 通常会把全部页面列入 OCR;Mixed 只列出图像页或文本质量不足的页面。

项目还会检查两类字体问题。一类是 Identity-HIdentity-V 字体缺少 ToUnicode 映射,另一类是只有 Type3 字体却没有可用映射。PDF 里可能存在文本操作符,但调用方拿到的字符仍然不可解码,这些页面会追加到 OCR 列表中。页级结果还可以记录 scannedno_textvector_textsuspected_garbled_text 等原因,方便上层系统记录路由依据。

这套设计适合下面的流水线:

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 扫描全部页面,适合需要更准确区分 MixedScanned 的场景;Sample(n) 采样均匀分布的页面,适合超长文档;Pages(vec) 只检查调用方指定的 1-based 页码。

Rust 中的最小调用如下:

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

rust
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_memdetect_pdf_mem,避免先把字节写入临时文件。

Markdown 转换处理了哪些结构

项目的提取器保留文本位置、字体、页码和链接信息,再交给布局与 Markdown 模块处理。Markdown 转换包括以下结构:

  1. 标题。通过字体大小层级识别 H1 至 H4,并对接近的字号做聚类。
  2. 列表。识别项目符号、数字列表和字母列表。
  3. 代码块。根据 Courier、Consolas、Monaco、Menlo、Fira Code、JetBrains Mono 等等宽字体以及关键词特征判断。
  4. 表格。既检查 PDF 绘图操作里的矩形边界,也根据文本对齐关系做启发式识别。
  5. 多栏布局。对报纸、论文和报告中的多列文本重新排列阅读顺序。
  6. 字体与编码。支持 CID、Type0、Identity-H、UTF-16BE、UTF-8 和 Latin-1 等编码路径,并标记无法可靠解码的页面。
  7. 链接与页面元素。保留 URL,过滤页码,处理脚注、标题首字下沉、上下标、断词和目录点线。

表格处理采用两条路径的原因很实际:有些 PDF 使用线条明确画出网格,有些 PDF 只通过列对齐表达表格结构。前者适合读取矩形绘图操作,后者需要比较文本项的 X 坐标、行间距和列关系。项目还提供 extract_text_with_positions,调用方可以拿到每个文本项的坐标、字号、字体、粗体和斜体等信息,做二次排版或自定义结构化抽取。

Python 接口的用法更短:

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:

bash
cargo install pdf-inspector

将 PDF 转为 Markdown:

bash
pdf2md document.pdf

机器处理时可以使用 JSON:

bash
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 允许只处理指定页面。检测命令支持:

bash
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标题 MHS200 份文档速度
pdf-inspector0.8750.9150.8140.7880.470s
liteparse0.8730.9130.6930.8110.750s
opendataloader0.8310.9020.4890.7392.569s
pymupdf4llm0.7350.8860.4010.42417.117s
markitdown0.5890.8440.2730.00016.165s

这组结果由项目方标注为 2026 年 7 月 31 日刷新,测试机器是 Apple M4 Pro。每个引擎在同一批 200 份文档上顺序运行单进程,速度取排除预热后的五次完整语料运行中位数。它适合用来了解项目的设计目标和测试方法,实际选型仍需要用自己的文档集复测,尤其是扫描件比例、表格复杂度、加密文件和字体编码分布不同的场景。

适合放在哪里

如果系统入口需要快速判断 PDF 是否带有可靠文本层,detect_pdf 可以独立部署在上传服务或队列前置消费者中。文本型报告进入 Rust 本地提取,扫描页进入 OCR,结果再按照页码合并。知识库建设可以保留 pages_needing_ocrpages_with_tablespages_with_columnshas_encoding_issues 等元数据,在检索和质量回溯时定位问题页。

如果团队主要使用 Python,PyO3 绑定可以把 Rust 核心嵌入现有文档流水线;Node.js 绑定适合放到 JavaScript 服务;WebAssembly 版本则可以在浏览器和 Web Worker 中本地运行,减少文件上传到服务器的环节。项目采用 MIT 许可,仓库还提供可复现 benchmark 的配套脚本,开发者可以用同一份语料和评估器比较自己的构建版本。

对 PDF 解析而言,分类、提取和 OCR 可以拆成三个独立组件。pdf-inspector 把文档识别、页面级路由、布局分析与 Markdown 输出拆成了可以独立调用的接口,适合成为文档处理系统中的本地解析层。

来源: