PDF 解析

PDF 需要 OCR 吗?用 PyMuPDF 做逐页预检,而不是整份重识别

逐页判断文本层与整页图片,附 PyMuPDF 可运行样例、旋转测试和 OCR 分流边界。

阅读文本 / Reading text
一份 300 页的 PDF,可能前 280 页有文本层,最后 20 页才是扫描附件。如果在入口把它归为“扫描 PDF”,整份 OCR 会增加等待时间,也可能把本来正确的数字识别错。另一个极端同样危险:第一页能够复制文字,就让全部页面跳过 OCR。 更实用的设计,是让每一页带着预检结果进入下一步。本文实现一个保守的三路分流:直接提取、送入 OCR 候选队列、人工检查。它是可解释的工程启发式,不是判断文本质量的万能分类器。 图中来自下面代码生成的合成 PDF,不是客户文件,也不是 OmniDoc 界面。图片页里的文字是栅格像素;文本页的文字则有真实文本层。 先决定要检查什么 这里用两个信号:去掉空白后的文本字符数,以及页面上最大一幅图片占页面面积的比例。前者回答“是否提取到了足够多的字符”,后者帮助区分整页扫描和空白页面。图片面积先与裁切框求交,避免超出页面的图片放大比例。 观测 本文分流 仍然需要注意 至少 40 个非空白字符 TEXT_CANDIDATE 隐藏的坏 OCR 层也可能有很多字 少于 40 字且单幅图覆盖至少 70% OCR_CANDIDATE 纯照片也会满足条件,OCR 不一定有意义 其余情况 REVIEW 空白、矢量字、稀疏标题和复杂拼图都可能在这里 阈值 40 和 0.70 是为了让这组测试容易审计,不是跨语言标准。中文、公式、封面和短表单应该分别标定。这里取最大图片面积,不把重叠图片面积简单相加;因此多块图拼接的扫描页可能落入 REVIEW,这是有意保留的边界。 完整可运行样例 样例使用 PyMuPDF==1.28.0 ,不依赖 Tesseract,不会调用任何在线 OCR 或翻译服务。保存为 example.py ,在空目录中运行。它创建的文件名固定为 routing-fixture.pdf ,真实项目请不要覆盖输入文件。 import json import pymupdf def inspect_page(page): chars = len("".join(page.get_text().split())) bounds = page.cropbox # 图片坐标与文本坐标都是未旋转坐标;不要混用旋转后的 page.rect。 bounds = pymupdf.Rect(0, 0, bounds.width, bounds.height) ratios = [] for image in page.get_image_info(): rect = pymupdf.Rect(image["bbox"]) & bounds ratios.append(max(0, rect.get_area()) / bounds.get_area()) largest = max(ratios, default=0.0) route = ("TEXT_CANDIDATE" if chars >= 40 else "OCR_CANDIDATE" if largest >= 0.70 else "REVIEW") return {"page": page.number + 1, "chars": chars, "largest_image_ratio": round(largest, 3), "route": route} with pymupdf.open() as source: page = source.new_page(width=360, height=480) page.insert_text((28, 60), "Sample invoice 2026-09-26", fontsize=16) page.insert_text((28, 100), "Quantity 12 Unit price USD 15.00", fontsize=12) page.insert_text((28, 132), "Amount USD 180.00 Checked manually", fontsize=12) scan = page.get_pixmap(matrix=pymupdf.Matrix(2, 2)).tobytes("png") page = source.new_page(width=360, height=480) page.insert_image(page.rect, stream=scan) source.new_page(width=360, height=480) source.save("routing-fixture.pdf") with pymupdf.open("routing-fixture.pdf") as pdf: result = [inspect_page(page) for page in pdf] assert [r["route"] for r in result] == [ "TEXT_CANDIDATE", "OCR_CANDIDATE", "REVIEW"] assert result[1]["chars"] == 0 assert result[1]["largest_image_ratio"] == 1.0 pdf[1].set_rotation(90) assert inspect_page(pdf[1])["route"] == "OCR_CANDIDATE" print(json.dumps(result, indent=2)) 接入 OCR 时,缓存的是 TextPage,不是一个成功标记 PyMuPDF 的 OCR 接口是 page.get_textpage_ocr(...) 。Tesseract 和对应语言数据需要单独准备;拿到 TextPage 后,后续提取应把它作为 textpage 参数传入,避免同一页重复识别。具体安装与参数以 PyMuPDF OCR 文档 为准。 本文没有运行 OCR 引擎,所以测试结果只能证明分流逻辑和坐标使用,不代表中文识别率、表格恢复率或翻译质量。生产中应记录 OCR 引擎版本、语言包、页号、输入文件摘要和人工抽检结果。不要只保留最终字符串,否则很难解释某个数字来自哪一页。 三类最容易漏掉的故障 第一,隐藏文字层可能是乱码。字符数量达标只是“可以继续尝试提取”,还要检查替换字符、重复行、关键字段与渲染图的对应关系。对合同编号和金额等关键内容,必须抽检。 第二,矢量路径画出来的字既不是文本,也不是图片。它会进入 REVIEW;不要自动标成空白或静默丢页。第三,关闭 Document 以后再使用 Page 会产生失效对象。保持 with pymupdf.open(...) 的作用域,完成预检、渲染和提取后再释放文件。 不可信 PDF 还应在独立进程中设置文件大小、页数、渲染像素、时间和内存上限。示例是小文件测试,不提供恶意文件沙箱。生产使用 PyMuPDF 前也应核对其许可条件和你的分发方式。 从分流进入翻译工作流 合格的输出不是一串文本,而是带页号和决策原因的记录。翻译前先确认哪些页可以可靠提取;翻译后再验收内容、数字和版式。你可以在 OmniDoc 产品入口 查看文档处理服务,但本文的本地程序独立运行,不依赖 OmniDoc 账号,也不宣称它就是产品内部算法。 结论:先逐页留证据,再决定是否 OCR。把不确定页面留在队列里,比把不确定结果伪装成成功更有用。关于坐标、图片信息与 Page 生命周期,可进一步查阅 PyMuPDF Page API 。 - 1 -
更新于 2026-10-09