人生最宝贵的是生命,人生最需要的是学习,人生最愉快的是工作,人生最重要的是友谊。——斯大林

当 PDF 不再一股脑丢给 OCR:认识一下 pdf-inspector

很多 PDF 处理流程里,最贵的那一步,往往不是“读文件”,而是“看不清就全送去 OCR”。

这件事听上去很自然,做起来也很省心:PDF 来了,扔进 OCR,等结果出来。但问题也很明显——有些 PDF 根本不是扫描件,它本来就带着清清楚楚的文本层;如果还是按同样的重型流程处理,时间和成本都会被悄悄放大。

pdf-inspector 想解决的,正是这个环节里的“误伤”。

项目地址:/firecrawl/pdf-inspector

它是一个用 Rust 编写的 PDF 检查与提取库,核心目标很直接:先判断 PDF 到底是什么类型,再决定后续该怎么走。README 对它的描述非常清楚——它可以做 PDF 分类、文本提取,并且能智能识别一个 PDF 是扫描件还是文本型文档,从而帮助上层流程做更聪明的路由决策。

它不是在“读取 PDF”,它更像是在“打量 PDF”

如果把普通 PDF 处理工具比作一位埋头苦干的搬运工,那 pdf-inspector 更像是站在门口先看一眼局势的人。

它先问的不是“怎么提取”,而是:

  • 这是文本型 PDF 吗
  • 这是扫描件吗
  • 这是纯图像型文档吗
  • 还是一份混合型 PDF

README 里给出的分类结果包括:

  • TextBased
  • Scanned
  • ImageBased
  • Mixed

而且这个判断不是一句模糊的“像不像”,它还会返回 confidence score,也就是 0.0 到 1.0 的置信度分数;同时还能给出 按页的 OCR 路由信息,指出哪些页面缺少文本、需要 OCR 介入。

这就很有意思了。因为现实里的 PDF 从来不是整齐划一的:有的文档前几页是正文,后几页是扫描附录;有的报告主体是文字,但夹着几页图片;有的材料明明能直接提文本,却总被整个送进 OCR。pdf-inspector 在这里做的事情,不是粗暴地把整份文档贴上一个标签,而是尽可能把判断拆细,把决策交给真正需要决策的地方。

一个很务实的出发点:不该做 OCR 的,就别做 OCR

README 里对这个项目的定位非常鲜明:它由 Firecrawl 构建,用来在本地处理文本型 PDF,并跳过昂贵的 OCR 服务。

甚至连使用场景都写得很直白——针对大规模 PDF 处理流程,不是把每份 PDF 都送去 OCR,而是先分类:

1
2
3
4
5
PDF arrives
→ pdf-inspector classifies it (~20ms)
→ TextBased + high confidence?
YES → extract locally (~150ms), done
NO → send to OCR service (2-10s)

这个流程图的价值在于,它没有把工具包装成“万能 PDF 魔法盒”,而是非常诚实地告诉你:它特别适合做前置判断与本地提取。当文档本身已经具备良好的文本结构时,就本地快速完成;当文档缺少可用文本层,再转去 OCR。

README 还提到,之所以这么做,是为了节省大多数本来就是文本型 PDF 所带来的成本与延迟。这一点很朴素,但也很关键。很多工程优化最后都不是更复杂,而是更会分流。

这份 README 的关键词只有两个:快,且知道自己在做什么

pdf-inspector 的“快”不是一句空泛宣传,README 里把它拆成了多个具体层面。

1. 分类很快

项目说明中写到,它通过采样内容流来检测 PDF 类型,分类大约在 10 到 50 毫秒之间完成。对于 300 页以上的 PDF,也能在毫秒级完成检测。

README 还解释了分类思路:

  1. 解析 xref table 和 page tree,而不是完整加载所有对象
  2. 根据 ScanStrategy 选择页面,默认扫描全部页面并支持提前退出
  3. 在内容流里查找 Tj / TJ 这样的文本操作符,以及 Do 这样的图像操作符
  4. 根据采样页中的文本操作符情况进行分类

这套思路很像一个经验老到的检查员:不急着把整栋楼拆开重装,而是先看结构、查关键线索,再决定判断。

2. 提取也快

README 里提到,这个库可以在本地 200ms 以内处理文本型 PDF。与此同时,它还有一个重要的架构设计:文档只加载一次

也就是说,检测和提取共享同一个已解析文档,不会为了“先判断、再提取”而重复做 I/O 和重复解析。这个细节非常工程化,也很能说明作者在意的不是孤立功能点,而是整条处理链的效率。

它提取的不只是“字”,而是尽量保留“文档的样子”

如果一个 PDF 工具只能把文字抠出来,那它离“可用”还差一步;真正难的是,把文本顺序、结构层级、列表、表格这些信息尽量保留下来。

pdf-inspector 在 README 里把这一块写得相当完整,它支持的并不是单纯文本输出,而是 带位置感知的提取,并进一步转换为 干净的 Markdown

文本提取:位置意识很强

项目支持带有以下信息的文本提取:

  • 字体信息
  • X/Y 坐标
  • 自动多栏阅读顺序

这意味着它不是在“把字符拼一遍”,而是在尽可能理解页面布局。尤其面对报纸式、多栏式排版时,这一点很重要。很多文档如果只按原始顺序硬拼,最后读起来会像把两条河流拧在了一起;而 pdf-inspector 明确支持 自动检测多栏布局,并按顺序组织阅读结果。

Markdown 转换:不是简化,而是整理

README 里列出了 Markdown 输出阶段会处理的内容,包括:

  • 标题 H1 到 H4,通过字体大小层级识别
  • 粗体和斜体,通过字体名称模式识别
  • 项目符号列表
  • 编号列表
  • 字母列表
  • 代码块,通过等宽字体和关键字检测
  • 表格
  • 财务表格中的数字拆分
  • 图表说明文字
  • 上标和下标
  • URL 转换为 Markdown 链接
  • 断行连字符恢复
  • 页码过滤
  • 首字下沉合并
  • 目录式点线压缩

看起来像是一长串细节,但这些细节组合起来,其实是在回答同一个问题:导出的 Markdown 到底能不能直接读、直接用。

一份好的结构化提取,不是把页面“打散”,而是把它“翻译”成另一种仍然有秩序的表达方式。pdf-inspector 做的就是这件事。

表格这件麻烦事,它没有回避

PDF 里的表格,向来是文档处理里的硬骨头。pdf-inspector 在 README 中把表格检测单独拿出来写,也说明这部分是它的重要能力之一。

它采用的是 双模式表格检测

  • 基于 PDF 绘图操作中的矩形进行检测
  • 基于文本对齐关系进行启发式检测

随后再做:

  • 列与行分配
  • 单元格组织
  • 最终格式化为 Markdown 表格

README 还特别提到,它可以处理:

  • financial tables
  • footnotes
  • continuation tables across pages

也就是说,它考虑的不是“有没有一张最标准的表”,而是现实里那些经常不那么整齐、甚至会跨页延续的表格。

这是一种很实在的设计态度:PDF 不是给解析器准备的,它原本是给人看的,所以所有结构化提取都天然带着一点“逆向理解”的意味。表格做得越认真,越说明项目没有停留在“能跑通”这个层面。

对字体编码问题,它保持警觉

不少 PDF 处理问题,看起来像“提取失败”,实际上根源常常藏在字体编码里。

README 里提到,pdf-inspector 支持:

  • ToUnicode CMap 解码
  • Type0 / Identity-H 字体
  • UTF-16BE
  • UTF-8
  • Latin-1 编码

同时,它还会自动检测 broken font encodings,并在发现问题时提示调用方回退到 OCR。

这一点很重要,因为“错误地提取出一堆乱码”往往比“明确告诉你这里不可靠”更糟糕。一个靠谱的系统,不只是会成功,也知道什么时候该承认自己不该继续硬提。

同一套核心,不只活在 Rust 里

虽然它的核心是 Rust,但这个项目并没有把自己困在单一语言生态里。README 展示了它在多个环境里的使用方式:

  • Python
  • Node.js
  • Browser WebAssembly
  • Rust
  • CLI

这不是“顺手做了几个绑定”,而是让同一个 PDF 能力,在不同上下文里都能被调用。

Python:直接处理文件

1
2
3
4
5
import pdf_inspector

result = pdf_inspector.process_pdf("document.pdf")
print(result.pdf_type) # "text_based", "scanned", "image_based", "mixed"
print(result.markdown) # Markdown string or None

Node.js:直接读取 Buffer 处理

1
2
3
4
5
6
import { readFileSync } from 'fs';
import { processPdf, classifyPdf } from '@firecrawl/pdf-inspector';

const result = processPdf(readFileSync('document.pdf'));
console.log(result.pdfType); // "TextBased", "Scanned", "ImageBased", "Mixed"
console.log(result.markdown); // Markdown string or null

浏览器 WebAssembly:本地跑,不走服务端

1
2
3
4
5
6
7
8
9
import init, { processPdf } from '@firecrawl/pdf-inspector-wasm';

await init();
const response = await fetch('/document.pdf');
const pdf = new Uint8Array(await response.arrayBuffer());
const result = processPdf(pdf);

console.log(result.pdfType);
console.log(result.markdown);

Rust:最原生的一种方式

1
2
3
4
5
6
7
use pdf_inspector::process_pdf;

let result = process_pdf("document.pdf")?;
println!("Type: {:?}", result.pdf_type);
if let Some(markdown) = &result.markdown {
println!("{}", markdown);
}

CLI:给命令行工作流留足空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Install the CLI tools
cargo install pdf-inspector

# Convert PDF to Markdown
pdf2md document.pdf

# JSON output (for piping)
pdf2md document.pdf --json

# Positioned TextItem JSON, including is_underline metadata
pdf2md document.pdf --items-json

# Raw markdown only (no headers)
pdf2md document.pdf --raw

# Token-efficient output (collapses long dot leaders and similar source padding)
pdf2md document.pdf --compact

# Insert page break markers (<!-- Page N -->)
pdf2md document.pdf --pages

# Process only specific pages
pdf2md document.pdf --select-pages 1,3,5-10

# Detection only (no extraction)
detect-pdf document.pdf
detect-pdf document.pdf --json

# Detection + layout analysis (tables, columns)
detect-pdf document.pdf --analyze --json

从这些入口能看出来,pdf-inspector 的姿态很明确:它不只想成为一个库,也想成为一块可嵌入、可集成、可组合的基础能力。

浏览器侧的 WASM 版本,有一种“把事情留在本地”的克制

WASM 版本在 README 里有几个很值得注意的点:

  • 解析在本地运行
  • PDF bytes 不会上传
  • 构建是单线程的
  • 不要求 cross-origin isolation
  • CMaps 已内嵌
  • init() 之后提取是同步的
  • 大文档建议放到 Web Worker 里执行
  • 纯图片文档依然需要单独 OCR

这几条信息拼起来,勾勒出了一个很清晰的浏览器侧能力边界:它能在浏览器里本地做分类和结构化提取,但不会假装自己能替代图像 OCR。

这种边界感很可贵。很多工具最容易让人失望的地方,是宣传成“全能型”,实际遇到复杂文档就沉默;而这里的说明方式更像一个稳重的工程组件——能做什么,说清楚;不能替代什么,也说清楚。

Node.js 绑定里,还有一项很实用的能力:区域提取

napi/README.md 里提到一个主 README 没有重点展开、但非常实用的能力:按区域提取文本

也就是:

1
extractTextInRegions(buffer: Buffer, pageRegions: PageRegions[]): PageRegionTexts[]

它的设计目标也写得很明白:适用于 混合 OCR 流程。先由页面图像上的布局模型识别区域,再由这个函数从 PDF 结构层里提取对应区域的文本。

示例里每个区域结果都带有 needsOcr 标记;如果文本不可靠,比如:

  • 提取结果为空
  • 字体是 GID 编码
  • 出现垃圾文本
  • 编码异常

那就把这个区域继续送 OCR。

这种能力很像是把“整份 PDF 是否 OCR”进一步细化成“某一页、某一块区域是否 OCR”。对于复杂文档来说,这种粒度显然更灵活。

它的内部结构,读起来很像一条分工明确的流水线

README 给了一张很完整的架构图,从原始 PDF bytes 开始,分成 detector 和 extractor 两条主线。

提取链路里又包括:

  • fonts
  • content_stream
  • xobjects
  • links
  • layout

然后布局再进入:

  • tables
  • markdown

最后经过分析、预处理、转换、分类和后处理,产出最终 Markdown。

这种结构感带来的最大好处,是让人一眼看出项目不是“功能堆在一起”,而是按职责拆开的。README 还列出了源代码目录结构,例如:

  • lib.rs 负责公共 API、PdfOptions builder 和便捷函数
  • python.rs 负责 PyO3 Python 绑定
  • types.rs 定义共享类型
  • text_utils.rs 处理字符与文本辅助逻辑
  • process_mode.rs 定义处理模式
  • detector.rs 负责快速 PDF 类型检测
  • glyph_names.rs 提供 Adobe Glyph List 到 Unicode 的映射
  • tounicode.rs 处理 ToUnicode CMap 解析
  • extractor/ 是文本提取流水线
  • tables/ 负责表格检测与格式化
  • markdown/ 负责 Markdown 转换与结构识别
  • bin/ 放置 CLI 工具
  • napi/ 是 Node.js/Bun 绑定
  • wasm/ 是浏览器绑定

如果你喜欢看项目“骨架”,这一部分会很有阅读价值。它没有试图用很多抽象术语把自己说得高深,反而是把模块划分摆在你面前,让你知道这套能力是怎么层层拼起来的。

扫描策略也不是只有一种

在分类环节里,README 还给出了 ScanStrategy 的几种策略:

  • EarlyExit:扫描所有页面,遇到第一个非文本页就停止
  • Full:扫描所有页面,不提前退出
  • Sample(n):均匀采样若干页
  • Pages(vec):只扫描指定页面

这意味着项目并不是只有一种固定的“检测姿势”,而是把速度和精度之间的权衡显式交给调用方。

如果你的场景更看重快速路由,可以用更偏速度的策略;如果你需要更准确地区分 MixedScanned,也有对应选择。这样的设计很适合真实系统接入,因为不同业务场景对“快”和“准”的容忍度并不相同。

从 Benchmark 看,它想证明的不只是能用,而是有竞争力

README 提供了一组基于 opendataloader-bench 语料库的 benchmark 结果,测试集包含 200 份 PDF,只展示本地引擎且关闭 OCR。

结果表里,pdf-inspector 的成绩是:

  • Overall:0.875
  • Reading Order (NID):0.915
  • Tables (TEDS):0.814
  • Headings (MHS):0.788
  • Speed (200 docs):0.470s

README 还写到,这组结果在 2026 年 7 月 31 日Apple M4 Pro 上刷新;速度统计是去掉预热后的五次完整跑分中位数。

项目还明确写出一条“Best fit”总结:它适合 native-text PDFs,尤其是在速度、阅读顺序和表格结构都重要的场景里。在这次对比中,它拿到了更高的整体分、阅读顺序分和表格分,同时也是最快的。

这段 benchmark 信息的价值,不只是“我更强”,而是它把“适合什么场景”说清楚了:文本型 PDF、重视结构质量、同时在意速度。这和它前面的整体定位是连贯的。

安装方式很直接,门槛并不高

如果你想从 README 给出的路径快速上手,不同环境的入口都非常明确。

Python

1
2
pip install maturin
maturin develop --release

Node.js

1
npm install @firecrawl/pdf-inspector

Browser WebAssembly

1
npm install @firecrawl/pdf-inspector-wasm

Rust

1
cargo add pdf-inspector

或者手动添加依赖:

1
2
[dependencies]
pdf-inspector = "0.1"

从源码构建 WASM

1
2
cargo install wasm-pack --version 0.15.0 --locked
wasm-pack build wasm --target web --scope firecrawl --release

Node.js 绑定的 README 里还特别提到,它提供预构建二进制,覆盖 Linux x64/ARM64、macOS ARM64 和 Windows x64,npm 安装时只会安装与你平台匹配的那个包,而且 不需要 Rust toolchain。这对使用者来说非常友好,尤其是在只想集成能力、不想处理本地编译链路的时候。

一个很有“工程气”的 PDF 工具

读完 README 和 description,会很明显地感受到 pdf-inspector 的气质:它不是那种试图在文案里包办一切的项目,而是一块边界清楚、能力扎实、特别适合接进处理流水线的基础组件。

它关心的不是“把 PDF 这件事说得多宏大”,而是几个很具体的问题:

  • 这份 PDF 是文本型还是扫描型
  • 哪些页需要 OCR
  • 能不能本地快速提取文本
  • 能不能尽量保住阅读顺序和结构
  • 能不能把结果整理成更干净的 Markdown
  • 遇到编码问题时,能不能及时识别并让上层回退

这种专注会让一个工具显得很可靠。因为它不是对所有问题都说“我能”,而是把自己最擅长的那一段路打磨得很深。

如果说 OCR 像重型机械,适合啃最硬的文档;那么 pdf-inspector 更像是一位懂分流、会判断、下手快的前场指挥。它先看看眼前这份 PDF 究竟是什么,再决定是轻快地本地处理,还是把真正难啃的部分交给后面的流程。

在 PDF 处理这条链路里,这样的角色,往往比想象中更重要。