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 页就是浪费算力。

最佳实践:基于文本密度的自适应策略

逻辑:

  1. 先尝试用 Fitz 提取文字。
  2. 计算 “有效字符数”“文本覆盖率”
  3. 如果每页少于 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),几乎免费,处理速度极快。

选型建议

  1. 默认使用 PyMuPDF (Fitz)。它能解决 90% 的合同、发票、研报解析需求。
  2. 构建“熔断机制”。当 Fitz 提取不出字时,再异步调用 OCR 服务。
  3. 不要迷信大模型。GPT-4o 也能读 PDF,但它读一页可能要 $0.05。用 Fitz 读一页几乎是 $0。

别拿显卡去算那些 CPU 就能搞定的东西。这是工程师的基本素养。