OCR 是最后的手段,而不是第一选择。
- OCR 的原理:看图 -> 猜字。计算量大,不仅慢,而且会丢字、错字。
- PyMuPDF 的原理:解包 -> 拿字。它是直接解析 PDF 的二进制流,提取指令中的 Text Object。
速度对比:
- OCR (Tesseract/Paddle):约 500ms ~ 2000ms / 页。
- PyMuPDF:约 5ms ~ 20ms / 页。 快了整整 100 倍。
1. 为什么是 Fitz,而不是 PDFPlumber?
Python 生态里有很多 PDF 库:PyPDF2(太老,甚至不支持提取坐标)、pdfplumber(好用,但它是纯 Python 写的,慢)、pdfminer.six(也是纯 Python,慢到离谱)。
PyMuPDF (Fitz) 是 C 语言库 MuPDF 的 Python 绑定。
- 快:底层是 C,解析速度吊打 plumber。
- 全:不仅能提文字,还能渲染图片、提取矢量图、甚至修改 PDF。
- 准:它对 PDF 坐标系的理解极其精准(Matrix 变换)。
2. 核心代码:极速提取文字
安装:
Bash
pip install pymupdf
场景 A:我要提取全文文本(用于 RAG 索引或搜索)
Python
import fitz # PyMuPDF 的包名是 fitz,这是个历史遗留问题
def extract_text_fast(pdf_path):
doc = fitz.open(pdf_path)
full_text = []
for page_num, page in enumerate(doc):
# get_text("text") 是最快的方法,保留基本的换行
# text = page.get_text("text")
# 推荐使用 "blocks",它能把文本按段落聚合,并返回坐标
# block 格式: (x0, y0, x1, y1, "text content", block_no, block_type)
blocks = page.get_text("blocks")
page_content = ""
for b in blocks:
# block_type=0 是文本,1 是图片
if b[6] == 0:
page_content += b[4] + "\n"
full_text.append(f"--- Page {page_num + 1} ---\n{page_content}")
return "\n".join(full_text)
# 耗时:处理 100 页 PDF 仅需 1 秒左右
print(extract_text_fast("report.pdf"))
3. 进阶玩法:混合解析策略 (Hybrid Parsing)
这是工程落地中最值钱的部分。 现实世界是复杂的:一份 PDF 里可能前 10 页是电子版,最后 2 页是扫描的附件(图片)。 如果你只用 Fitz,最后 2 页就是空的。如果你全用 OCR,前 10 页就是浪费算力。
最佳实践:基于文本密度的自适应策略
逻辑:
- 先尝试用 Fitz 提取文字。
- 计算 “有效字符数” 或 “文本覆盖率”。
- 如果每页少于 50 个字,或者乱码率极高 -> 降级(Fallback)调用 OCR。
Python
import fitz
from paddleocr import PaddleOCR # 假设我们用 Paddle 作为兜底
# 初始化 OCR (懒加载,只有需要时才跑)
ocr_engine = PaddleOCR(use_angle_cls=True, lang="ch", show_log=False)
def smart_parse_pdf(pdf_path):
doc = fitz.open(pdf_path)
for page_index, page in enumerate(doc):
# 1. 尝试直接提取
text = page.get_text("text")
# 2. 启发式判断:这是扫描件吗?
# 比如:字数少于 50,或者全是换行符
is_scanned = len(text.strip()) < 50
if not is_scanned:
print(f"[Page {page_index}] 命中原生文本 (Fast)")
print(text[:100] + "...") # 处理你的业务逻辑
else:
print(f"[Page {page_index}] 疑似扫描件,启动 OCR (Slow)...")
# 3. 渲染为图片 (Render to Image)
# matrix=fitz.Matrix(2, 2) 表示放大 2 倍,相当于 144 DPI -> 288 DPI
# 对 OCR 来说,清晰度至关重要,默认分辨率往往不够
pix = page.get_pixmap(matrix=fitz.Matrix(2, 2))
# 4. 将 pixmap 转换为 OCR 能吃的格式
# 这一步完全在内存中进行,不需要写磁盘
import numpy as np
# fitz 的 samples 是 RGB 字节流
img_array = np.frombuffer(pix.samples, dtype=np.uint8)
img_array = img_array.reshape(pix.h, pix.w, pix.n) # reshape 成 (H, W, C)
# 如果是 RGB (3通道) 或 RGBA (4通道),转一下
if pix.n == 4:
img_array = img_array[..., :3] # 丢掉 Alpha 通道
# 5. 调用 OCR
ocr_result = ocr_engine.ocr(img_array, cls=True)
# 拼接 OCR 结果
ocr_text = "\n".join([line[1][0] for line in ocr_result[0]]) if ocr_result[0] else ""
print(f"OCR Result: {ocr_text[:100]}...")
4. 解决“表格”噩梦:提取坐标
Fitz 还有一个杀手锏:提取文字坐标。 做表格解析时,最怕的是 PDF 里没有 <table> 标签,只有一堆绝对定位的字。
使用 page.get_text("words") 可以拿到每一个单词的 (x0, y0, x1, y1, word)。
Python
# 提取带有坐标的单词列表
words = page.get_text("words")
# words[0] = (x0, y0, x1, y1, "Invoice", block_no, line_no, word_no)
# 简单的表格还原逻辑:
# 1. 找到所有 y 坐标接近的词,归为“同一行”。
# 2. 找到所有 x 坐标接近的词,归为“同一列”。
虽然 Fitz 不像 pdfplumber 那样内置了表格线检测算法,但它的坐标精度是最高的。对于复杂的报表,我们通常用 Computer Vision (OpenCV) 检测表格线,用 Fitz 提取单元格内的文字,两者结合(IoU 匹配)来还原 Excel。
5. 总结:工程师的“省钱”之道
在云原生时代,算力就是钱。
- OCR 方案:需要 GPU 实例(如 T4),每小时成本 $0.5+,处理速度慢。
- Fitz 方案:只需要 CPU 实例(如 t3.micro),几乎免费,处理速度极快。
选型建议:
- 默认使用 PyMuPDF (Fitz)。它能解决 90% 的合同、发票、研报解析需求。
- 构建“熔断机制”。当 Fitz 提取不出字时,再异步调用 OCR 服务。
- 不要迷信大模型。GPT-4o 也能读 PDF,但它读一页可能要 $0.05。用 Fitz 读一页几乎是 $0。
别拿显卡去算那些 CPU 就能搞定的东西。这是工程师的基本素养。