Skip to content

Python PDF 文本提取

从 PDF 提取文本是文档处理流水线里最常见的任务之一:搭建搜索索引、喂给 RAG 系统、数据挖掘、合规审查,起点几乎都在这里。本指南把用 PDF Oxide 在 Python、JavaScript 和 Rust 中提取 PDF 文本所需要知道的内容都讲清楚,包括纯文本提取、字符级坐标、带样式的 span、扫描件 OCR、加密文件处理,以及批量流水线的性能调优。

用三行代码就能从任意 PDF 取出文本:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("document.pdf")
text = doc.extract_text(0)  # 第 0 页
print(text)

WASM

import { WasmPdfDocument } from "pdf-oxide-wasm";

const bytes = new Uint8Array(buffer);
const doc = new WasmPdfDocument(bytes);
const text = doc.extractText(0); // 第 0 页
console.log(text);
doc.free();

Rust

use pdf_oxide::PdfDocument;

let mut doc = PdfDocument::open("document.pdf")?;
let text = doc.extract_text(0)?;
println!("{}", text);

Go

package main

import (
    "fmt"
    "log"
    pdfoxide "github.com/yfedoseev/pdf_oxide/go"
)

func main() {
    doc, err := pdfoxide.Open("document.pdf")
    if err != nil { log.Fatal(err) }
    defer doc.Close()

    text, err := doc.ExtractText(0) // 第 0 页
    if err != nil { log.Fatal(err) }
    fmt.Println(text)
}

C#

using PdfOxide;

using var doc = PdfDocument.Open("document.pdf");
var text = doc.ExtractText(0); // 第 0 页
Console.WriteLine(text);

PDF Oxide 每页平均 0.8 毫秒——比 PyMuPDF 快 5 倍,比 pypdf 快 15 倍,并且在 3,830 份测试 PDF 上保持 100% 通过率。

为什么 PDF 文本提取很难

PDF 是视觉格式,而不是文本格式。和 HTML 或 Markdown 不一样,PDF 文件里不存「段落」或「句子」,它存的是页面上摆在特定坐标的单个字符。要把这些字符还原成可读的文本,得经过以下几步:

  • 字体解码:PDF 字体通过编码表(WinAnsi、MacRoman、Unicode CMap、Type 1、TrueType、CIDFont)把字符码映射到字形。同一个代码 0x41,在一种字体里是「A」,在另一种字体里可能是「α」。
  • 文本流解析TjTJ'" 这些操作符把字符摆到页面上。TJ 数组里的字偶距调整以点的零头为单位挪动字符,空格则要根据字符位置之间的间距推断出来。
  • 版面重建:页面上的字符没有显式的阅读顺序。两栏版面、页眉、页脚、表格和侧栏都要经过空间分析,才能拼成一条线性的文本流。
  • 编码边界:CJK(中文、日文、韩文)文本使用拥有数千字形的 CIDFont/CMap 编码;阿拉伯文和希伯来文需要从右到左重排;连字(fi、fl、ffi)必须拆分。
  • 嵌入子集:很多 PDF 只嵌入实际用到的字形,并配有自定义编码向量。某个字体可能把字形索引 1→“T”、2→“h”、3→“e” 这样映射,不对应任何标准编码。

这就是为什么不同 PDF 库处理同一个文件会给出不同的文本结果——有些库在复杂文档上甚至直接失败。PDF Oxide 用基于 Rust 的解析器把上述情况一并覆盖,并在 3,830 份真实 PDF 上验证出 100% 通过率。

安装

Python(PyPI):

pip install pdf_oxide

提供针对 Linux(x86_64、aarch64)、macOS(Intel 与 Apple Silicon)以及 Windows(x86_64)的预编译 wheel。要求 Python 3.8 及以上版本,没有任何系统依赖:Rust 内核编译进 wheel,不需要额外安装 Poppler、MuPDF 或任何 C 库。

JavaScript(npm):

npm install pdf-oxide-wasm

在 Node.js 18 及以上版本和主流浏览器中均可运行,WASM 二进制已经打包在包里。

Rust(Cargo):

cargo add pdf_oxide

需要 Rust 1.70 及以上版本。除了标准 Rust 工具链,没有其他系统依赖。

提取所有页面

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("report.pdf")
full_text = []
for i in range(doc.page_count()):
    text = doc.extract_text(i)
    full_text.append(text)

print("\n".join(full_text))

WASM

const doc = new WasmPdfDocument(bytes);
const fullText = doc.extractAllText();
console.log(fullText);
doc.free();

Rust

let mut doc = PdfDocument::open("report.pdf")?;
let mut full_text = Vec::new();
for i in 0..doc.page_count()? {
    full_text.push(doc.extract_text(i)?);
}
println!("{}", full_text.join("\n"));

Go

doc, err := pdfoxide.Open("report.pdf")
if err != nil { log.Fatal(err) }
defer doc.Close()

full, err := doc.ExtractAllText()
if err != nil { log.Fatal(err) }
fmt.Println(full)

C#

using var doc = PdfDocument.Open("report.pdf");
var parts = new List<string>();
for (int i = 0; i < doc.PageCount; i++)
    parts.Add(doc.ExtractText(i));
Console.WriteLine(string.Join("\n", parts));

连同字符位置一起提取

获取每个字符的精确坐标、字体名和字号:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("paper.pdf")
chars = doc.extract_chars(0)

for ch in chars[:20]:
    print(f"'{ch.char}' at ({ch.x:.1f}, {ch.y:.1f}) "
          f"font={ch.font_name} size={ch.font_size:.1f}")

WASM

const doc = new WasmPdfDocument(bytes);
const chars = doc.extractChars(0);
for (const ch of chars.slice(0, 20)) {
    console.log(`'${ch.char}' at (${ch.x.toFixed(1)}, ${ch.y.toFixed(1)}) font=${ch.fontName} size=${ch.fontSize.toFixed(1)}`);
}
doc.free();

Rust

let mut doc = PdfDocument::open("paper.pdf")?;
let chars = doc.extract_chars(0)?;
for ch in chars.iter().take(20) {
    println!("'{}' at ({:.1}, {:.1}) font={} size={:.1}",
        ch.char, ch.x, ch.y, ch.font_name, ch.font_size);
}

Go

doc, _ := pdfoxide.Open("paper.pdf")
defer doc.Close()

chars, _ := doc.ExtractChars(0)
for _, ch := range chars[:20] {
    fmt.Printf("%q at (%.1f, %.1f) font=%s size=%.1f\n",
        ch.Char, ch.X, ch.Y, ch.FontName, ch.FontSize)
}

C#

using var doc = PdfDocument.Open("paper.pdf");
var chars = doc.ExtractChars(0);
foreach (var ch in chars.Take(20))
    Console.WriteLine($"'{ch.Char}' at ({ch.X:F1}, {ch.Y:F1}) font={ch.FontName} size={ch.FontSize:F1}");

每个字符对象包含以下字段:

字段 类型 说明
char str Unicode 字符
x, y float 以点为单位的位置
font_size float 以点为单位的字号
font_name str PostScript 字体名
bbox tuple 包围盒 (x0, y0, x1, y1)

字符级提取非常适合重建表格、按字号识别标题,或者给文本区域绘制包围盒。例如,按照 y 坐标把字符聚成一行,再通过 x 位置之间的空隙判断分栏边界。

提取带样式的文本 span

把同字体、同字号的连续字符聚成一段:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("paper.pdf")
spans = doc.extract_spans(0)

for span in spans:
    print(f"'{span.text}' font={span.font_name} size={span.font_size:.1f}")

WASM

const doc = new WasmPdfDocument(bytes);
const spans = doc.extractSpans(0);
for (const span of spans) {
    console.log(`'${span.text}' font=${span.fontName} size=${span.fontSize.toFixed(1)}`);
}
doc.free();

Rust

let mut doc = PdfDocument::open("paper.pdf")?;
let spans = doc.extract_spans(0)?;
for span in &spans {
    println!("'{}' font={} size={:.1}", span.text, span.font_name, span.font_size);
}

这对识别标题、加粗段落,或构建结构化输出很方便。

批量处理

一口气处理成百上千的 PDF:

from pdf_oxide import PdfDocument, PdfError
from pathlib import Path

pdf_dir = Path("documents/")
for pdf_path in pdf_dir.glob("*.pdf"):
    try:
        doc = PdfDocument(str(pdf_path))
        for i in range(doc.page_count()):
            text = doc.extract_text(i)
            # 处理文本...
    except PdfError as e:
        print(f"已跳过 {pdf_path.name}: {e}")

每页 0.8 毫秒的速度下,处理 3,830 份 PDF 大约 3.1 秒就能完成。生产流水线的做法可以参考批处理指南,里面有 multiprocessing 和异步 IO 的并行模式。

扫描件处理(OCR)

如果 PDF 里是扫描图片而不是文本,extract_text() 会返回空串或接近空串。这时使用 PDF Oxide 自带的 OCR:

from pdf_oxide import PdfDocument

doc = PdfDocument("scanned.pdf")
text = doc.extract_text(0)

if not text.strip():
    # 很可能是扫描页,改用 OCR
    text = doc.extract_text_ocr(0)
    print(text)

PDF Oxide 通过 ONNX Runtime 调用 PaddleOCR,不需要再装 Tesseract。模型选择和参数调优见 OCR 指南

加密 PDF 处理

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("protected.pdf", password="secret")
text = doc.extract_text(0)
print(text)

WASM

const doc = new WasmPdfDocument(bytes);
doc.authenticate("secret");
const text = doc.extractText(0);
console.log(text);
doc.free();

Rust

let mut doc = PdfDocument::open_with_password("protected.pdf", "secret")?;
let text = doc.extract_text(0)?;
println!("{}", text);

Go

doc, _ := pdfoxide.Open("protected.pdf")
defer doc.Close()

if _, err := doc.Authenticate("secret"); err != nil { log.Fatal(err) }
text, _ := doc.ExtractText(0)
fmt.Println(text)

C#

using var doc = PdfDocument.OpenWithPassword("protected.pdf", "secret");
Console.WriteLine(doc.ExtractText(0));

支持 AES-256、AES-128 和 RC4 加密的 PDF。相比之下,pdfplumber 根本打不开加密文件,pdfminer 在 AES-256 上会失败,而 PDF Oxide 对 PDF 标准的各种加密方式都能透明处理。

输出为 Markdown

需要带标题和格式的结构化输出时:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("paper.pdf")
md = doc.to_markdown(0, detect_headings=True)
print(md)

WASM

const doc = new WasmPdfDocument(bytes);
const md = doc.toMarkdown(0);
console.log(md);
doc.free();

Rust

let mut doc = PdfDocument::open("paper.pdf")?;
let md = doc.to_markdown(0, true)?;
println!("{}", md);

Go

doc, _ := pdfoxide.Open("paper.pdf")
defer doc.Close()

md, _ := doc.ToMarkdown(0)
fmt.Println(md)

C#

using var doc = PdfDocument.Open("paper.pdf");
Console.WriteLine(doc.ToMarkdown(0));

与 RAG、LLM 的对接方法请看 PDF 转 Markdown 指南

在 PDF 中搜索

带位置信息在所有页面查找文本:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("manual.pdf")
results = doc.search("configuration")
for r in results:
    print(f"Page {r.page}: '{r.text}' at ({r.x:.0f}, {r.y:.0f})")

WASM

const doc = new WasmPdfDocument(bytes);
const results = doc.search("configuration", false);
for (const r of results) {
    console.log(`Page ${r.page}: '${r.text}' at (${r.x.toFixed(0)}, ${r.y.toFixed(0)})`);
}
doc.free();

Rust

let mut pdf = Pdf::open("manual.pdf")?;
let results = pdf.search("configuration")?;
for r in &results {
    println!("Page {}: '{}' at ({:.0}, {:.0})", r.page, r.text, r.bbox.x, r.bbox.y);
}

Go

doc, _ := pdfoxide.Open("manual.pdf")
defer doc.Close()

results, _ := doc.SearchAll("configuration", false)
for _, r := range results {
    fmt.Printf("Page %d: %q at (%.0f, %.0f)\n", r.PageIndex, r.Text, r.X, r.Y)
}

C#

using var doc = PdfDocument.Open("manual.pdf");
foreach (var r in doc.SearchAll("configuration", caseSensitive: false))
    Console.WriteLine($"Page {r.PageIndex}: '{r.Text}' at ({r.X:F0}, {r.Y:F0})");

与其它 Python PDF 库的对比

Python 里可以用来提取 PDF 文本的库有好几个,对比如下:

  • pypdf — 纯 Python,无需 C 依赖。安装简单,但速度慢(每页 12 ms),字体和编码支持有限,约 1.6% 的 PDF 会失败;也不提供字符位置。适合速度不敏感的简单 PDF。
  • pdfplumber — 基于 pdfminer,能提供细致的字符和表格信息。速度非常慢(每页 23 ms),而且打不开加密 PDF。更适合关注单元格数据而对性能要求不高的表格提取场景。
  • PyMuPDF(fitz) — MuPDF C 库的 Python 绑定。速度快(每页 4.6 ms),通过率也高(99.3%),但需要安装 C 库,并采用 AGPL 许可证。如果许可证条款对你的项目没问题,它是个稳健的选择。
  • pypdfium2 — Google PDFium 引擎的 Python 绑定。速度快(每页 4.1 ms),不过在复杂文档上 p99 会上升到 42 ms,API 面比 PyMuPDF 小。
  • pdfminer.six — 纯 Python,提供详细的版面分析,但非常慢且已经没什么维护,在 AES-256 加密 PDF 上会失败,大部分场景已经被 pdfplumber 取代。
  • PDF Oxide — Rust 内核 + 通过 PyO3 暴露的 Python 绑定。最快(每页 0.8 ms),100% 通过率,支持所有加密方式并内置 OCR。采用 MIT 许可证,没有系统依赖。

PDF Oxide 正是为了补齐现有库的短板而生:纯 Python 解析器的速度瓶颈、MuPDF 的许可证限制,以及其它库在面对字体怪异、交叉引用表损坏或非标准编码的真实 PDF 时常见的稳定性问题。

性能:PDF Oxide 到底有多快

基于三份独立公开测试集共 3,830 份 PDF 的测量结果:

平均 p99 通过率
PDF Oxide 0.8 ms 9 ms 100%
PyMuPDF 4.6 ms 28 ms 99.3%
pypdfium2 4.1 ms 42 ms 99.2%
pypdf 12.1 ms 97 ms 98.4%
pdfplumber 23.2 ms 189 ms 98.8%

按 10,000 份 PDF 的流水线算:

  • PDF Oxide:8 秒
  • PyMuPDF:46 秒
  • pypdf:2 分钟
  • pdfplumber:3.9 分钟

完整的测量方法和复现步骤在基准测试详情

常见问题与排查

提取到的文本是空串

如果 extract_text() 返回空串,页面大概率是扫描图而不是文本。此时改用 extract_text_ocr()。配置步骤见扫描件 OCR

字符乱码或错乱

通常是字体使用了非标准的编码向量,或者缺少 ToUnicode CMap。PDF Oxide 能处理绝大多数这类情况,但对于故意做了混淆的 PDF(比如受 DRM 保护的内容)仍有可能出现不正确的输出。

空格丢失或词语粘连

PDF 的文本操作符是按字符单独摆放的,空格要根据字符间距和字体空格宽度去推断。如果单词看起来粘在一起,可以用 extract_chars() 拿到坐标,再自己写一套间距逻辑。

与其它库结果不同

各库在空格推断、换行和阅读顺序上都有自己的启发式。PDF Oxide 在 3,830 份 PDF 上与 PyMuPDF 的文本一致率达到 99.5%,剩下 0.5% 的差异主要在空白归一化和连字处理上。

真实使用场景

搜索索引:从文档仓库里每一份 PDF 的每一页提取文本,喂给 Elasticsearch、Typesense 或向量数据库做全文检索。PDF Oxide 的速度让按需重建成千上万文档的索引变得现实可行。

RAG 流水线(检索增强生成):提取并切分 PDF 文本,再用 OpenAI、Cohere 或开源模型做 embedding。借助 extract_spans() 保留标题结构,切分出来的 chunk 自然对齐到文档章节。面向 LLM 的结构化输出可参考 PDF 转 Markdown 指南

合规与审计:扫描合同、发票、监管文件里的特定条款或关键字。doc.search() 可在所有页面上带坐标地定位用词,或者提取整段文本交给 NLP 做条款识别。

数据抽取:从发票、收据、银行对账单和表单里取出结构化数据。把 extract_chars() 的位置信息和业务规则结合起来,就能找到像「总额」「发票日期」这样的字段并提取旁边的值。

学术研究:面向文献综述、引文提取或元分析,批量处理成千上万篇论文。PDF Oxide 覆盖学术出版中常见的 LaTeX、Word、InDesign、Quark 等各种 PDF 生成器以及它们使用的字体编码。

相关页面