简介:这是基于PaddleOCR打造的离线文字识别工具包,将OCR能力完整封装为可直接运行的exe程序,适合没有Python环境的普通用户、办公人员及嵌入式部署场景。使用者只需输入本地图片路径,程序便会调用PaddleOCR预训练模型完成文字识别,并把结果输出到指定txt文件,适合票据、文档、截图等离线识别任务。压缩包共2000个文件,大小约279.22MB,主要包含319个py源码、319个pyc编译文件、232个pyd扩展模块、139个dll动态链接库、143个msg资源,配套tcl、qm、png等辅助文件;py与pyc便于查看和调试逻辑,pyd与dll保障推理性能,tcl、qm提供界面与国际化支持,整体结构完整清晰。目前已有3774人学习下载。除可执行工具外,资源还保留了完整的打包脚本和依赖目录,便于开发者二次修改、替换识别模型,或学习如何用PyInstaller将PaddleOCR项目打包成单文件exe,对需要快速落地离线识别方案的中级Python开发者很有帮助。 最近做了一轮 PaddleOCR 工具的交付,需求很简单,但也挺折磨人:把基于 PaddleOCR 的识别小程序打包成一个 exe,交给没有 Python 环境、甚至不太懂电脑的同事直接双击运行,而且整个识别过程必须在纯离线环境下完成。折腾了几天,踩了不少坑,也把 PyInstaller、Nuitka、模型路径、动态库缺失这些问题从头到尾理了一遍。这篇文章就完整复盘一下,从方案选型到打包命令、从踩坑实录到排错清单,给后面要做类似工具的朋友一个能直接参考的路线。
先说清楚这套工具是干什么的:输入一张图片或者一个 PDF 页面,自动识别里面的文字,输出 txt 或 Excel。整体上看就是一个“图片文字提取器”的桌面工具,核心是 PaddleOCR 的文本检测和文字识别能力,外层套一个简单的界面或者命令行入口,最后用打包工具把 Python 解释器、依赖库、模型文件全部塞进一个可执行文件里。这样做的好处是目标机器上完全不需要装 Python、不用配 CUDA、不用管 pip 依赖,真正实现开箱即用。
1. 项目整体设计与方案选型
1.1 需求拆解:要打包的到底是什么
很多人一想到“打包 exe”,就以为是执行一条 PyInstaller 命令的事,实际上先要把项目本身的结构理清楚。PaddleOCR 打包和普通 Python 脚本打包最大的区别在于它有三个“重量级”组成部分:Python 解释器与依赖库、PaddlePaddle 推理框架的动态库、以及 OCR 模型文件。
这三个部分缺一不可,而且每一部分都会带来不同的坑。Python 依赖可以用 PyInstaller 自动收集大部分,但 Paddle 的某些动态库(比如 paddle 的 fluid 编译模块)PyInstaller 无法自动识别;模型文件则更麻烦,它只是磁盘上的静态文件,PyInstaller 默认只收集代码和二进制库,不会把模型目录塞进去,必须手动指定。
我的项目结构大概是这样的:
ocr_tool/ ├── main.py # 程序入口 ├── ocr_engine.py # PaddleOCR 封装模块 ├── models/ │ ├── det/ # 文本检测模型 │ ├── rec/ # 文字识别模型 │ └── cls/ # 方向分类模型 ├── icons/ │ └── app.ico └── requirements.txt在动手打包之前,一定要先在开发环境跑通完整流程,确定哪些模型文件是实际运行需要的。PaddleOCR 的ocr_engine.py里如果指定了det_model_dir、rec_model_dir、cls_model_dir,那么打包时就只需要带上这六个文件:检测模型的inference.pdmodel和inference.pdiparams、识别模型的同样两个文件、方向分类模型的同样两个文件。如果你直接用paddleocr这个包内置的默认模型,没有手动指定路径,那打包时就得去 site-packages 里找到实际的模型缓存目录,一并收集。
1.2 打包工具对比:PyInstaller 还是 Nuitka
先亮结论,我最终用的是 PyInstaller,但我也拿 Nuitka 做过对比测试。这里把两个方案的真实差异写出来。
PyInstaller 的工作方式是把 Python 字节码、依赖库、资源文件收集到一起,生成一个带引导加载器的可执行文件。它最大的优势是兼容性好、社区成熟,PaddleOCR 相关的坑基本都能在网上找到对应解法,而且支持--add-data直接打包模型等静态文件。缺点是生成的 exe 体积大,因为它是把整个 Python 运行时和依赖都复制一份,而且启动时需要解压到临时目录再运行,第一次启动会明显偏慢。
Nuitka 则是把 Python 代码编译成 C 语言再编译成机器码,性能确实更好,启动速度也快不少,而且因为它是真正的编译产物,反编译的难度比 PyInstaller 高一个量级,适合对代码保护有要求的场景。但 Nuitka 对 PaddlePaddle 这种大量使用 Cython 扩展和动态加载机制的框架支持并不友好,我实测时在编译阶段就报了一堆链接错误,需要额外写很多编译参数去适配,对大部分只想快速交付工具的团队来说太折腾了。
所以如果你不是对启动速度极度敏感、也不是为了防反编译,PyInstaller 是更稳的选择。后来我参考了一些做法,用 PyInstaller 的--onedir模式打包,然后对比了--onefile,结论是:工具要分发给同事用,优先选--onedir。--onefile虽然只有一个 exe 很清爽,但每次启动都要把十几个 MB 甚至几十个 MB 的依赖解压到临时目录,Paddle 这种重型库会导致启动时间高达十几秒,而且更容易被杀毒软件误报。
1.3 离线方案的关键点:模型、推理后端、显存模式
“离线工具”这个概念要分两层理解。第一层是模型推理不联网,PaddleOCR 的模型在第一次使用或者明确指定下载时会从服务器拉取预训练权重,但如果本地已经存在模型文件,它会直接加载本地文件,不会发起网络请求。第二层是目标机器上不需要任何 Python 环境和网络依赖,所有运行所需的 DLL、依赖库全部跟着 exe 走。
为了让工具真正离线可用,我在代码里做了几个强制约束:
- 所有模型路径都改成绝对路径或者相对程序目录的路径,不允许使用默认下载逻辑。
- 推理后端只启用 CPU 推理,不加载 CUDA 相关动态库。因为目标机器大概率没有 NVIDIA 显卡,强行带 CUDA 库只会让打包体积更大、启动更慢。
- 在
paddle.set_device("cpu")层面硬编码,避免运行时去探测环境。 - 关闭 PaddleOCR 的内部日志输出,减少无意义的控制台刷屏。
这样做下来,exe 在完全没有网、没有 Python、没有显卡驱动的 Windows 10 机器上可以正常运行,识别一张普通图片的速度在 1 到 3 秒之间,完全够用。
2. 踩坑前置准备:环境与依赖
2.1 Python 版本和 Paddle 版本怎么搭配
这一块是最容易出问题的,因为 PaddlePaddle 的版本和 Python 版本的兼容矩阵卡得很死。我的建议是直接用 Python 3.8 或 3.9,配上 paddlepaddle 2.4 或 2.5 系列,然后用 paddleocr 2.6 或 2.7 版本,这套组合的兼容性经过最多人验证。
千万不要一上来就装最新版 Python 3.12 配最新版 PaddleOCR 3.x。Paddle 框架的官方 Windows 轮子对高版本 Python 的支持经常会慢半拍,尤其是涉及 C++ 扩展编译的部分,哪怕能装上,PyInstaller 打包时也容易出现奇怪的段错误和 DLL 加载失败。我一开始图新鲜装了 Python 3.11 + paddleocr 3.0,结果打包出来的 exe 在部分机器上直接闪退,后来回退到 3.9 + paddleocr 2.7 才好。
另外要注意:paddlepaddle 有两个版本,一个叫paddlepaddle,是 CPU 版;另一个叫paddlepaddle-gpu,是 GPU 版。我们做离线工具、要尽量控制体积,就只装 CPU 版。GPU 版带一堆 CUDA 和 cuDNN 的库,打包出来动辄上 GB,而且目标机器上没有对应版本的显卡驱动根本跑不起来。
2.2 依赖裁剪:不是所有包都要塞进去
PyInstaller 默认会扫描你 import 的模块,但它的静态分析并不完善,经常会遗漏一些动态导入的库,也会把明明没用到的大库误收进来。我的做法是:先用 pipreqs 扫描当前项目的依赖生成精简版 requirements.txt,然后逐个检查 Paddle 相关包的依赖树。
比如 PaddleOCR 2.7 实际运行只需要这几个核心依赖:
paddlepaddle==2.5.2 paddleocr==2.7.0 numpy Pillow PyYAML shapely scikit-image pyclipper opencv-python-headless这里特别注意opencv-python-headless和opencv-python的区别。桌面工具不需要 GUI 版的 OpenCV 窗口功能,用 headless 版可以减少打包体积。再有就是shapely这个包,它在 Windows 上偶尔会出现 DLL 加载错误,如果遇到可以直接指定安装shapely==1.8.2,这个版本相对稳定。
依赖数量越少,打包越容易,体积越小。我自己测试过,如果原封不动把 conda 环境里所有包都收进去,exe 目录体积能轻松超过 1GB;经过裁剪后可以控制在 400MB 左右。注意这只是体积优化,不是功能阉割,识别能力完全一样。
2.3 模型文件的选择和存储路径
PaddleOCR 官方提供了多套模型,区分检测、识别、方向分类和文本矫正等不同任务。做中文识别的话,我推荐用 PP-OCRv4 或 PP-OCRv5 的中文模型,识别精度比老版本提升非常明显,特别是在中英文混排、印刷体、表格字符这些场景下。
模型文件下载好之后,建议按固定目录存放,我放在项目目录下的models/文件夹里,然后再通过代码指定路径加载。这里有一个关键点:代码中不要写绝对路径,因为打包后的程序可能在任意目录运行,最好用相对路径拼出当前程序所在目录,比如:
import sys import os def resource_path(relative_path): """获取资源文件的绝对路径,兼容打包后的 exe 运行场景""" base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))) return os.path.join(base_path, relative_path)如果是--onedir模式,_MEIPASS这个变量不存在,直接取当前 exe 所在目录就行。如果是--onefile模式,PyInstaller 会把资源解压到临时目录,_MEIPASS就是指这个临时目录,这个兼容逻辑非常重要,否则打包后运行会找不到模型文件。
3. 实操:PyInstaller 打包完整流程
3.1 打包前的代码改造:封装 OCR 引擎
为了打包顺利,建议把 PaddleOCR 的调用单独封装一个模块,尽量不要直接在 UI 回调函数里到处初始化模型。这样做有两个好处:一是模型只初始化一次,避免重复加载内存溢出;二是打包时只需要关注这一个模块的依赖,排查问题也更聚焦。
我的ocr_engine.py核心逻辑大致是:
from paddleocr import PaddleOCR import logging logging.getLogger("ppocr").setLevel(logging.WARNING) class OcrEngine: def __init__(self, model_dir): # model_dir 是模型根目录 self.ocr = PaddleOCR( det_model_dir=model_dir + "/det", rec_model_dir=model_dir + "/rec", cls_model_dir=model_dir + "/cls", use_angle_cls=True, lang="ch", show_log=False, use_gpu=False, ) self._warmed_up = False def warm_up(self): # 预热:先跑一次空图,把模型加载到内存里 import numpy as np from PIL import Image blank = np.zeros((64, 64, 3), dtype=np.uint8) self.ocr.ocr(blank) self._warmed_up = True def recognize(self, image_path): result = self.ocr.ocr(image_path, cls=True) lines = [] if result and result[0]: for item in result[0]: text = item[1][0] lines.append(text) return "\n".join(lines)这里有个小经验:在正式识别前做一次warm_up,用一个空白图片把模型先加载到内存,这样用户真正丢图片进来时响应会快不少,避免第一次识别等很久。
另外,入口文件main.py里建议加一个简单的命令行交互逻辑,方便在没有图形界面的场景下使用。可以做成:
- 直接拖拽图片文件到 exe 上运行,识别结果输出到同名 txt 文件。
- 或者加一个简单的 tkinter 界面,选择图片后点击按钮输出结果。
如果为了省事,先做成拖拽识别也没问题,用 sys.argv 读取拖进来的文件路径即可。
3.2 写 spec 文件:比命令行更适合复杂项目
直接用pyinstaller -F -w main.py这种方式做简单脚本没问题,但 PaddleOCR 这种复杂项目,我强烈建议用 spec 文件。spec 文件相当于 PyInstaller 的配置文件,可以把所有参数、数据文件、排除项都固化下来,方便重复构建。
我的ocr_tool.spec参考如下:
# -*- mode: python ; coding: utf-8 -*- a = Analysis( ["main.py"], pathex=[], binaries=[], datas=[ ("models", "models"), ("icons", "icons"), ], hiddenimports=[ "paddleocr", "paddle", "paddle.nn", "paddle.tensor", "shapely", "skimage", "pyclipper", "imghdr", ], hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=[ "matplotlib", "IPython", "jupyter", "pytest", "tkinter.test", ], noarchive=False, ) pyz = PYZ(a.pure) exe = EXE( pyz, a.scripts, [], exclude_binaries=True, name="OCR工具", debug=False, bootloader_ignore_signals=False, strip=False, upx=False, console=False, disable_windowed_traceback=False, icon="icons/app.ico", ) coll = COLLECT( exe, a.binaries, a.datas, strip=False, upx=False, name="OCR工具", )重点看几个参数:
datas: 把models目录原样复制到产物目录,这是模型文件能够被找到的关键。hiddenimports: 把 PaddleOCR 内部用到的动态导入模块显式列出来,避免漏掉。excludes: 排除不用的重量级库,比如 matplotlib、Jupyter 这些,能显著减小体积。console=False: 隐藏黑色控制台窗口,做 GUI 工具时更干净。upx=False: 不启用 UPX 压缩。UPX 虽然能压缩体积,但经常把 PyInstaller 打包的程序压坏,出现运行时崩溃,所以直接关掉。
3.3 执行打包命令与产物检查
写好 spec 文件后,在项目根目录执行:
pyinstaller ocr_tool.spec --noconfirm --clean--noconfirm表示覆盖已有产物不询问,--clean清理之前的缓存文件。构建时间取决于机器性能,一般在 3 到 10 分钟不等,中间如果出现黄色警告可以不用太紧张,关键是最后要看到completed successfully之类的提示。
构建完成后,在dist/OCR工具/目录下会有 exe、一堆 DLL、模型文件夹和依赖包。理想情况是整个目录可以直接拷贝到其他机器上运行。但在交付前,一定要在干净的虚拟机或者没有安装 Python 的开发机上做一轮完整验证,我有一次自以为打包没问题,结果交付到客户机器上直接报错DLL load failed,原因就是漏了一个运行库。
验证清单可以按这个流程走:
- 双击 exe,确认窗口能正常打开。
- 拿一张包含中文、数字、英文的测试图,跑一次识别,确认输出结果与开发环境一致。
- 断网状态下再跑一次,确保不依赖任何网络请求。
- 拷贝整个
dist/OCR工具目录到另一台机器运行,确认不会因为缺 Python 环境而报错。
3.4 体积优化:从 800MB 到 400MB 的调整
打包完成后第一件事就是看体积,正常情况下 PaddleOCR 打包出来不会小于 300MB,这是框架特性决定的,别指望压缩到几十 MB。但可以做几件事来优化:
第一是我前面提到的排除 matplotlib 等无关库。PaddleOCR 内部有些子模块会 import matplotlib 用于可视化,但如果你只是做文字提取,完全用不上,可以排除掉。
第二是模型瘦身。PP-OCRv4 的完整模型包含检测、识别、方向分类三类参数,合起来大约 30MB 到 80MB,这部分没有太多压缩空间。但要注意:如果只做水平文字识别,不需要方向分类的话,可以去掉cls_model_dir的加载,能省掉一个模型文件的体积。
第三是使用upx=False,虽然 UPX 理论上能压缩,但实测压缩后程序启动反而更慢,而且部分安全软件会标记带 UPX 壳的程序为可疑文件。权衡下来,不压更省心。
4. 常见问题与排查技巧实录
4.1 打包后运行报“DLL load failed”怎么查
这是 PaddleOCR 打包最经典的问题,基本上每个做这个的人都会遇到。原因大多是 PyInstaller 没有正确收集 Paddle 底层的 C++ 动态库。排查方法是:在开发环境写一个最小脚本,用ctypes.WinDLL逐个加载 Paddle 相关 DLL,看具体是哪一个加载失败。
经验做法是在 spec 文件的binaries参数里直接指定 Paddle 的 DLL 目录,可以通过paddle.sysconfig.get_include()和paddle.sysconfig.get_lib()拿到实际路径。或者更简单粗暴:找出 Python 环境 site-packages 里的paddle/libs目录,把这个目录下的所有 DLL 全部加入binaries。
4.2 模型文件找不到,但明明已经在 datas 里指定了
如果代码里使用相对路径models/det这种形式,打包 exe 后当前工作目录可能不是 exe 所在目录,特别是双击运行时,工作目录可能被定位到系统目录。解决办法统一用前面说的resource_path函数,基于 exe 所在目录拼接模型绝对路径,不要依赖相对路径。
4.3 杀毒软件误报为木马怎么办
PyInstaller 打包的程序被误报是高频问题,当然我不能说这种方式打包的程序存在恶意,但现实情况是很多杀毒软件对 PyInstaller 的引导启动器有比较高的误报率。个人经验是:尽量用--onedir模式,不要用--onefile;加一个正规的版本信息文件和图标;如果是内部工具,可以申请加入杀毒软件的白名单。
给 exe 加版本信息和图标可以用这个资源文件,在 spec 里这样指定:
from PyInstaller.utils.win32.versioninfo import FixedFileVersion version_info = FixedFileVersion( filevers=(1, 0, 0, 0), prodvers=(1, 0, 0, 0), mask=0x3f, cmp=0x0, flags=0x0, OS=0x40004, fileType=0x1, subtype=0x0, date=(0, 0), )配合一个.ico图标,能降低一部分误报概率,但不是百分百有效。
4.4 启动速度太慢,用户以为程序卡死了
因为 Paddle 框架的库比较大,冷启动时加载动态库、初始化模型都会耗时。我的做法是在程序入口加一个 Splash 启动画面,先弹一个“正在初始化 OCR 引擎”的进度提示,让用户知道程序在干活,不是卡死了。如果用了--onefile模式,还可以在 exe 旁放一个快捷方式,配合运行时预热策略,把模型初始化放在后台线程,界面先响应起来。
另外一个优化点是只加载必要的模型文件。在我的使用场景里,方向分类模型不是必须的,设置了use_angle_cls=False之后,启动速度快了大概 20%。
4.5 高频问题速查表
| 现象 | 核心原因 | 处理方式 |
|---|---|---|
| 运行即闪退 | 缺少动态库或 PyInstaller 收集不完整 | 用 ONEDIR 模式,检查 paddle/libs 目录 DLL |
| 模型找不到 | 路径基于当前工作目录 | 改用sys._MEIPASS或 exe 所在目录拼接路径 |
| 中文识别乱码 | 模型加载错误或图片分辨率过低 | 检查模型目录是否正确,配置rec_image_shape |
| 控制台黑框不美观 | console=True导致 | spec 中设置console=False |
| 杀毒误报 | PyInstaller 引导器特征 | 添加版本信息、图标、考虑 onedir 模式 |
| 第一次运行很慢 | 动态库加载和模型初始化 | 加启动画面,做预热识别 |
4.6 我在实际打包中踩过的几个坑
第一个坑是升级了 PaddleOCR 3.x 后,之前的 spec 文件不能直接复用,因为 3.x 对模型管理的 API 做了较大调整,目录结构也不一样了。如果看到类似got an unexpected keyword argument的报错,多半是版本不匹配,不要急着改代码,先检查 paddleocr 和 paddlepaddle 的版本对应关系。
第二个坑是 Python 3.11 环境打包后在 Windows 10 老版本上跑不起来,报错提示缺少VCRUNTIME140.dll的某几个函数。这是因为高版本 Python 依赖的 VC 运行库较新,老系统上没装。要么在目标机器上装 VC++ 运行库,要么直接换 Python 3.8/3.9 打包,明显更省事。
第三个坑是 opencv 的cv2模块。PaddleOCR 依赖 opencv,但如果你在代码里也用了cv2,PyInstaller 有时会收集到一堆不必要的 opencv 视频编码相关 DLL,白白增大体积。用opencv-python-headless替换后问题少很多。
5. 离线工具的几个扩展方向
如果这套 PaddleOCR 离线工具用顺手了,后面可以做的事情其实不少,我在交付后顺手做了两个小升级,反馈都还不错。
第一个是增加批量识别能力。现在的代码一次只能识别一张图,稍微改一下可以支持一个文件夹下所有图片的批量处理,配合glob遍历目录、把结果统一写入 CSV 文件,对票据整理、截图归档这类办公场景特别实用。
第二个是增加 PDF 支持。PaddleOCR 直接识别 PDF 需要额外转图片,可以用 PyMuPDF 把 PDF 每一页渲染成高分辨率图片再喂给 OCR 引擎。这样一套下来,一个“PDF 电子发票批量识别工具”就出来了,打包流程一模一样,只是代码里多一个 PDF 转图片的环节。
这三个方向都可以沿用这次搭好的 PyInstaller spec 文件框架,只要把datas里的模型路径和hiddenimports里的模块做对应调整就行。
我自己做这类工具最大的感受是:PaddleOCR 本身不难用,难的是让它变成别人也能随手启动的成品。打包 exe 这个环节看起来只是工程上的收尾,实际坑却不少。如果你正在做类似的事,建议一开始就把模型路径、版本兼容、spec 文件这些基础打好,后面扩展功能就顺畅多了。希望这份实操记录能帮你省掉几个晚上的调试时间。
本文还有配套的精品资源,点击获取