拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ddddocr打包exe踩坑全解析:闪退、依赖与杀软误报解决

ddddocr打包exe踩坑全解析:闪退、依赖与杀软误报解决 本来以为ddddocr打包是个十分钟就能收工的活模型推理、图片转文字工具写好后在开发机上跑得飞起。真正开始折腾是从pyinstaller -F那一刻——生成的exe在同事电脑上双击即闪退日志一闪而过没有人在一个报错的cmd窗口面前等我抓bug。后来我才明白不把资源文件、第三方库和运行时环境三个问题理顺ddddocr就算能跑也永远交付不出去。这次记录一下完整的踩坑过程。文章会按实际槽点来组织项目为什么选ddddocr、exe闪退怎么定位、pip版本冲突怎么收场、模型文件为什么找不到、杀软为什么会盯上一个正经工具以及从“能跑”到“好用”我做了哪些调整。如果你也在用ddddocr做验证码识别工具或者正准备把一个带深度模型推理的小项目用PyInstaller交付出去这篇应该能帮你少走不少冤枉路。1. 项目背景为什么要把ddddocr塞进exe里1.1 需求很简单交付却没那么简单这次做的事情其实很常规给测试团队做接口回归系统登录时会给一张验证码图片测试脚本需要在本地把里面的四位数字识别出来然后带着结果继续走后续的自动化请求。开发机上跑这个流程毫无压力pip install ddddocr几行代码就能出结果准确率在普通数字字母验证码上也非常能打。问题出在交付环节。测试员手里的机器就是普通Windows台式机让他们搭Python环境、装依赖、维护虚拟环境既不现实也不友好。最好的方式就是一个exe或者一个安装包双击就能用不要碰命令行。ddddocr在这个场景里几乎是唯一选项完全本地推理图片数据不用出内网模型直接打包在pip包里离线环境也能装没有GPU需求CPU推理就够用。虽然也有人用在线OCR接口应付但内网环境根本连不出去数据也不可能传给第三方。1.2 最小可用代码长什么样先把核心调用逻辑写出来方便后面对照import ddddocr ocr ddddocr.DdddOcr() with open(captcha.png, rb) as f: image f.read() result ocr.classification(image) print(result)这段代码非常经典DdddOcr()是主入口初始化时负责加载内部模型classification()接收图片的字节流bytes返回识别出的文本字符串图片格式、尺寸不需要提前处理ddddocr内部会做归一化。在开发机上跑通之后我以为交付只是顺手的事。结果真正的坑全部在后半程PyInstaller的隐藏依赖、pip版本解析错乱、模型文件丢失、杀软误报——随便哪一个都能让人折腾到半夜。1.3 打包前先确认三件事经历完这次折腾我总结出任何带深度学习模型的小工具打包前都要先把三件事确认明白Python版本和依赖版本对不对。用Python 3.10 ddddocr 1.6.x整体很顺利。如果一开始就上Python 3.12遇到onnxruntime兼容问题后面所有事情都会乱套。有没有外部数据文件。ddddocr的模型文件在pip包安装目录下PyInstaller默认不会替你把它带进产物必须显式收集。最终交付形态是什么。是单文件exe、目录包还是做成安装程序这个决定要前置否则后面路径处理、误报规避全都得返工。这三件事确定好才算真正开始和PyInstaller打交道。2. exe双击就消失PyInstaller的隐藏依赖收集机制2.1 第一次打包后的崩溃现场第一次打包没有任何花哨操作一条最简单的命令pyinstaller --clean --noconfirm --onefile --windowed main.py --name captcha_ocr构建过程很顺产物captcha_ocr.exe大概60MB的样子。当时心想虽然大了点但能交付就行。双击运行窗口没弹出来任务管理器里进程闪了一下就消失了。同事在旁边看着场面相当尴尬。处理闪退的第一原则不要在一台干净机器上瞎猜回到命令行里把真实报错打出来。--windowed会把所有控制台输出吞掉所以我又打了一遍全控制台版本pyinstaller --clean --noconfirm --onefile main.py --name captcha_ocr_debug在cmd里执行captcha_ocr_debug.exe终于看到了关键TracebackModuleNotFoundError: No module named onnxruntime.capi.onnxruntime_pybind11_state2.2 为什么PyInstaller会漏掉onnxruntime很多人在这一步就卡住了明明pip show onnxruntime显示已安装site-packages里也能看到包怎么打包出来就是找不到原因出在PyInstaller的工作机制上。它通过静态分析代码里的import语句顺着依赖树去收集Python模块。问题在于onnxruntime的核心接口是C动态库加上一层Python绑定真正的onnxruntime_pybind11_state模块是运行时通过动态加载方式引入的静态扫描根本看不见。这种情况下只能手动把漏掉的模块塞进产物。最省事的方式是给PyInstaller加参数pyinstaller --clean --noconfirm --onefile main.py ^ --name captcha_ocr ^ --collect-all ddddocr ^ --hidden-import onnxruntime.capi.onnxruntime_pybind11_state--collect-all ddddocr会把ddddocr包内所有子模块、数据文件、动态库一并收集--hidden-import显式告诉打包器存在一个隐藏依赖。重新打包后再跑模块缺失这一关算是过了但很快又暴露了第二个问题。2.3 排查链路比报错本身更重要的是定位顺序整个过程中最值得讲的是我后面总结出来的一套通用排查思路。以后打任何带模型依赖的工具都能复用现象可能原因验证方法对应处理exe启动即闪退打包时缺Python模块或动态库去掉--windowed用console模式跑--hidden-import补依赖报错指向某个路径下的文件不存在数据文件没进产物检查产物目录是否有对应数据文件--collect-data或--collect-all本机能跑、目标机器报错目标机器缺VC运行库或系统组件换一台干净Windows再跑一次分发前补装运行库杀毒软件直接删除exePyInstaller壳特征被启发式规则命中看杀毒日志或暂时关闭实时防护验证换onedir形态、做成安装包用这套顺序排查基本能覆盖ddddocr打包时90%的同类问题。不要一上来就怀疑代码逻辑先把打包链路理清楚。3. 一锅乱麻的pip环境cannot install ddddocr到底错在哪3.1 三个版本并排列出的诡异报错整理打包环境时我顺手重建了一个干净的虚拟环境。结果在装依赖时遇到了一个非常经典的pip解析报错ERROR: Cannot install ddddocr1.0.6, ddddocr1.6.0 and ddddocr1.6.1 because these package versions have conflicting dependencies.三个版本被pip并列排出来看着特别像官方在搞事情。冷静下来看其实是旧环境里曾经装过1.0.6新项目又声明了1.6.0另一处还声明了1.6.1pip在同一环境下发现同一包的不同版本约束互相打架整个解析过程直接作废。这种问题最容易在“pip install包名”的惯性动作中被掩盖。遇到就按下面的顺序处理删掉旧环境重建一个全新的venv。项目依赖只先写ddddocr和pyinstaller不要一次锁太多版本。确认能跑之后再用pip freeze导出完整版本信息。如果仍然解析冲突用pip list | findstr ddddocr看是否存在多版本残留然后pip uninstall ddddocr清干净重装。顺手pip cache purge清理缓存避免pip复用了损坏的包元数据。3.2 ddddocr三个主要版本段区别其实很大网上搜ddddocr充斥着两三年前的旧教程截图里用的还是老API。把几个主流版本段放在一起看差异非常明显版本段常见问题对打包的影响我的建议1.0.x依赖锁得过死容易跟新版numpy起冲突旧版对Python 3.9支持不稳打包后更脆弱不推荐碰1.4.x模型文件在包内接口稳定必须手动收集数据文件漏了就FileNotFoundError老项目可参考新项目慎选1.5.x / 1.6.x对onnxruntime版本要求更明确配合--collect-all ddddocr比较顺推荐固定最新版另外还有一个很常见的误解网上很多教程只说“先装opencv-python再装ddddocr”导致大量读者第一反应就是把opencv塞进环境。实际上ddddocr本身并不依赖opencv-python官方读取图片走的是Pillow。如果后续要自己做灰度、二值化之类的预处理那装上opencv完全合理但不要拿“opencv已安装、ddddocr未安装”当成正常状态。检查是否真的装好正确命令是pip show ddddocr能正常输出版本号才算成功。3.3 把依赖锁清楚后面会少很多事经历这次冲突之后我最终在requirements.txt里只留了三行ddddocr1.6.3 Pillow10.1.0 pyinstaller6.6.0numpy本身是ddddocr通过onnxruntime间接依赖的不需要也不建议单独锁让pip自己解析即可。虚拟环境里一次到位之后所有机器上都没再出现过版本冲突。依赖锁得越乱后续踩坑的概率就越大。4. 模型文件找不到onefile模式下的资源路径陷阱4.1 FileNotFoundError模型被丢在了exe外面--collect-all ddddocr重新打包后程序启动还是失败。这次console里给出的是另一个经典报错FileNotFoundError: [Errno 2] No such file or directory: ddddocr/common_old.onnx这个报错比缺模块更难意识到。它说明的不是Python模块缺失而是数据文件没有进产物。ddddocr在初始化DdddOcr()时需要读取包目录下的onnx模型PyInstaller收集Python源码没问题但非.py文件默认会被忽略。在spec文件里补齐数据文件就能解决# captcha_ocr.spec a Analysis( [main.py], pathex[], binaries[], datas[ (venv/Lib/site-packages/ddddocr, ddddocr), ], hiddenimports[ onnxruntime.capi.onnxruntime_pybind11_state, ddddocr, ], ... )如果用--collect-all ddddocrPyInstaller会自动把这类数据加进来。但如果你习惯直接改spec文件就要主动把datas配好。每个元组的含义是“本机源路径 - exe内目标相对路径”目标路径必须写成ddddocr因为库内部是按ddddocr/xxx.onnx这个相对路径去查找的。4.2 onefile模式的临时目录是第二个大坑这里要说一下onefile模式的本质问题单文件exe在启动时会把自己内部打包的资源解压到系统临时目录类似C:\Users\xxx\AppData\Local\Temp\_MEI98765432然后从这个目录加载模型和动态库。这带来两个副作用如果杀毒软件拦截了临时目录里的解压动作exe会在启动瞬间消失得无影无踪代码里如果用了相对路径读取外部图片会因为“当前工作目录”不等于exe所在目录而失败。针对后者我在入口脚本里统一做了一个资源路径函数import sys import os def resource_path(relative): if getattr(sys, frozen, False): base getattr(sys, _MEIPASS, os.path.dirname(sys.executable)) else: base os.path.dirname(os.path.abspath(__file__)) return os.path.join(base, relative)之后所有外部图片读取都走这个函数不再用裸相对路径。这一步中后期省了特别多的事至少测试员把exe拖到什么目录都能正常使用。4.3 onefile还是onedir我建议直接选后者经历这些之后我对“单文件exe”的执念基本放下了。对ddddocr这种带几十MB推理库的工具onedir模式远比onefile更可控形态优点缺点适用场景onefile单文件分发路径清晰启动要解压误报更高临时目录问题多给懂IT的人发小工具onedir启动快误报少便于排查目录里一堆文件看起来不够“正式”内部使用首选onedir安装包交付体验最好能快捷方式、卸载入口需要额外做安装包步骤面向非技术业务人员最终我采用的是onedir加Inno Setup安装程序的方案。Inno Setup免费开源配置脚本几十行就能把整个目录封装成一个setup.exe还带卸载入口。体验比发一个裸exe好太多。5. 杀软误报没有被杀毒软件点名不算完成交付5.1 PyInstaller的exe为什么总被盯上功能调通、安装包就绪我以为终于可以收工。结果测试同事发来截图Windows Defender直接把exe干掉了红彤彤一片告警。这类误报在PyInstaller产物里太常见了。原因不是代码有毒而是PyInstaller的可执行文件在结构上有几个“高危特征”压缩段、自解压逻辑、在临时目录释放文件并运行、入口代码做了大量反射式加载。这些特征叠加在一起正好撞上杀软启发式规则里“恶意自解压程序”的画像于是被当成木马隔离。5.2 实测有效的几个缓解手段我不能打包票说一定过所有杀软特征库更新速度比翻书还快。但从多次实测看下面几个操作组合起来会好很多改用onedir模式。单文件exe是误报重灾区拆成目录后“自解压”特征消失误报率明显下降。不要开UPX压缩。很多人觉得压缩一下体积小但UPX壳本身就是杀软重点标记对象压完更容易被判毒。用Inno Setup封装安装包。安装程序整体可信度比裸exe高我实测中Windows Defender很少再直接拦截。保留源码和构建命令。把main.py和requirements一并提供给有权限的同事便于内部IT做申诉和白名单操作。5.3 交付前的自检清单我在实际交付前会跑一遍自查防止“自己电脑能跑、别人电脑打不开”的情况换一台没有Python的全新Windows虚拟机完整安装一次把杀软实时防护调到最高档重新安装并观察误报检查%TEMP%目录是否残留_MEI文件夹确认程序退出时干净记录构建时的Python版本、ddddocr版本、PyInstaller版本方便后续问题复现。6. 从能跑到好用并发、预处理和体积优化的实测经验6.1 一个全局OCR对象别每次重新创建代码跑通不等于交付完成。测试脚本动不动就要处理几百张图片如果每张图都重新创建DdddOcr()性能会非常难看——初始化要重新加载模型耗时以秒计而单张识别只要几十到几百毫秒。正确做法是全局只建一份实例import ddddocr ocr ddddocr.DdddOcr() def recognize_bytes(image_bytes): return ocr.classification(image_bytes)多进程场景下可以配合进程池让每个worker自己初始化一个DdddOcr实例from concurrent.futures import ProcessPoolExecutor import ddddocr def init_worker(): global ocr ocr ddddocr.DdddOcr() def worker(image_bytes): return ocr.classification(image_bytes) with ProcessPoolExecutor(max_workers4, initializerinit_worker) as pool: results pool.map(worker, images)注意每个worker都会加载一份完整模型内存占用按worker数量成倍增加。我用的是16GB内存的机器开4个worker很宽裕但完全不建议盲目开几十个。6.2 预处理不要病急乱投医要针对边界情况ddddocr对普通数字字母验证码的识别效果已经不错直接原图识别在多数情况下就是最优解。真正容易出错的反而是边界情况图片四周有大片空白边框先把白边裁掉再识别图片尺寸太小放大2倍后识别率有提升背景色和文字色分布太乱转灰度后用简单阈值处理。我常用的预处理只依赖Pillowfrom PIL import Image from io import BytesIO def preprocess(data): img Image.open(BytesIO(data)).convert(L) w, h img.size img img.crop((int(w * 0.05), int(h * 0.05), int(w * 0.95), int(h * 0.95))) if min(img.size) 40: img img.resize((w * 2, h * 2), Image.LANCZOS) buf BytesIO() img.save(buf, formatPNG) return buf.getvalue()但千万不要把预处理当成识别准确率的万能钥匙对每张图都叠加各种滤镜反而可能破坏模型已经学好的分布。这里只是一个思路具体图片要具体做实验。6.3 体积和启动速度接受现实适当优化ddddocr的exe动辄50MB以上原因很直白onnxruntime推理引擎、模型文件、Python运行时都要跟着走。想压体积可以从两个方向入手只收集真正用到的子模块不要无脑--collect-all所有无关包。如果目标机器都有Python环境直接分发源码和requirements.txt完全绕开PyInstaller的体积问题。实际交付中我最后采用的是“onedir目录 安装包”方案。安装包40MB出头启动速度可接受。毕竟在稳定的依赖版本和“极致的体积小”之间我宁愿选前者。启动时模型加载那两三秒用个启动过渡页就能盖过去没必要为了几个MB牺牲可靠性。6.4 更进一步的替代方案常驻HTTP服务如果只是给一两个人用的内部小工具exe完全够了。但如果要处理大量请求还有一条更稳的路把识别服务做成本地HTTP接口用FastAPI或Flask包一层进程常驻模型只加载一次。from fastapi import FastAPI, UploadFile import ddddocr app FastAPI() ocr ddddocr.DdddOcr() app.post(/ocr) async def ocr_image(file: UploadFile): content await file.read() return {result: ocr.classification(content)}跑起来之后调用方只需要POST图片过去python api_server.py curl -X POST -F filecaptcha.png http://127.0.0.1:8000/ocr这个方案我在后续版本里验证过模型只载入一次进程内连续处理几百张图片依然稳定。将来如果要做管道化批量处理我会直接走这条路而不是在PyInstaller里继续死磕。写到这里这次ddddocr打包的完整心路差不多梳理完了。回想整个流程最贵的教训就是“提前确认依赖版本、资源文件收集方式和交付形态”这三件事。它们看起来不起眼却在各个环节反复折腾人。如果你手头也有类似带深度学习模型的小工具要打包先从这三件事开始规划一定比我少熬两个晚上。
返回列表