性能
PDF Oxide v0.3.11 在 3,830 个 PDF 文件的语料库上,Python 文本提取平均耗时 0.8ms——比 PyMuPDF 快 5 倍,比 pypdf 快 15 倍,对有效 PDF 实现 100% 通过率。
基准测试结果
语料库:3,830 个 PDF
由三个独立的公开测试集组成:
| 测试集 | PDF 数量 | 来源 |
|---|---|---|
| veraPDF | 2,907 | PDF/A 合规性测试语料库 |
| Mozilla pdf.js | 897 | 浏览器 PDF 渲染测试集 |
| SafeDocs | 26 | DARPA SafeDocs 畸形 PDF 语料库 |
为什么这个语料库是可靠的
这些不是精心挑选的 PDF。每个测试集都是由标准组织或大型开源项目维护的、经过同行评审的成熟测试语料库:
-
veraPDF 是 PDF 标准社区使用了超过 10 年的官方 PDF/A 合规性验证工具。其 2,907 个测试文件是原子级的——每个文件只测试一项 PDF 规范特性——涵盖所有 PDF/A 版本(1A/1B、2A/2B/2U、3A/3B/3U、4/4E/4F)、PDF/UA 无障碍标准(UA1、UA2),以及 ISO 32000-1(PDF 1.7)和 ISO 32000-2(PDF 2.0)两个标准。采用 CC BY 4.0 许可。
-
Mozilla pdf.js 驱动 Firefox 的 PDF 渲染引擎,每年处理数十亿个 PDF。其 897 个测试文件覆盖了真实场景的渲染边界情况:复杂的多页文档、各种注释类型(边框样式、高亮、插入符号、文件附件)、表单控件、特殊字体编码、大型压力测试文档以及内容流的边界情况。其中 7 个文件是故意损坏的,用于测试正确的错误拒绝行为。
-
DARPA SafeDocs 是美国政府资助的安全研究项目,专注于解析器的鲁棒性。其 26 个文件针对最困难的边界情况:Type3 字体中的内容流循环、双 startxref 尾部、紧凑 PDF 语法、方言变体、内联图片边界情况、递归字体嵌套,以及使用 Unicode 密码(包括 UTF-16 LE)的加密 PDF。这些文件专门设计来使脆弱的解析器崩溃、挂起或被利用。
语料库覆盖范围
- 所有主要 PDF 版本:PDF 1.0 到 PDF 2.0
- 加密与密码:AES-256、RC4、Unicode 密码、UTF-16 LE 编码
- 安全边界情况:递归结构、内容流循环、畸形尾部、模糊测试生成的损坏文件
- 字体多样性:TrueType、CIDFont、Type1、Type3、CJK 编码、嵌入子集
- 文档复杂度:从单页测试文件到 10,000+ 页的文档,内联图片,嵌套 Form XObject
- 正确拒绝:7 个故意损坏的文件(缺少 PDF 头部、无效 xref 流)——能"解析"这些文件的库安全性更差,而非更可靠
3,830 个文件中,3,823 个是有效 PDF。7 个无效文件是错误处理测试用例——PDF Oxide 正确拒绝了全部 7 个。
Python 库对比
在完整 3,830 个 PDF 语料库上,每个 PDF 的平均文本提取耗时:
| 库 | 平均耗时 | p99 | 通过率 | 许可证 |
|---|---|---|---|---|
| PDF Oxide | 0.8ms | 9ms | 100% | MIT |
| PyMuPDF | 4.6ms | 28ms | 99.3% | AGPL-3.0 |
| pypdfium2 | 4.1ms | 42ms | 99.2% | Apache-2.0 |
| pymupdf4llm | 55.5ms | 280ms | 99.1% | AGPL-3.0 |
| pdftext | 7.3ms | 82ms | 99.0% | GPL-3.0 |
| pdfminer | 16.8ms | 124ms | 98.8% | MIT |
| pdfplumber | 23.2ms | 189ms | 98.8% | MIT |
| markitdown | 108.8ms | 378ms | 98.6% | MIT |
| pypdf | 12.1ms | 97ms | 98.4% | BSD-3 |
PDF Oxide 是目前最快的 Python PDF 库。与 PyMuPDF 不同,它使用 MIT 许可证——商业使用无需担心 AGPL 限制。
Rust 库对比
| 库 | 平均耗时 | p99 | 通过率 | 文本提取 |
|---|---|---|---|---|
| PDF Oxide | 0.8ms | 9ms | 100% | 内置,生产级 |
| oxidize_pdf | 13.5ms | 11ms | 99.1% | 基础 |
| unpdf | 2.8ms | 10ms | 95.1% | 基础 |
| pdf_extract | 4.08ms | 37ms | 91.5% | 基础 |
| lopdf | 0.3ms | 2ms | 80.2% | 无内置提取 |
lopdf 在它能解析的 PDF 上速度更快,但在语料库中 20% 的文件上会失败。pdf_oxide 是唯一同时具备 100% 可靠性和内置文本提取功能的 Rust crate。注意 lopdf 不提供文本提取——你需要自己实现字体解码和间距分析。
文本质量
PDF Oxide 在完整语料库上与 PyMuPDF 和 pypdfium2 的文本输出达到 99.5% 的一致性。质量评估通过对所有 3,823 个有效 PDF 逐字符比较提取文本来完成。
各语料库详细数据
| 语料库 | PDF 数量 | PDF Oxide 平均 | pypdfium2 平均 | PyMuPDF 平均 |
|---|---|---|---|---|
| veraPDF | 2,907 | 0.7ms | 3.6ms | 4.1ms |
| Mozilla pdf.js | 897 | 1.1ms | 5.8ms | 6.2ms |
| SafeDocs | 26 | 0.9ms | 4.0ms | 4.3ms |
优化历程:v0.3.5 到 v0.3.8
v0.3.8 消除了两个关键的 O(n) 瓶颈,使 Python 平均耗时从 23.3ms 降至 0.8ms。
1. 页面树批量缓存
优化前:get_page() 在每次访问未缓存的页面时,都从根节点遍历整个页面树。对于顺序提取所有页面的场景,每页耗时 O(n),总计 O(n²)。
优化后:在首次访问页面时,一次性遍历整棵页面树,将所有页面缓存到 HashMap<usize, Object> 中。之后每次访问都是 O(1)。
这个优化将一个 10,000 页 PDF 的处理时间从 55 秒降到了 332 毫秒。
2. 扫描对象偏移量缓存
优化前:当 xref 表中缺少对象时,scan_for_object() 会对每个缺失对象读取整个 PDF 文件。带标签的 PDF 如果有数百个不在 xref 中的结构树元素,就会触发数百次全文件读取。
优化后:文件只扫描一次,所有对象偏移量缓存到 HashMap 中,后续查找都是 O(1)。
3. 单遍文本提取
优化前:extract_spans() 对页面内容执行两遍处理——第一遍分类文档类型(学术论文、报纸、表单等),第二遍提取文本。
优化后:完全移除了分类遍历。自适应的字体感知阈值在单遍处理中即可产生同等或更好的结果。
4. 内容流预分配
优化前:parse_content_stream() 从默认容量开始构建操作符 Vec,在大型内容流上会导致频繁的重新分配。
优化后:根据流大小预分配 Vec(data.len() / 20),大约每 20 字节估算一个操作符。
测试方法
所有基准测试使用相同的方法:
- 每个库使用 Python multiprocessing 处理全部 3,830 个 PDF(每个 PDF 一个进程)
- 每个 PDF 设置 60 秒超时——超时即计为失败
- 提取的文本按库分别保存到磁盘,用于质量对比
- 计时范围从文件打开到文本提取完成的壁钟时间
- 无预热运行,文件之间不共享缓存
- 每个 PDF 单线程处理
基准测试工具在同一台机器、同一语料库、同一条件下运行全部 18 个库(3 个 Rust,15 个 Python)。
复现基准测试
公开测试语料库可免费获取:
- veraPDF: github.com/veraPDF/veraPDF-corpus
- Mozilla pdf.js: github.com/mozilla/pdf.js/tree/master/test/pdfs
- SafeDocs: github.com/pdf-association/safedocs
运行验证:
cargo run --release --example verify_corpus -- \
/path/to/veraPDF-corpus \
/path/to/pdfjs-test \
/path/to/safedocs \
--csv results.csv
性能特征
PDF Oxide 的性能优势
- 文本提取:主要优化目标。对于常规文档,耗时低于毫秒级。
- 顺序多页提取:页面树缓存使得从大型文档中提取所有页面几乎与提取单页一样快。
- 带标签的 PDF:结构树遍历和对象解析现已缓存。
- 畸形 PDF:宽松解析配合回退策略,避免高代价的重试。
线性增长的场景
- 页面数量:每页独立处理。100 页大约是单页的 100 倍。
- 内容流大小:操作符解析与流长度成线性关系。
- 图片提取:与图片数量和大小成正比。
预期较慢的场景
- 扫描 PDF 配合 OCR:如果启用 OCR,处理速度会显著慢于文本提取。
- 渲染:页面渲染为图片取决于内容复杂度和目标 DPI。
- 高强度加密 PDF:AES-256 解密会增加每个流的开销。
- 包含数千种字体的 PDF:字体解析按文档级缓存,但首次解析的耗时与字体数量成正比。
下一步
- 更新日志 – 完整版本历史
- Python 库对比 – 与 PyMuPDF、pypdf、pdfplumber、pdfminer 的详细对比
- Rust 库对比 – 与 lopdf、pdf_extract、pdf-rs 的详细对比
- Rust 快速上手 – 安装和首次文本提取
- Rust API 参考 – 完整 API 文档