简介:这是一份围绕 PaddleOCR 构建离线 OCR exe 工具的完整打包资料,面向无 Python 运行环境、希望输入图片路径即可获得识别结果的使用者,也适合想研究桌面化封装与依赖组织的开发者参考,可用于内网、断网或不便安装解释器的办公与实验环境。压缩包共 2000 个文件,约 279.22MB,包含 319 个 py 源码、319 个 pyc、232 个 pyd、139 个 dll,以及 enc、tcl、txt、png 等运行时配置与资源文件;源码、字节码、扩展模块和动态库相互配合,构成可脱离开发环境运行的目录结构。已有 3789 人学习下载,说明其在离线文字识别与打包实践场景中具有一定参考价值。读者可获得可执行的离线识别程序及完整依赖,免去配置 Python 环境;同时能借源码与目录组织理解 PaddleOCR 调用、识别结果写入 txt、模型与依赖随包分发等要点,为二次开发和打包排错提供基础。
1. 内网机器上跑 OCR:PaddleOCR 打包 exe 离线工具到底解决什么问题
把 PaddleOCR 打包成 exe 的离线工具,真正难的不是识别率,而是让一台不能装 Python、不能联网的 Windows 机器也能批量出结果。上周一个做档案数字化的朋友丢来一台内网机,三千多张扫描件,现场只允许双击运行,不允许装环境。这个项目就是干这件事:GUI/CLI 收任务,本地模型推理,结果落 txt/json/csv,模型目录外置,换模型不用重新打包。适合要把 OCR 交付到客户现场、又不想让客户碰 Python 环境的人;也适合想拿 PaddleOCR 做离线批处理工具的开发者。
2. 工程架构与目录:从模型文件到 exe 的离线边界怎么划
打包离线工具最怕一上来就写pyinstaller -F main.py,跑是能跑,换台机器就翻车。我一般先把边界划清楚:哪些东西必须进 exe,哪些东西必须留在外面。PaddleOCR 的推理层、业务代码、UI 资源可以进包;模型文件、配置文件、日志、临时输出目录必须外置。这样客户换模型不用重新打包,现场排查也能直接看日志和配置,不用把 exe 当黑匣子猜。
这个项目的架构不复杂,但每一层都有明确的交付目标。最上面是 GUI 和 CLI 两个入口,GUI 给现场操作员用,CLI 给批处理脚本用;中间是任务层,负责扫描文件、排队、失败重试、进度回传;下面是 OCR 引擎层,封装 PaddleOCR 的检测、方向分类、识别三个模型;最后是导出层,把结果写成 txt、json、csv,并保留坐标和置信度。四层之间用配置和数据结构解耦,不让 UI 直接碰推理代码。
2.1 运行链路:从选择文件夹到结果落盘
一次完整任务的链路是:入口接收路径 → 文件扫描器过滤图片和 PDF → 任务队列逐条处理 → 预处理器做旋转、缩放、灰度等可选操作 → OCR 引擎返回文本块和坐标 → 后处理器做空结果过滤、置信度阈值过滤、按行合并 → 导出器写文件。链路里最容易出问题的是“路径”和“编码”,不是模型本身。开发机上跑得好好的,打包后工作目录变成 exe 所在目录,相对路径全部失效,所以项目里统一用Path和配置中的绝对路径来定位模型与输出目录。
任务队列建议做成单生产者单消费者,GUI 只负责投递任务和刷新进度,不直接调用 OCR。这样批量处理时界面不会卡死,失败任务也能单独记录。对于几百张图片,单进程顺序跑足够;上千张再考虑多进程,但多进程会把 PaddleOCR 的模型加载多次,内存直接上去,现场机器吃不消。常见做法是主进程只加载一次模型,用线程池控制并发,或者干脆顺序跑,用稳定的吞吐换内存。
启动顺序也要固定:先读配置,再解析模型目录,再初始化日志,最后才创建 OCR 引擎。很多人把引擎初始化放在 UI 构造函数里,结果模型路径错了,窗口还没出来就崩了,现场只看到闪退。正确做法是先把错误写进日志,再弹一个可读的错误框,告诉操作员“模型目录不存在”或“检测模型文件缺失”,而不是丢一个 Python traceback。
2.2 目录结构与资源外置策略
项目目录我一般按下面这样拆,核心原则是“代码和资源分离,模型和配置可替换”。下表是文件清单,不是安装包清单,实际交付时会把models、config、logs留在 exe 同级目录。
| 路径 | 作用 | 是否进 exe |
|---|---|---|
main.py | GUI/CLI 入口,解析参数 | 是 |
app/ui/ | 窗口、进度条、文件选择 | 是 |
app/core/ocr_engine.py | PaddleOCR 初始化与推理封装 | 是 |
app/core/model_locator.py | 冻结路径与外部模型目录解析 | 是 |
app/core/batch_runner.py | 批量任务、失败重试、进度回调 | 是 |
app/core/exporter.py | txt/json/csv 导出 | 是 |
config/app.yaml | 模型路径、阈值、输出格式 | 否 |
models/det/ | 检测模型 | 否 |
models/rec/ | 识别模型 | 否 |
models/cls/ | 方向分类模型 | 否 |
logs/ | 运行日志 | 否 |
output/ | 默认输出目录 | 否 |
模型目录再往下拆一层,让每个模型有独立的inference.pdmodel和inference.pdiparams这类文件。项目里不把模型塞进 Python 包,也不在运行时自动下载。配置里写相对路径时,解析规则是“先找 exe 同级models,再找开发环境项目根目录models,最后才报错”。这条规则能同时兼容开发机和客户现场,减少“开发机能跑、打包不能跑”的玄学问题。
配置文件建议用 YAML,字段少但必须稳定。det_model_dir、rec_model_dir、cls_model_dir三个路径分开写,use_angle_cls控制是否启用方向分类,confidence_threshold控制结果过滤,output_format控制导出类型。配置不要写死在代码里,否则客户想调阈值只能找你重新打包。日志目录和输出目录也走配置,默认放在 exe 同级,避免写到C:\Program Files下面没有权限。
2.3 打包边界与构建步骤
打包边界决定交付形态。单文件 exe 看起来干净,但每次启动都要解压到临时目录,模型如果也在里面,启动慢、杀软误报也多。我一般用 onedir 模式:exe 和依赖库放一个目录,模型、配置、日志放同级外部目录。这样升级模型只替换models,升级程序只替换 exe 和_internal,客户现场不用重新配环境。
构建步骤按顺序走:第一,在干净的虚拟环境里安装依赖,避免把开发机全局包带进去;第二,先跑通开发模式下的单图识别和批量识别;第三,准备外置模型目录,确认不联网也能初始化;第四,写 spec 文件,显式收集 PaddleOCR 的动态库和隐藏导入;第五,在另一台没装 Python 的 Windows 机器上做首次启动验证;第六,补日志、错误码和版本号。每一步都要留记录,不然出问题只能靠猜。
提示:构建机尽量和客户机器保持相同的系统位数和运行库环境,不要在 64 位开发机上打 32 位包,也不要把开发机的 CUDA 环境带进 CPU 交付包。
3. 从零跑通:开发环境、模型准备和 exe 构建的三段式流程
这一章按“能跑起来”的目标走,不追求一次到位。先在开发机跑通 Python 入口,再把模型目录固定下来,最后才打包。顺序反了,打包后出问题很难定位是代码、模型还是打包器。下面命令里的包版本不写死,实际项目里用requirements.txt锁版本,避免今天能打包、明天依赖升级又缺 DLL。
3.1 开发环境与依赖锁定
创建虚拟环境,安装 PaddleOCR 推理依赖、GUI 依赖和打包依赖。CPU 交付包只装 CPU 版推理库,不要装 GPU 版,否则包体积和动态库数量都会上去。下面的命令是开发机初始化流程,-i用你实际可用的镜像源,没有就删掉。
python -m venv .venv .venv\Scripts\activate python -m pip install --upgrade pip pip install paddlepaddle paddleocr opencv-python pyyaml pyside6 pyinstaller pip freeze > requirements.txt这段命令做了三件事:建隔离环境、装依赖、把当前环境锁定到requirements.txt。paddlepaddle是推理运行时,paddleocr提供检测和识别封装,opencv-python处理图像读写,pyyaml读配置,pyside6做 GUI,pyinstaller负责打包。实际项目里 GUI 可以用 Tkinter 减体积,但 Pyside6 的进度条和文件选择更稳,现场操作员不容易点错。
依赖锁定后,先写一个最小入口验证模型能加载。不要一上来就跑 GUI,先用 CLI 跑单张图,把模型路径、日志、输出目录三个变量确认下来。最小验证脚本只做一件事:读一张图,调用 OCR,打印第一行文本。能打印出来,说明推理链路通了;打印不出来,先查模型目录和动态库,不要怀疑识别效果。
# smoke_test.py from pathlib import Path from paddleocr import PaddleOCR # 模型目录全部用绝对路径,避免工作目录变化导致找不到 base = Path(__file__).resolve().parent / "models" ocr = PaddleOCR( det_model_dir=str(base / "det"), rec_model_dir=str(base / "rec"), cls_model_dir=str(base / "cls"), use_angle_cls=True, lang="ch", show_log=False, ) result = ocr.ocr(str(Path("samples/demo.png")), cls=True) print(result[0][0][1][0] if result and result[0] else "no text")这段代码的关键参数是三个*_model_dir,它们必须指向本地模型目录,不能留空。留空时 PaddleOCR 可能走默认模型下载逻辑,离线机器直接卡住。use_angle_cls=True表示启用方向分类,扫描件倒置时有用;show_log=False关掉推理日志,避免 GUI 控制台刷屏。result的结构是“页 → 文本块 → [坐标, (文本, 置信度)]”,取文本时要判空,不然空图片会抛异常。
3.2 模型目录和离线配置
模型准备的原则是“提前下载、离线放置、配置显式指定”。检测、识别、方向分类三个目录分开,配置里写相对路径,由model_locator统一解析成绝对路径。下面是一个可用的配置示例,字段名和代码里的读取逻辑保持一致。
# config/app.yaml model: det_model_dir: models/det rec_model_dir: models/rec cls_model_dir: models/cls use_angle_cls: true lang: ch cpu_threads: 4 runtime: confidence_threshold: 0.5 output_format: json output_dir: output log_dir: logsdet_model_dir是文本框检测模型,rec_model_dir是文字识别模型,cls_model_dir是方向分类模型。cpu_threads控制 CPU 推理线程数,现场机器核数少就调低,不然界面会卡。confidence_threshold低于阈值的文本块直接丢弃,减少噪声。output_format可以选txt、json、csv,批量交付时 json 保留坐标最有用。
配置解析代码要处理三种情况:开发环境从项目根目录找,打包后从 exe 同级找,用户手动指定路径优先。常见做法是写一个resolve_path函数,先判断路径是不是绝对路径,再依次拼接候选目录,最后返回存在的那个。找不到就抛一个带候选路径的错误,日志里写清楚“我找过哪些目录”,现场排查不用猜。
模型放好之后,断网跑一次smoke_test.py。如果这时还在下载模型,说明初始化参数没生效,或者配置没有传到PaddleOCR。离线工具的第一条验收标准就是“拔网线能启动、能识别”,这一条不过,后面打包没有意义。
3.3 构建 exe 与首次启动验证
打包用 spec 文件,不用命令行一把梭。spec 里显式写datas、hiddenimports、binaries,把 PaddleOCR 的动态库和资源收进去。下面是一个简化片段,重点是collect_all和外部模型目录不打包。
# build.spec from PyInstaller.utils.hooks import collect_all datas, binaries, hiddenimports = [], [], [] for pkg in ["paddle", "paddleocr", "cv2", "yaml"]: d, b, h = collect_all(pkg) datas += d binaries += b hiddenimports += h a = Analysis( ["main.py"], pathex=[], binaries=binaries, datas=datas, hiddenimports=hiddenimports, excludes=["matplotlib", "pandas"], ) pyz = PYZ(a.pure) exe = EXE(pyz, a.scripts, exclude_binaries=True, name="ocr_offline") coll = COLLECT(exe, a.binaries, a.datas, name="ocr_offline")collect_all把包内的动态库、数据文件、隐藏导入一起收集,适合 Paddle 这种依赖多的库。excludes里排掉不用的重型包,能减体积。COLLECT输出的是目录形态,模型、配置、日志放在ocr_offline同级,不要打进_internal。打包完成后,把整个目录复制到一台没装 Python 的 Windows 机器,双击 exe,先看日志有没有“模型目录不存在”,再看单图识别能不能出结果。
首次启动验证按清单走:窗口能打开、模型能加载、单图能识别、批量能跑完、输出文件能打开、日志有记录、断网不报错。任何一项失败,先看logs/app.log,再看 exe 同级目录结构。不要直接在开发机上判断打包成功,开发机有 Python 环境,很多缺失依赖会被兜住。
4. 核心模块拆解:模型定位、OCR 初始化和批量导出三段代码
这一章拆三个最容易被打包破坏的模块:模型定位、引擎初始化、批量导出。三个模块的共同点是都依赖“路径”和“配置”,打包后工作目录变化、临时解压目录变化,都会让它们失效。代码按项目里实际会写的方式给,参数说明放在代码后面。
4.1 冻结路径下的模型定位
打包后的程序可能从sys._MEIPASS解压临时目录运行,也可能从 exe 同级目录读外部资源。模型必须优先读外部目录,不能读临时目录,否则客户换模型要重新打包。下面函数先判断是否冻结,再按“用户指定 → exe 同级 → 项目根目录”的顺序找。
# app/core/model_locator.py import sys from pathlib import Path def app_root() -> Path: """返回程序可写根目录:打包后是 exe 所在目录,开发时是项目根目录。""" if getattr(sys, "frozen", False): return Path(sys.executable).resolve().parent return Path(__file__).resolve().parents[2] def resolve_model_dir(configured: str) -> Path: """把配置里的相对模型路径解析成绝对路径,找不到时给出候选列表。""" raw = Path(configured) candidates = [] if raw.is_absolute(): candidates.append(raw) else: candidates.append(app_root() / raw) candidates.append(Path.cwd() / raw) for path in candidates: if path.exists() and path.is_dir(): return path.resolve() raise FileNotFoundError(f"模型目录不存在,候选路径:{[str(p) for p in candidates]}")app_root()是路径解析的锚点。冻结状态下用sys.executable的父目录,开发状态用源码文件往上推。resolve_model_dir()先处理绝对路径,再拼 exe 同级和当前工作目录。找不到时把候选路径拼进异常信息,现场看日志就知道程序到底在找哪里。参数configured来自 YAML,不要在这里写死models/det。
4.2 OCR 引擎初始化与参数
引擎初始化要懒加载,不要放在模块导入时执行。模块导入时初始化,一旦模型缺失,整个程序启动就崩,连错误框都弹不出来。下面类把初始化包在ensure_ready()里,调用方先读配置、再初始化、再推理。
# app/core/ocr_engine.py from paddleocr import PaddleOCR from .model_locator import resolve_model_dir class OcrEngine: def __init__(self, config: dict): self.config = config self._ocr = None def ensure_ready(self): if self._ocr is not None: return self._ocr model = self.config["model"] self._ocr = PaddleOCR( det_model_dir=str(resolve_model_dir(model["det_model_dir"])), rec_model_dir=str(resolve_model_dir(model["rec_model_dir"])), cls_model_dir=str(resolve_model_dir(model["cls_model_dir"])), use_angle_cls=bool(model.get("use_angle_cls", True)), lang=model.get("lang", "ch"), cpu_threads=int(model.get("cpu_threads", 4)), show_log=False, ) return self._ocr def recognize(self, image_path: str): ocr = self.ensure_ready() result = ocr.ocr(image_path, cls=True) return result[0] if result else []ensure_ready()做两件事:第一次调用时初始化 PaddleOCR,之后复用同一个实例。det_model_dir、rec_model_dir、cls_model_dir都走resolve_model_dir(),保证打包后也能找到。cpu_threads从配置读,现场机器性能差就调小。recognize()返回第一页的结果,空结果返回空列表,调用方不用处理None。show_log=False关掉底层日志,项目自己的日志统一写文件。
初始化失败要捕获异常并转成可读错误,不要直接把 traceback 丢给操作员。常见错误包括模型文件缺失、动态库加载失败、内存不足。日志里记录模型路径和异常类型,错误框只提示“模型加载失败,请查看日志”。这样现场人员能按提示找维护人员,不会乱点。
4.3 批量任务与结果导出
批量任务的核心是“单条失败不影响整批”,每张图独立 try,失败记录到结果列表,最后统一导出。导出格式用 json 保留坐标和置信度,csv 给表格工具用,txt 给纯文本交付。下面代码是批量处理和 json 导出的简化版。
# app/core/batch_runner.py import json from pathlib import Path from .ocr_engine import OcrEngine SUPPORTED = {".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff"} def collect_images(input_dir: Path): return [p for p in input_dir.rglob("*") if p.suffix.lower() in SUPPORTED] def run_batch(input_dir: str, output_dir: str, engine: OcrEngine, threshold: float = 0.5): images = collect_images(Path(input_dir)) records = [] for image in images: try: blocks = engine.recognize(str(image)) lines = [] for block in blocks: text, score = block[1][0], float(block[1][1]) if score >= threshold: lines.append({"text": text, "score": score, "box": block[0]}) records.append({"file": str(image), "ok": True, "lines": lines}) except Exception as exc: records.append({"file": str(image), "ok": False, "error": str(exc)}) out = Path(output_dir) / "result.json" out.parent.mkdir(parents=True, exist_ok=True) out.write_text(json.dumps(records, ensure_ascii=False, indent=2), encoding="utf-8-sig") return outcollect_images()递归扫描支持的图片后缀,PDF 可以另起一个转换分支,先把页转成图片再进 OCR。run_batch()对每张图单独捕获异常,失败也写进records,最后统一落盘。threshold控制置信度过滤,低分文本不进结果。ensure_ascii=False保证中文不转义,utf-8-sig让 Excel 打开 csv 不乱码,json 用这个编码也能兼容部分 Windows 工具。
导出层不要和推理层混在一起,exporter.py可以再拆 txt、csv、json 三个函数,批量任务只负责收集结果。这样二次开发加一种输出格式,不用改推理代码。批量跑完输出一条汇总日志:总文件数、成功数、失败数、耗时。现场人员看到汇总就知道有没有漏图。
5. 避坑与排查:打包后模型、动态库和路径的翻车现场
打包离线工具的血泪经验基本集中在这几类:模型下载、动态库缺失、路径错乱、杀软误报。每一条我都见过现场版,现象看起来像玄学,原因都很具体。下面按“现象 → 原因 → 解决”写,排查时按顺序看日志和目录,不要一上来就重装系统。
5.1 启动就联网下载模型
现象:双击 exe 后进度条卡住,日志里出现下载超时或连接失败,断网机器直接报错。原因通常是PaddleOCR初始化时没有传本地模型目录,或者配置里的相对路径在打包后失效,底层走了默认模型下载逻辑。解决方法是三个模型目录全部显式传绝对路径,配置解析失败时直接报错退出,不要回退到默认下载。构建前在断网环境跑一次初始化,确认不触发网络请求。日志里打印最终解析到的模型路径,现场一眼能看出找没找对。
5.2 动态库缺失导致闪退
现象:开发机运行正常,复制到客户机器双击没反应,或者命令行里看到DLL load failed。原因是打包器没有收集 Paddle 依赖的推理动态库、OpenMP 运行库或图像处理库,单文件模式解压后路径又变了。解决方法是 spec 里用collect_all收集paddle、paddleocr、cv2的 binaries,构建环境保持干净,不要混装多个 Python 版本。打包后在没装 Python 的机器上测试,别只看开发机结果。命令行启动 exe 能看到更完整的错误输出,GUI 闪退时先切命令行复现。
5.3 中文路径和输出乱码
现象:模型放在中文目录下找不到,或者导出的 csv 用 Excel 打开乱码。原因是部分底层库对非 ASCII 路径处理不一致,加上打包后工作目录变成 exe 所在目录,相对路径全部偏了。解决方法是模型和输出目录尽量用英文路径,配置解析统一用Path.resolve()转绝对路径,导出文本统一utf-8-sig。日志文件也用固定编码,不要依赖系统默认编码。现场如果必须用中文目录,先在开发机复现一次,确认模型加载和输出都正常再交付。
5.4 杀软误报和包体积过大
现象:exe 被拦截、双击没反应,或者整个交付目录几百兆,客户拷贝都嫌慢。原因是单文件自解压行为容易被杀软盯上,加上 Paddle 依赖本身就不小,GPU 版依赖更大。解决方法是改用 onedir 模式,CPU 交付包只装 CPU 推理库,spec 里排除不用的重型包,给 exe 做代码签名或至少提供校验值。包体积不要靠删模型解决,模型外置后程序本体能小很多。现场加白名单要提前沟通,别等交付当天才处理。
注意:排查顺序固定为“看日志 → 看模型目录 → 看动态库 → 看路径编码”,不要同时改三个地方,不然问题复现不了。
6. 二次开发:换模型、加接口和做增量升级的三个改造点
二次开发不建议直接改ocr_engine.py里的初始化参数,先把模型配置抽成注册表:config/models.yaml里写多套模型,运行时按profile选。换模型只改配置,不改代码,验证方法是用同一批固定样张跑新旧模型,对比文本行数和低分块比例。加接口可以在批量任务外面套一层轻量 HTTP 服务,请求传图片路径,返回 json,现场其他系统就能调。接口层只做参数校验和任务投递,不重复写推理逻辑。
# app/api/server.py from fastapi import FastAPI from pydantic import BaseModel from app.core.ocr_engine import OcrEngine app = FastAPI() engine = OcrEngine(config={}) # 实际从配置加载 class OcrRequest(BaseModel): image_path: str @app.post("/ocr") def ocr_one(req: OcrRequest): blocks = engine.recognize(req.image_path) return {"lines": [b[1][0] for b in blocks if b[1][1] >= 0.5]}这个接口只做最小闭环,image_path必须是服务端可访问的绝对路径,返回文本列表。上线前用curl测单图、空图、错误路径三种情况,确认错误码和日志对得上。增量升级的做法是把版本号写进配置,升级时只替换models和 exe,保留config、logs、output。每次发版前在干净机器上走一遍“解压 → 启动 → 单图 → 批量 → 断网”流程。从那以后我每次打离线包都强制走一遍断网验证,不再相信开发机上的“应该没问题”。希望帮到你。
本文还有配套的精品资源,点击获取