
简介这是基于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 实际运行只需要这几个核心依赖paddlepaddle2.5.2 paddleocr2.7.0 numpy Pillow PyYAML shapely scikit-image pyclipper opencv-python-headless这里特别注意opencv-python-headless和opencv-python的区别。桌面工具不需要 GUI 版的 OpenCV 窗口功能用 headless 版可以减少打包体积。再有就是shapely这个包它在 Windows 上偶尔会出现 DLL 加载错误如果遇到可以直接指定安装shapely1.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_dirmodel_dir /det, rec_model_dirmodel_dir /rec, cls_model_dirmodel_dir /cls, use_angle_clsTrue, langch, show_logFalse, use_gpuFalse, ) self._warmed_up False def warm_up(self): # 预热先跑一次空图把模型加载到内存里 import numpy as np from PIL import Image blank np.zeros((64, 64, 3), dtypenp.uint8) self.ocr.ocr(blank) self._warmed_up True def recognize(self, image_path): result self.ocr.ocr(image_path, clsTrue) 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, ], noarchiveFalse, ) pyz PYZ(a.pure) exe EXE( pyz, a.scripts, [], exclude_binariesTrue, nameOCR工具, debugFalse, bootloader_ignore_signalsFalse, stripFalse, upxFalse, consoleFalse, disable_windowed_tracebackFalse, iconicons/app.ico, ) coll COLLECT( exe, a.binaries, a.datas, stripFalse, upxFalse, nameOCR工具, )重点看几个参数datas: 把models目录原样复制到产物目录这是模型文件能够被找到的关键。hiddenimports: 把 PaddleOCR 内部用到的动态导入模块显式列出来避免漏掉。excludes: 排除不用的重量级库比如 matplotlib、Jupyter 这些能显著减小体积。consoleFalse: 隐藏黑色控制台窗口做 GUI 工具时更干净。upxFalse: 不启用 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的加载能省掉一个模型文件的体积。第三是使用upxFalse虽然 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), mask0x3f, cmp0x0, flags0x0, OS0x40004, fileType0x1, subtype0x0, date(0, 0), )配合一个.ico图标能降低一部分误报概率但不是百分百有效。4.4 启动速度太慢用户以为程序卡死了因为 Paddle 框架的库比较大冷启动时加载动态库、初始化模型都会耗时。我的做法是在程序入口加一个 Splash 启动画面先弹一个“正在初始化 OCR 引擎”的进度提示让用户知道程序在干活不是卡死了。如果用了--onefile模式还可以在 exe 旁放一个快捷方式配合运行时预热策略把模型初始化放在后台线程界面先响应起来。另外一个优化点是只加载必要的模型文件。在我的使用场景里方向分类模型不是必须的设置了use_angle_clsFalse之后启动速度快了大概 20%。4.5 高频问题速查表现象核心原因处理方式运行即闪退缺少动态库或 PyInstaller 收集不完整用 ONEDIR 模式检查 paddle/libs 目录 DLL模型找不到路径基于当前工作目录改用sys._MEIPASS或 exe 所在目录拼接路径中文识别乱码模型加载错误或图片分辨率过低检查模型目录是否正确配置rec_image_shape控制台黑框不美观consoleTrue导致spec 中设置consoleFalse杀毒误报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但如果你在代码里也用了cv2PyInstaller 有时会收集到一堆不必要的 opencv 视频编码相关 DLL白白增大体积。用opencv-python-headless替换后问题少很多。5. 离线工具的几个扩展方向如果这套 PaddleOCR 离线工具用顺手了后面可以做的事情其实不少我在交付后顺手做了两个小升级反馈都还不错。第一个是增加批量识别能力。现在的代码一次只能识别一张图稍微改一下可以支持一个文件夹下所有图片的批量处理配合glob遍历目录、把结果统一写入 CSV 文件对票据整理、截图归档这类办公场景特别实用。第二个是增加 PDF 支持。PaddleOCR 直接识别 PDF 需要额外转图片可以用 PyMuPDF 把 PDF 每一页渲染成高分辨率图片再喂给 OCR 引擎。这样一套下来一个“PDF 电子发票批量识别工具”就出来了打包流程一模一样只是代码里多一个 PDF 转图片的环节。这三个方向都可以沿用这次搭好的 PyInstaller spec 文件框架只要把datas里的模型路径和hiddenimports里的模块做对应调整就行。我自己做这类工具最大的感受是PaddleOCR 本身不难用难的是让它变成别人也能随手启动的成品。打包 exe 这个环节看起来只是工程上的收尾实际坑却不少。如果你正在做类似的事建议一开始就把模型路径、版本兼容、spec 文件这些基础打好后面扩展功能就顺畅多了。希望这份实操记录能帮你省掉几个晚上的调试时间。本文还有配套的精品资源点击获取