OCR It:为LLM应用打通不可复制文档的文本提取链路

发布时间:2026/8/27 9:45:12
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 rC:\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, langchi_simeng) 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(dpi300) img Image.open(io.BytesIO(pix.tobytes(png))) text pytesseract.image_to_string(img, langchi_simeng) print(f--- Page {page_num 1} ---) print(text)这里最关键的是dpi300。渲染分辨率太低文字边缘会糊识别率直线下降分辨率太高生成的图片文件巨大处理速度变慢还容易把扫描件的噪点放大。300 dpi 是我比较常用的起点如果字体较小或者文档清晰度不高再往上调到 400 或 500。4.3 关键参数 psm、lang、oem 的含义Tesseract 有几个参数几乎每次都要用值得花点时间理解。参数含义常用值建议psm页面分割模式3 自动分页6 单块文本复杂版面用 3单栏正文用 6lang语言包chi_sim, eng, chi_simeng中英文混合用 chi_simengoemOCR 引擎模式3 默认引擎一般用默认即可psm 参数是很多人忽略的坑。psm6的意思是“把整张图当成一个文本块”适合单栏正文psm3是“自动分段”适合像合同、书籍这样有标题、段落、页眉页脚的结构。如果没设置--psmTesseract 会默认尝试自动分析页面结构但分析结果未必可靠。一个可复用的判断方法先让 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(filenameocr_batch.log, levellogging.INFO) input_dir input output_dir output os.makedirs(output_dir, exist_okTrue) 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, langchi_simeng) if not text.strip(): logging.warning(fempty result: {path}) with open(os.path.join(output_dir, name .txt), w, encodingutf-8) as f: f.write(text) success 1 logging.info(fsuccess: {path}) except Exception as e: failed 1 logging.error(ffailed: {path}, error: {e}) print(fsuccess: {success}, failed: {failed})这个脚本里有两个值得注意的设计一是把空文本单独记为 warning而不是当成功处理二是失败的图片不会中断整个任务会记录到日志里。批量处理完成后先不看成功数优先看失败数和空文本数。空文本比报错更难排查因为它往往是图片质量太差、预处理参数不合适或者是输入格式不匹配。7.3 失败重试和并发怎么控制批量任务跑得慢很多人第一反应是加并发。建议不要急着加先把单文件耗时测出来。如果一张 300 dpi 的 A4 扫描件在单线程下需要 3 秒1000 张就是 3000 秒接近一个小时。加完并发也许能提速但并发的成本往往出现在资源占用和日志混乱上。如果真的要并发先小规模测试比如 4 个 worker 跑 20 张图片观察 CPU、内存和输出顺序是否正常。不要一上来就开 32 并发。另外重试机制不能只看次数还要看错误类型。如果是文件损坏重试 10 次也没用如果是内存不足降低并发比重试更有效。还有一个容易被忽略的问题批量任务中断后要不要支持断点续跑。最简单的方案是输出文件已经存在就跳过这样重新执行脚本时已经处理完的文件不会重复计算。这个逻辑虽然简单但在大批量任务里能节省很多时间。8. 常见问题排查顺序从日志到环境再到输入8.1 常见错误与处理做一个表格把最常见的现象、可能原因和处理方式列出来方便直接对照。现象可能原因处理方式报错找不到 tesseract安装目录未加入 PATHpytesseract 不知道路径手动指定 tesseract_cmd或者在系统环境变量里加 PATH中文识别乱码语言包缺失lang 没有指定编码错误安装 chi_sim 语言包确认 lang 参数输出用 UTF-8 保存输出为空文本图片太暗、文字太小、格式不支持查看原始图片质量先做预处理和放大再识别文字东一块西一块排版复杂psm 模式不合适尝试 psm6 或先做版面切分再逐块识别批量中间崩溃某个文件损坏内存不足加日志跳过失败文件降低并发或者减小批次识别速度特别慢图片分辨率过高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 这条链路在大多数项目里都不会成为瓶颈。