Skip to content

性能

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 字节估算一个操作符。

测试方法

所有基准测试使用相同的方法:

  1. 每个库使用 Python multiprocessing 处理全部 3,830 个 PDF(每个 PDF 一个进程)
  2. 每个 PDF 设置 60 秒超时——超时即计为失败
  3. 提取的文本按库分别保存到磁盘,用于质量对比
  4. 计时范围从文件打开到文本提取完成的壁钟时间
  5. 无预热运行,文件之间不共享缓存
  6. 每个 PDF 单线程处理

基准测试工具在同一台机器、同一语料库、同一条件下运行全部 18 个库(3 个 Rust,15 个 Python)。

复现基准测试

公开测试语料库可免费获取:

运行验证:

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:字体解析按文档级缓存,但首次解析的耗时与字体数量成正比。

下一步