当 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,而是先分类: ``` 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. 1. 解析 xref table 和 page tree,而不是完整加载所有对象 1. 2. 根据 `ScanStrategy` 选择页面,默认扫描全部页面并支持提前退出 1. 3. 在内容流里查找 `Tj` / `TJ` 这样的文本操作符,以及 `Do` 这样的图像操作符 1. 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:直接处理文件** ``` 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 处理** ``` 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:本地跑,不走服务端** ``` 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:最原生的一种方式** ``` 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:给命令行工作流留足空间** ``` # 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 () 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 没有重点展开、但非常实用的能力:**按区域提取文本**。 也就是: ``` 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)`:只扫描指定页面 这意味着项目并不是只有一种固定的 “检测姿势”,而是把速度和精度之间的权衡显式交给调用方。 如果你的场景更看重快速路由,可以用更偏速度的策略;如果你需要更准确地区分 `Mixed` 和 `Scanned`,也有对应选择。这样的设计很适合真实系统接入,因为不同业务场景对 “快” 和 “准” 的容忍度并不相同。 ## **从 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** ``` pip install maturin maturin develop --release ``` ### **Node.js** ``` npm install @firecrawl/pdf-inspector ``` ### **Browser WebAssembly** ``` npm install @firecrawl/pdf-inspector-wasm ``` ### **Rust** ``` cargo add pdf-inspector ``` 或者手动添加依赖: ``` [dependencies] pdf-inspector = "0.1" ``` ### **从源码构建 WASM** ``` 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 处理这条链路里,这样的角色,往往比想象中更重要。