OCR It:为LLM应用打通不可复制文档的文本提取链路
OCR It 这条链路解决的问题,是所有 LLM 应用都会碰到的第一道坎:文字明明存在,却拿不出来。扫描件、图片型 PDF、截图里的表格、归档合同、产品说明书,这些文档在屏幕上能看能翻页,但一旦复制就是空白,或者复制出来全是乱码。大模型真正需要的是可检索、可切分、可推理的文本,不是一张图。所以这篇文章要说的,就是怎么把“无法复制的文档”里的内容,通过 OCR 提取成干净文本,再交给 LLM 继续处理。整个过程我会按实际落地顺序来讲:先选工具,再搭环境,跑通单条识别,做图片预处理,把文本清理成 LLM 能用的格式,最后扩展成批量任务。如果你正在做一个 RAG 知识库、文档问答或批量合同提取项目,这篇文章可以帮你把最容易卡住的“文本提取”环节一次理清。
1. 先搞清楚“无法复制的文档”到底卡在哪里
1.1 复制不了的文档,通常卡在三种载体上
第一种是扫描版 PDF。很多人拿到一个 PDF,发现文字可以选中,以为自己能复制,结果粘到编辑器里要么是空白,要么是乱码。这种 PDF 本质上是图片,每一页都是扫描仪生成的图片文件,PDF 外壳里根本没有文本层。第二种是截图和照片,比如网页截图、聊天记录截图、产品包装照片、手机拍的发票。图片里的文字不会天然带编码,复制这个概念在图片层面就不存在。第三种是带权限保护或带复杂排版的文档,比如一些电子合同平台导出的文件,文字被做成矢量图形,或者用特殊字体嵌入,正常的文本复制只能拿到一部分内容,而且顺序还会乱。
还有一种更容易被忽略的情况:文档打开后内容能选中,但复制出去以后排版全乱,表格关系丢了。这种情况不算严格意义上的“无法复制”,但对 LLM 应用来说同样致命。因为模型拿到的是缺失列结构的纯文本,后续做结构化抽取时,字段对应关系会完全错位。所以“无法复制”不是单一问题,而是一类问题的统称。
1.2 为什么 LLM 管道里要先有一步 OCR
多模态大模型确实可以直接读图片,但如果你的目标不是做简单的看图问答,而是要把整份合同、整本手册、整批历史文档做结构化处理,直接把图片投给 LLM 就会遇到三个问题:输入成本高、上下文占用大、输出不稳定。一张高分辨率扫描件的 token 消耗远高于同页文本,而且模型对图片中密集小字的识别能力并没有想象中那么强。
更合适的做法是:先用 OCR 把图片和扫描件转成文本,再做清洗和结构化,最后把干净文本交给 LLM。这样,模型拿到的输入是可控的,你可以决定哪些内容进入上下文,哪些内容先过滤掉。这也是 OCR It 这类项目出现在 LLM 工具链里的原因——它不是在和 OCR 软件竞争,而是在给 LLM 准备“能吃”的数据。
OCR 之后要不要做数据标注,取决于准确率阈值。如果是给自己做一个内部知识库,识别结果差不多能用就行;如果是要上线给客户用,那就需要抽样检查识别结果,把错误样本标出来,反推预处理方案和模型参数。这部分工作很不起眼,但实际项目中,OCR 数据标注和文本提取指标的验证,往往比选模型更费时间。
2. 选型不只看识别率,还要看运行环境和接口方式
2.1 选型前先想清楚四个条件
做 OCR 选型,很多人上来就问哪个工具最好用。我的建议是先确认你自己的运行条件,条件不同,答案完全不同。
第一个条件是语言。纯英文文档和中文文档的选型逻辑不一样。Tesseract 对英文支持成熟,中文需要额外下载语言包;PaddleOCR 对中文版面更友好;在线服务对中英文混排通常处理得都不错。
第二个条件是离线还是在线。如果文档涉及内部资料、合同、身份证件、医疗信息,最好离线处理,不要往第三方接口传;如果只是处理公开网页截图,在线接口反而更方便。
第三个条件是资源。你的环境是普通 Windows 电脑、云服务器,还是 RK3588 这样的 ARM 开发板?CPU 跑和 NPU 跑的方案完全不同。模型大小、内存占用、单张图片推理耗时,都要提前估算。
第四个条件是输出格式。有的工具只能输出纯文本,有的可以输出带坐标的块信息,有的还支持表格结构还原。如果你后续要做版面分析或者表格抽取,那坐标信息和表格结构就是刚需,不能只看文字识别率。
2.2 Tesseract、PaddleOCR、百度 OCR 怎么选
我用一个表格把常见方案的特点列出来,方便你做初步对比。
| 方案 | 离线可用 | 中文识别 | 表格结构 | 部署成本 | 适用场景 |
|---|---|---|---|---|---|
| Tesseract OCR | 支持 | 一般 | 弱 | 低 | 单机小批量、英文文档、快速验证 |
| PaddleOCR | 支持 | 较好 | 中等 | 中 | 中文为主、离线生产、可训练模型 |
| 百度 OCR 云接口 | 不支持 | 较好 | 支持较好 | 按量付费 | 在线高频调用、前端应用 |
| 商业 OCR SDK | 视产品而定 | 稳定 | 支持 | 中高 | 正式项目、并发要求高 |
我的经验是:学习阶段先用 Tesseract,因为它安装简单,语言包覆盖广,遇到问题容易搜到解决方案。中期如果想要更好的中文识别效果,可以迁移到 PaddleOCR,特别是它带版面分析能力,对复杂文档更友好。百度 OCR 这类在线接口,适合不想维护本地环境、调用量稳定、数据不敏感的场景。但要注意,在线接口意味着你的文档内容会经过外部服务器,隐私边界必须先确认清楚。
2.3 在 RK3588 这类 ARM 板卡上跑 OCR 的注意点
热搜里有一条“百度 OCR 怎么在 RK3588 运行”,这个问题的思路需要纠正一下。百度 OCR 是云端接口,本身不依赖本地硬件,RK3588 只需要能发 HTTP 请求就能调用。真正需要本地化运行的是 Tesseract、PaddleOCR 这些离线方案。
RK3588 是有 NPU 的开发板,性能不错,但 NPU 加速不是装个包就能用的。PaddleOCR 要跑在 RK3588 的 NPU 上,通常需要把模型转换成针对 RKNN 的格式,这个过程涉及框架版本匹配、算子支持、量化精度,调试成本不小。如果只是验证功能,直接用 CPU 跑 Tesseract 或者用 PaddleOCR 的 CPU 推理版就够了。
我建议你在板端项目里划分两个阶段:第一阶段先把“单张图片能出文本”这件事跑通,哪怕慢一点,先把输入输出链路确认好;第二阶段再考虑 NPU 推理,这时候你至少知道自己要在哪一类模型上做加速。不要一开始就想着把模型调得飞起,结果连输入图片的分辨率和编码都没处理好。
3. 本地环境配到能跑,Tesseract 和 Python 是最小组合
3.1 最小环境准备
如果你想快速跑通一条 OCR 链路,Tesseract 加 Python 是最省事的组合。Tesseract 负责识别,Python 负责图像读取和文本处理。Windows 系统需要安装 Tesseract OCR 的安装包,安装时注意勾选需要的语言包;Linux 系统可以直接用包管理器安装。
# Linux 安装 tesseract 和中文语言包 sudo apt install tesseract-ocr tesseract-ocr-chi-sim # Windows 则下载安装包,安装后把 tesseract 所在目录加入 PATHPython 侧需要安装 pytesseract 和 Pillow。pytesseract 是 Python 调用 Tesseract 的封装,Pillow 负责读图片、处理图片。
pip install pytesseract pillow如果你的系统里还没有 Python 环境,建议先装一个 Python 3.9 以上版本。安装完成以后,先不要着急写代码,先用命令行验证 Tesseract 本身能不能跑。
tesseract --version这条命令如果正常输出版本信息,说明 Tesseract 核心程序没问题。然后检查语言包:
tesseract --list-langs要能看到chi_sim和eng,中英文识别才有基础。
3.2 验证环境时最容易踩的路径问题
我见过最多的报错不是代码问题,而是“tesseract 命令找不到”。明明安装了 Tesseract,但 Python 里调用一直报错,通常原因有两个:一是安装目录没有加入 PATH,二是 pytesseract 不知道去哪里找可执行文件。
在 Windows 里,经常需要手动指定路径:
pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"在 Linux 环境下,更有可能是语言包缺失,而不是路径问题。如果识别中文时报错Failed loading language 'chi_sim',基本可以断定是中文语言包没有装。另一个常被忽略的问题是文件路径带中文空格。Windows 下如果图片路径或者输出目录包含中文,某些老版本 Tesseract 会出现识别不了或者输出乱码的情况。稳妥做法是先用纯英文路径建一个测试目录,跑通以后再处理复杂路径。
环境验证完,一定要跑一张最简单的图片再继续。不要直接拿扫描合同做测试,先用一张白底黑字的截图,确认识别流程是完整的。这张测试图应该只有一行文字,比如“Hello OCR 测试”。
4. 从一张图片到一段干净文本:单条识别流程
4.1 图片识别:最小代码
当环境和前置条件都准备好之后,单条识别的代码其实非常简洁。下面是最小示例:
import pytesseract from PIL import Image img = Image.open("test.png") text = pytesseract.image_to_string(img, lang="chi_sim+eng") print(text)这段代码的逻辑很简单:读取图片,调用 Tesseract 识别,把识别结果打印出来。但如果真的直接去测试,你会发现结果可能不太理想,原因有几个。第一,图片质量决定识别质量,Tesseract 对模糊、倾斜、光线不均匀的图片很敏感。第二,lang 参数中语言包顺序也会影响结果,建议把主要语言写在前面。第三,image_to_string默认参数并不适合所有版面,需要根据文档类型调整。
这里我建议先做一个判断:如果是单栏正文,直接跑上面这段就行;如果是带表格、多栏、封面页的复杂文档,就要进入版面分析阶段,而不是指望一次识别就能拿到完整结构。
4.2 PDF 文档:先渲染成图片再识别
PDF 类型多种多样。如果不是扫描版 PDF,而是带文本层的 PDF,根本不需要 OCR,直接抽取文本就行。判断方法很简单,用 PDF 阅读器选中文字,如果能拖动选中,说明有文本层。如果没有文本层,就需要把每一页渲染成图片,再做 OCR。
常见的做法有两种:一种是用 pdf2image 配合系统里的 poppler 组件,把 PDF 页转换成 PIL 图片;另一种是用 PyMuPDF 直接渲染页面。如果不想额外安装系统级依赖,我推荐用 PyMuPDF,它的安装和调用都更轻量。
import fitz import pytesseract from PIL import Image import io doc = fitz.open("scan.pdf") for page_num in range(len(doc)): page = doc[page_num] pix = page.get_pixmap(dpi=300) img = Image.open(io.BytesIO(pix.tobytes("png"))) text = pytesseract.image_to_string(img, lang="chi_sim+eng") print(f"--- Page {page_num + 1} ---") print(text)这里最关键的是dpi=300。渲染分辨率太低,文字边缘会糊,识别率直线下降;分辨率太高,生成的图片文件巨大,处理速度变慢,还容易把扫描件的噪点放大。300 dpi 是我比较常用的起点,如果字体较小或者文档清晰度不高,再往上调到 400 或 500。
4.3 关键参数 psm、lang、oem 的含义
Tesseract 有几个参数几乎每次都要用,值得花点时间理解。
| 参数 | 含义 | 常用值 | 建议 |
|---|---|---|---|
| psm | 页面分割模式 | 3 自动分页,6 单块文本 | 复杂版面用 3,单栏正文用 6 |
| lang | 语言包 | chi_sim, eng, chi_sim+eng | 中英文混合用 chi_sim+eng |
| oem | OCR 引擎模式 | 3 默认引擎 | 一般用默认即可 |
psm 参数是很多人忽略的坑。psm=6的意思是“把整张图当成一个文本块”,适合单栏正文;psm=3是“自动分段”,适合像合同、书籍这样有标题、段落、页眉页脚的结构。如果没设置--psm,Tesseract 会默认尝试自动分析页面结构,但分析结果未必可靠。
一个可复用的判断方法:先让 Tesseract 跑一遍默认参数,看看输出里有没有把不同区域的文字混在一起。如果混在一起,就换 psm 或者先做版面切分。不要盲目调参,每次只改一个变量,对比输出结果。
5. 输出质量不稳时,优先怀疑图片预处理而不是 OCR 参数
5.1 预处理不是必须的,但很常用
很多刚接触 OCR 的人,遇到识别率低,第一反应是换工具、调参数。但实际排查下来,更多问题出在图片本身。文字不清晰、背景有底纹、拍摄角度倾斜、光照不均匀,这些都会让识别结果乱七八糟。
图片预处理的目的,是让 OCR 拿到一张更接近“白底黑字、横平竖直、字符清晰”的图像。常见的预处理手段包括:
- 灰度化:把彩色图像转成灰度图,减少颜色干扰。
- 二值化:把灰度图转成纯黑白的二值图,增强文字和背景的对比度。
- 放大:对低分辨率图片做 2 倍放大,让字符占更多像素。
- 降噪:去除扫描件里的污点、网格线、水印。
- 纠偏:把倾斜的文档转正,避免字符歪斜。
5.2 怎么判断该不该预处理
判断标准很简单:跑一次 OCR,看输出的文本是不是明显存在错字、漏字、乱码。如果错字集中在某些区域,比如图片四角、表格线上、水印附近,就说明是局部干扰,应该用预处理把干扰区域处理掉。如果整页都识别不好,可能是分辨率不足,先放大再看。
一个反面案例是,图片本身已经足够清晰,但还是有人加上厚重的二值化处理,结果把笔画薄弱的字给切断,识别率反而下降。所以预处理不是越多越好,而是越合适越好。每一步处理完,都要重新跑一次 OCR,对比前后输出,才能确认这一步是有效还是有害。
我建议先把原图跑一遍,记录输出;然后只做灰度化和放大,再看输出;最后才尝试二值化和降噪。不要一次性把所有处理都加上,否则出了问题,你根本不知道是哪一步引入的。
5.3 一套通用预处理基线
如果你手里已经有了一批测试图片,可以先按这个基线做一轮:
from PIL import Image, ImageOps, ImageFilter img = Image.open("input.png") img = img.convert("L") img = img.resize((img.width * 2, img.height * 2), Image.LANCZOS) img = img.filter(ImageFilter.SHARPEN) img.save("processed.png")这个过程只做三件事:转灰度、放大两倍、轻微锐化。它不会大幅改变图片结构,但通常能让识别率提升不少。如果处理后结果没有改善,再考虑更重的二值化、去噪和纠偏。
实操中,同一批扫描件可能有不同的质量波动。稳妥的方案是先抽 5 到 10 张代表性样本,分别跑预处理前后对比,确认预处理参数能稳定提升效率,再应用到全量数据。不要拿一页效果好就急着铺开。
6. 文本交给 LLM 前,必须做的清理和结构化
6.1 OCR 原始文本里最需要移除的内容
OCR 输出的原始文本,远不是可以直接投喂给 LLM 的格式。它通常包含页眉页脚、页码、识别出来的表格线、乱码符号、多余换行和空白字符。这些内容对人工阅读影响不大,但对 LLM 来说会污染语义。
比如一份合同扫描件,OCR 后可能在每一页顶部都识别出“第 X 页”,在正文中间夹杂着因表格线识别出来的竖线符号,在段落结尾出现多余的空格和换行。这些噪声会让 LLM 在做摘要时把页码当成正文内容,在做信息抽取时把表格线符号当成字段值。
清理思路不复杂,但要有顺序:先去掉明显的页眉页脚和页码,再合并被错误截断的换行,最后用正则清理多余空白和特殊符号。清理完以后,再看有没有遗漏。
6.2 从纯文本到结构化文档
如果你的文档是表格密集型,或者内容有明确层级,单纯清理还不够,需要做结构化。例如发票,OCR 之后最好转成 JSON 字段,比如发票号、开票日期、金额、税额;合同,则按标题、条款、落款切块;说明书,可以按章节组织成 Markdown。
把 OCR 结果转化为结构化数据,通常有两种路径。一种是在 OCR 阶段就利用工具的版面分析能力,比如 PaddleOCR 的表格识别、划框结构化;另一种是把 OCR 文本按规则切分,再用 LLM 做字段抽取。实际项目里两种方法经常结合使用:先让 OCR 尽量保留版面信息,再由 LLM 按提示词抽取关键字段。
6.3 组装成 LLM agent 的输入
当文本已经干净并且结构化,下一步才是把它组装成 LLM 的输入。这里有一个常见误区:把整份 OCR 文本直接塞给 LLM,或者直接把清理后的全文塞进向量库,没有任何分层和截断。
更合理的做法是:把内容按逻辑块拆分,比如按章节、按条款、按问题区域,每个块保留足够的上下文信息,然后决定是直接发给 LLM,还是先做向量化再检索。嵌入向量时,要注意 LLM 文本向量 API 需要在配置里提供正确的向量模型;如果调用没反应,先回来看文本编码和数据格式,是不是把 OCR 的乱码也一起送进去了。
RAG 项目里,OCR 文本只是原料。真正决定检索效果的是文本切分粒度、清洗规则、向量化模型和重排策略。OCR 这一步要是留下太多噪声,后面所有环节都会被放大影响。
7. 从单条到批量:文件命名、失败重试和日志
7.1 批量任务不要只看“能不能跑”
单条识别跑通以后,很多人直接开始批量处理,把所有文件丢进一个循环里,然后就去等结果。等到半夜发现任务停了,或者输出了一堆空文本,才意识到批量处理和单条处理根本不是一回事。
批量任务要额外考虑三件事:第一,文件名不能乱。原始文件命名要是重复,输出文件就会互相覆盖。第二,失败要能重跑。中间某个 PDF 损坏、某张图片太过异常,程序不应该直接崩溃,而应该记录错误,跳过并继续。第三,日志要能定位。哪张图片识别成功、哪张失败、失败原因是什么,都要有记录。
正确的做法是先建好输入目录和输出目录结构。输入目录里放原始文件,输出目录按“处理时间”或者“批次号”建立子目录,每一张图片的识别结果单独保存成一个文件,同时保存一份 OCR 日志,记录每个文件的处理状态。
7.2 批量脚本的骨架
一个稳妥的批量脚本,可以按这个思路组织:
import os import glob import logging from PIL import Image import pytesseract logging.basicConfig(filename="ocr_batch.log", level=logging.INFO) input_dir = "input" output_dir = "output" os.makedirs(output_dir, exist_ok=True) files = sorted(glob.glob(os.path.join(input_dir, "*.png"))) success = 0 failed = 0 for path in files: name = os.path.splitext(os.path.basename(path))[0] try: img = Image.open(path) text = pytesseract.image_to_string(img, lang="chi_sim+eng") if not text.strip(): logging.warning(f"empty result: {path}") with open(os.path.join(output_dir, name + ".txt"), "w", encoding="utf-8") as f: f.write(text) success += 1 logging.info(f"success: {path}") except Exception as e: failed += 1 logging.error(f"failed: {path}, error: {e}") print(f"success: {success}, failed: {failed}")这个脚本里有两个值得注意的设计:一是把空文本单独记为 warning,而不是当成功处理;二是失败的图片不会中断整个任务,会记录到日志里。批量处理完成后,先不看成功数,优先看失败数和空文本数。空文本比报错更难排查,因为它往往是图片质量太差、预处理参数不合适,或者是输入格式不匹配。
7.3 失败重试和并发怎么控制
批量任务跑得慢,很多人第一反应是加并发。建议不要急着加,先把单文件耗时测出来。如果一张 300 dpi 的 A4 扫描件在单线程下需要 3 秒,1000 张就是 3000 秒,接近一个小时。加完并发也许能提速,但并发的成本往往出现在资源占用和日志混乱上。
如果真的要并发,先小规模测试,比如 4 个 worker 跑 20 张图片,观察 CPU、内存和输出顺序是否正常。不要一上来就开 32 并发。另外,重试机制不能只看次数,还要看错误类型。如果是文件损坏,重试 10 次也没用;如果是内存不足,降低并发比重试更有效。
还有一个容易被忽略的问题:批量任务中断后要不要支持断点续跑。最简单的方案是输出文件已经存在就跳过,这样重新执行脚本时,已经处理完的文件不会重复计算。这个逻辑虽然简单,但在大批量任务里能节省很多时间。
8. 常见问题排查顺序,从日志到环境再到输入
8.1 常见错误与处理
做一个表格,把最常见的现象、可能原因和处理方式列出来,方便直接对照。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 报错找不到 tesseract | 安装目录未加入 PATH,pytesseract 不知道路径 | 手动指定 tesseract_cmd,或者在系统环境变量里加 PATH |
| 中文识别乱码 | 语言包缺失,lang 没有指定,编码错误 | 安装 chi_sim 语言包,确认 lang 参数,输出用 UTF-8 保存 |
| 输出为空文本 | 图片太暗、文字太小、格式不支持 | 查看原始图片质量,先做预处理和放大,再识别 |
| 文字东一块西一块 | 排版复杂,psm 模式不合适 | 尝试 psm=6 或先做版面切分,再逐块识别 |
| 批量中间崩溃 | 某个文件损坏,内存不足 | 加日志,跳过失败文件,降低并发或者减小批次 |
| 识别速度特别慢 | 图片分辨率过高,CPU 性能不足 | 降低 dpi,缩小输入尺寸,必要时做并发 |
8.2 通用排查顺序
遇到问题,我通常会按下面这个顺序排查,而不是直接猜。
第一步,看现象。是直接报错,还是输出结果不对?直接报错看日志,输出不对看原始图片和识别文本。
第二步,看输入。图片格式对不对,是 PNG、JPG 还是 PDF?图片有没有通过代码完整读取?路径有没有中文或者空格?文件本身有没有损坏?
第三步,看环境。Tesseract 版本、语言包、pytesseract 版本、系统类型,这些是最容易出问题的变量。运行tesseract --list-langs确认语言包,运行pip show pytesseract确认安装版本。
第四步,看参数。psm、lang、dpi、预处理步骤,是不是在最新一次变更之后才出现问题的?如果是,回滚到上一个能正常输出的状态。
第五步,看工具边界。有的问题不是配置错了,而是 Tesseract 确实处理不了这种类型的内容,比如复杂的旋转表格、手写体、低对比度的水印文字。这时要换思路,要么换 PaddleOCR,要么在 OCR 之前先做版面分析,要么引入人工校验。
排查过程中,不要把原始图片覆盖掉,也不要直接删除失败的中间产物。保留原图、预处理图、OCR 文本三份数据,是定位问题的最低成本方案。
9. 最后我自己的落地建议
OCR It 这类项目真正落地时,最该盯住的不是单张图片识别有多准,而是整条链路够不够稳。我见过不少项目,Demo 阶段效果很好,一上全量数据就崩。原因通常是:输入文件格式太杂,图片质量参差不齐,输出目录没有合理规划,失败任务没有日志,更不用说断点续跑了。
我个人的建议是,先把范围缩到最小:确定你要处理的是哪一类“不可复制文档”,选一个工具,搭好最小环境,用 10 张真实样例跑通单条识别,记录首批输出质量。质量可以接受,再写批量脚本;批量脚本稳定,再考虑并发、接口化、接入 LLM agent 和向量库。如果连 10 张样例都跑不稳,就不要急着扩大规模。
另外,把 OCR 数据和 LLM 之间的边界想清楚。OCR 负责“把字提取出来”,LLM 负责“理解文字意思”。前者出错是丢字、错字、乱码,后者出错是语义偏差、抽取字段不对。两者问题要分开排查,不要一出现抽取结果不对,就去调 OCR 参数。很多时候,OCR 输出的文本本身没问题,是切片逻辑、提示词或者向量检索环节出了问题。
最后留一个自查清单:输入文件能在原工具里正常打开吗?OCR 输出保存成了什么编码?失败文件有没有日志?批量任务重跑会不会覆盖结果?图片质量是否一致?如果你能回答清楚这几个问题,OCR It 这条链路在大多数项目里都不会成为瓶颈。
