
第 22 讲 | 打包成 exe让没装 Python 的人也能双击运行本讲目标把程序变成一个 exe 文件拷给谁都能双击运行。打包本身是一条命令的事90% 的失败都出在路径——这一讲把路径问题彻底讲透。配套代码cases/c22_package_ready.py、build/build.py、build/app.spec完整下载地址请看底部 *** 完整专栏共 22 讲、22 个可运行案例。一、为什么打包是交付的最后一关你写好了工具功能都对了。发给同事的时候你面对的现实是同事电脑上没有 Python就算有他也不会pip install PyQt6 pandas openpyxl就算装了你还要教他打开命令行、cd到目录、python xxx.py这一步不解决你的工具就只能自己用。PyInstaller 解决的就是这件事它把 Python 解释器、你的代码、所有依赖库、需要的资源文件全部塞进一个可执行文件里。用户双击就运行不需要任何环境。pipinstallpyinstaller装完之后最简打包命令就是一行pyinstaller-F-w-n我的工具 cases/c22_package_ready.py三个参数参数含义不写会怎样-F--onefile打成单个exe默认是-D输出一个文件夹几十个文件-w--windowed窗口程序不弹黑框控制台运行时会多一个黑色命令行窗口跟着-n--name输出文件名用脚本文件名当 exe 名c22_package_ready.exe很难看打包完产物在dist/目录下。这个 exe 就是你要交付的东西。-F还是-D这是个真问题-Fonefile-Donedir产物一个 exe约 4060MB一个文件夹里面几百个文件启动速度慢每次都要先解压到临时目录快分发极简单就发一个文件要打包成 zip 发整个文件夹杀毒软件误报率更高自解压行为像病毒低定位问题难容易文件都在明面上推荐策略给同事/客户用小工具 →-F内部长期使用的工具 →-D。-F的启动慢是真实存在的它每次运行都会把内部压缩包解压到%TEMP%\_MEIxxxxxx大程序要等 25 秒才有界面。用户会以为点了没反应。这也是为什么下面的路径知识必须先搞懂。二、核心路径三兄弟打包后 90% 的报错都是同一个根因找不到文件。源码里这么写跑得好好的dfpd.read_excel(data/input.xlsx)# ← 打包后必然失败iconQIcon(assets/app.ico)# ← 打包后必然失败为什么打包前能用因为你在 PyCharm 里运行时当前工作目录是工程根目录data/input.xlsx正好能找到。为什么打包后不行因为用户双击 exe 时当前工作目录是 exe 所在的那个目录——可能是桌面、可能是 D 盘某个文件夹总之不是你的工程根目录。相对路径从这里出发当然找不到。结论打包后一律用绝对路径绝对路径由代码算出来。而算出来又分两种因为只读资源和可写文件的位置规则完全不同只读资源可写文件例子图标、内置模板、字典文件日志、导出结果、数据库打包后在哪sys._MEIPASS临时解压目录exe 所在目录能不能写不能退出时会被整目录删掉可以用什么函数resource_path()app_dir()/data_dir()defis_frozen()-bool:是不是被 PyInstaller 打包运行打包后这个标志为 Truereturngetattr(sys,frozen,False)defresource_path(rel:str)-Path:只读资源图标、内置模板、字典文件等。 源码运行 → 工程根目录 打包运行 → PyInstaller 解压出来的临时目录sys._MEIPASS ifis_frozen():basePath(getattr(sys,_MEIPASS,Path(sys.executable).parent))else:baseROOTreturnbase/relifrelelsebasedefapp_dir()-Path:程序所在目录可写日志、导出文件、数据库都放这里。 ★ 绝对不要把可写文件放进 resource_path()那个目录在退出时会被清理。 ifis_frozen():returnPath(sys.executable).resolve().parentreturnROOTdefdata_dir()-Path:可写数据目录app_dir 下的 data/不存在就创建dapp_dir()/datad.mkdir(parentsTrue,exist_okTrue)returndsys._MEIPASS到底是什么它是 PyInstaller 在打包运行时的临时解压目录。-Fonefile模式下exe 内部其实是一个压缩包。运行的时候PyInstaller 会先把所有内容解压到一个临时目录C:\Users\xxx\AppData\Local\Temp\_MEI123456\ ├── c22_package_ready.exe ← 你的脚本编译后的字节码 ├── utils/ ← --add-data 收进来的包 ├── PyQt6/ └── ...然后把sys._MEIPASS指向这个目录并把它加到sys.path里最后才启动 Python 执行你的脚本。程序退出时这个目录会被整个删掉。所以图标、模板从这里读 →没问题日志、导出文件往这里写 →灾难用户关掉程序文件就没了这三个函数我建议你直接抄进任何要打包的工程它是能不能顺利打包的地基。打包自检工具cases/c22_package_ready.py就是一个自检面板把所有路径摊开给你看frozen是否已打包 : True sys.executable : D:\dist\打包自检工具.exe sys._MEIPASS : C:\Users\xxx\AppData\Local\Temp\_MEI234567 当前工作目录 os.getcwd() : C:\Users\xxx\Desktop ── 本工程解析结果 ────────────────────── resource_path() 只读资源根 : C:\...\_MEI234567 app_dir() 可写目录 : D:\dist data_dir() 数据目录 : D:\dist\data建议你每次打包完先运行这个自检页把路径念一遍——比事后猜为什么找不到文件快一百倍。三、四个容易漏的打包参数①--paths加搜索路径--paths工程根目录你的案例脚本里有import utils.env而utils是工程根目录下的一个包。PyInstaller 分析时找不到它就会报ModuleNotFoundError: No module named utils。--paths告诉它去这里找。注意--paths只管分析时能找到不会把包的内容收进去。执行时import utils还是会失败——所以还要第二个参数。②--add-data把资源/包一起打包进去--add-data源路径;utils# Windows 用分号--add-data源路径:utils# Linux/macOS 用冒号 ⚠️这是个经典的跨平台坑Windows 用;分隔Linux/macOS 用:。写脚本自动打包时要按平台判断sep;ifos.namentelse:cmd[--add-data,f{ROOT/utils}{sep}utils]把utils包收进去之后exe 里就有utils/了import utils.env正常工作。③--exclude-module砍掉用不到的大块头--exclude-module matplotlib --exclude-module PyQt6.QtWebEngineCorePyInstaller 有时会因为间接依赖把一堆你用不到的库也收进去matplotlib、QtWebEngineCore一个就 150MB、QtMultimedia……排除它们是体积优化的最大来源。一个普通工具 exe 从 180MB 砍到 60MB是常事。④--collect-all万一提示缺模块--collect-all openpyxl有些库带数据文件比如openpyxl的样式模板、pandas的pandas.libsPyInstaller 的静态分析可能漏掉。运行时报找不到 xxx.txt / xxx.dat时用--collect-all强制整个包收进来。四、一键打包脚本本工程提供了build/build.py把上面所有参数自动化NAME_MAP{c08:(c08_excel_merge.py,Excel批量合并工具),c09:(c09_word_batch.py,劳动合同批量生成器),...c22:(c22_package_ready.py,打包自检工具),}EXCLUDES[matplotlib,scipy,IPython,notebook,PyQt6.QtWebEngineCore,PyQt6.QtWebEngineWidgets,PyQt6.QtQuick,PyQt6.QtQml,PyQt6.Qt3DCore,PyQt6.QtMultimedia,PyQt6.QtBluetooth,]用法python build/build.py c08# 只打包第 8 讲的工具python build/build.py c22# 打包自检工具python build/build.py all# 全部打包很慢python build/build.py clean# 清理中间产物两个值得学的设计①NAME_MAP把脚本名和给用户看的 exe 名分开。exe 叫Excel批量合并工具.exe而不是c08_excel_merge.exe——用户看到的名字就是产品的名字这一步的体验差别非常大。② 打包前先探测图标是否存在不存在就不加--iconiconROOT/assets/app.icoificon.is_file():cmd[--icon,str(icon)]别让缺一个图标文件导致整个打包失败。五、spec 文件正式发布该用它命令行参数多一点就长得没法看。PyInstaller 还有一个更专业的方式——用build/app.spec配置文件它其实就是一份 Python 脚本pyinstaller build/app.specspec 文件的好处命令行spec 文件可读性差一行几百字符好结构化版本管理难散在文档里可以直接提交进 git多人协作每人手敲一遍容易不一致一份配置人人相同高级定制基本做不了可以改 Analysis/PYZ/EXE 各段结论临时试试用命令行要正式发布、要给别人复现用 spec 文件。六、常见报错对照表这一节建议收藏。打包的报错其实就那么几种。报错 / 现象原因解法双击 exe闪一下就没了有异常但-w模式下看不到 stderr改用pyinstaller -c打包在命令行里跑一次看报错ModuleNotFoundError: No module named utils自定义包没被收进去加--paths 根目录--add-data 根/utils;utilsFileNotFoundError: data/input.xlsx用了相对路径改用resource_path()/app_dir()图标、模板打包后不显示资源没被--add-data收进去--add-data 源;assets并用resource_path()读导出文件重启就没了写到_MEIPASS里去了改写到app_dir()那个目录退出时会被删ImportError: DLL load failed缺 VC 运行库 或 架构不匹配装 VC Redistributable确认 64 位 Python 打 64 位 exeexe被 Windows Defender 拦下-F自解压行为触发启发式换-D或做代码签名或提交误报申诉exe体积 200MB收进了 QtWebEngine / matplotlib 等--exclude-module逐个排除其他地方能跑换台电脑就不能目标机缺运行库同上装 VC Redist其中闪一下就没了最值得练一遍把-w换成-c或直接删掉-w在一个命令行窗口里运行 exe报错信息就全打出来了。这是排查打包问题的第一个动作。七、体积优化清单打出来的 exe 太大按这个顺序砍步骤效果1.--exclude-module PyQt6.QtWebEngineCore最大能砍 100MB2.--exclude-module matplotlib scipy notebook砍 3080MB3. 用-D代替-F体积差不多但启动快、误报少4. UPX 压缩--upx-dir再小 30%但更容易被杀毒误报5. 检查是不是装了一堆没用的包干净的虚拟环境能省很多第 5 条最容易被忽略PyInstaller 会扫描你的环境。如果你的全局环境里装了几百个包它可能把一些间接依赖也收进去。正确做法是在一个干净的虚拟环境里打包。八、发布前的最终检查清单一个能交付的版本过一遍这张表在另一台没装 Python 的电脑上试过最容易被跳过也最容易出问题启动时间可接受-F要等几秒必要时加个启动 splash首次运行不需要管理员权限日志文件确实生成在app_dir()/data/下配置文件能正常读写第 21 讲的QSettings换个分辨率/缩放比例125%、150%界面不错位窗口标题是产品名不是c22_package_ready图标正常任务栏、窗口左上角、文件管理器里都对版本号写在--name或 About 对话框里方便用户报问题时说清版本附一份 README怎么用、日志在哪、出问题找谁九、小结打包一条命令pyinstaller -F -w -n 名字 脚本.py-F单文件、-D文件夹对外分发用-F长期自用/内部用-D90% 的打包问题都是路径问题记住路径三兄弟is_frozen()判断是否已打包resource_path()读只读资源sys._MEIPASSapp_dir()/data_dir()读写可写数据exe 所在目录sys._MEIPASS是临时解压目录程序退出就被删绝不能往里写文件--paths解决找不到自定义包--add-data把包/资源真正收进去Windows 用;Linux/macOS 用:--exclude-module是体积优化的主力--collect-all是缺模块时的救兵正式发布用.spec文件能进版本管理exe 闪退就用-c重新打包报错就看得见了十、全专栏结语到这里22 讲结束了。回头看一下这条路径Part讲了什么你获得了什么Part 11-3环境、布局、信号与槽PyQt6 的地基——任何界面都是从这三样长出来的Part 24-7控件、Designer、主窗口、对话框能搭出完整的界面骨架Part 38-11Excel / Word / PDF / 重命名自动化办公四件套能立刻省下加班时间Part 412-14SQLite / Model-View / Excel 双向同步能处理有持久化需求的真实业务Part 515-17QChart / 实时面板 / QPainter能做出好看的、实时的、自绘的界面Part 618-20归档 / 去重 / 实时监控文件自动化的完整方法论Part 721-22工程化三件套、打包发布从能跑到能交付但比这 22 个案例更重要的是我在每一讲里反复塞进去的几个原则。它们不依赖 PyQt6换成任何 GUI 框架、任何语言都成立① 耗时操作永远不要放在主线程。第 7 讲给你的TaskThread是通用解药。判断标准很简单这次操作会读写文件/网络吗会就起线程。② 任何批量、破坏性操作都必须能撤销。第 11 讲的两阶段改名、第 18 讲的反向映射、第 19 讲的隔离目录——三讲三种实现同一个思想。一个工具能不能被同事接受往往不取决于它多强而取决于用户敢不敢按那个按钮。③ 业务逻辑和界面彻底分开。你会发现每一讲的xxx_task()函数都不认识 PyQt——它们只接收一个report回调。这样写出来的代码可测试、可复用、可换界面。第 8 讲的 Excel 合并函数换个 CLI 壳子就变成命令行工具一行业务代码都不用改。④ 防御性代码不是多余的 if是对用户的尊重。if dst.exists()不覆盖、except OSError: continue不让一个坏文件搞崩整批、_MEIPASS不往里写——这些看起来啰嗦的代码就是工具和脚本的分界线。⑤ 写测试尤其是给看不见的逻辑写测试。第 20 讲那个新增事件永远不触发的 bug在界面上完全看不出来——不报错、不崩溃、只是感觉不太灵敏。我是靠一个手动驱赶时钟的回归测试把它挖出来的。凡是状态机 定时推进的逻辑界面测试都不靠谱。接下来你可以做什么挑一个自己工作中真正重复的流程用第 811 讲的骨架写一版打包发给同事。这是把 22 讲内容真正变成你自己的东西的唯一办法。回到第 1517 讲把可视化做好看一点。界面颜值是看起来专业的第一印象。回到第 21 讲把那四件事补齐。它们不增加功能但决定你的工具有没有人愿意用。关于这个专栏22 讲里所有的代码都是可运行、已实测的——22 个案例在无显示器环境下用python run_all.py批量跑过全部通过并且每个案例都产出了界面截图在shots/目录。配套工程pyqt6-client/可以直接用 PyCharm 打开运行README.md里有 5 分钟上手说明和案例↔讲次对照表。如果这个专栏帮你省下了加班的几个小时欢迎在 CSDN 上订阅支持。有问题直接在评论区提我会回。—完整下载地址git clone https://gitee.com/wuyikui555/pyqt6-client.git本讲是《PyQt6 客户端编程从入门到实战》专栏的第 22 讲。专栏共 22 讲、22 个可运行案例覆盖布局/控件/信号槽 → 办公自动化 →数据库 → 数据可视化 → 文件自动化 → 工程化与打包发布。