简介:PyInstaller(早期包名 pyinstall)0.1.4 源码安装包,面向需要将 Python 脚本转换为独立可执行文件的开发者,尤其适合研究打包工具实现、验证免解释器分发流程的学习者。压缩包共19个文件,约37KB,以txt说明与测试记录、py源码、pkg-info元数据及cfg配置为主,另含文档生成相关脚本;体量轻小,目录围绕源码、测试、文档与包元信息组织。已有589人学习,说明其可作为了解早期 PyInstaller 工作方式的入口。包内提供安装入口、核心打包脚本、基础测试用例及文档配置,读者可据此观察依赖分析、脚本冻结与可执行文件生成的基本思路,并参考测试用例理解打包前后的验证方法,适合作为源码阅读、实验复现和旧版本工具研究的辅助材料。
1. 打包闪退这件事:pyinstaller 安装包到底该装在哪个环境里
某开发者把一个图像处理 Demo 用 pyinstaller 打成了安装包,自己的机器上双击秒开,发到同事电脑上却是黑框一闪就没了;重打一次,换台机器又好了。这种时好时坏的情况,八成不是代码写错,而是打包环境本身没弄干净——解释器位数、虚拟环境、依赖清单、资源路径,任何一环漏掉,产出的 pyinstaller 安装包就只是半成品。它适合所有要把 Python 脚本交到没有 Python 的机器上跑的人:写内部工具的、交付小工具给客户的、做课设要交 exe 的。装不好会怎么样?最轻是体积翻三倍,最重是你在自己机器上永远复现不出别人的报错。
2. 装前把版本定死:Python 3.10 与 PyInstaller 6.x 的搭配清单
打包这件事的玄学程度,和版本组合的混乱程度成正比。同一份代码,Python 3.8 打出来的包在 Windows 7 上能跑,Python 3.12 打出来的直接提示缺少 api-ms-win 系列 DLL;PyInstaller 5.x 和 6.x 对--add-data的分隔符处理也不一样。所以第一步不是敲pip install,而是把「用哪个解释器、装在哪个环境、锁哪个版本」这三件事先写进一张纸。
2.1 解释器怎么选:位数、版本与系统兼容下限
PyInstaller 打包出来的程序,架构跟打包时用的 Python 解释器完全一致,它不做跨架构转换。用 32 位 Python 打包,产物就是 32 位 exe,在 64 位系统上能跑但吃不到大内存;用 64 位打包,产物拿到 32 位系统上直接打不开。所以打包机的解释器架构,必须和交付目标机器的架构对齐。
版本上,PyInstaller 6.x 官方支持 Python 3.8 到 3.13 区间,但「支持」和「稳」是两回事。我一般会选 Python 3.10 或 3.11 这个区间:新语法够用,第三方库的预编译 wheel 覆盖最全,遇到numpy、pandas、PyQt这类带 C 扩展的依赖时,不至于卡在源码编译上。如果交付目标里还有 Windows 7,那 Python 版本要压到 3.8,再高就缺系统 API。
打包前先把解释器的底细摸清楚,不要凭记忆:
# 确认解释器版本、位数、编译模式,这三个值决定产物能跑在哪 python -V python -c "import platform, sys; print(platform.architecture()[0], platform.machine(), sys.maxsize > 2**32)"第一行给出主版本号,第二行输出类似64bit AMD64 True,最后那个布尔值True表示这是 64 位解释器。如果输出是32bit,而你要交付的机器都是 64 位,那就该换解释器了,别指望 PyInstaller 帮你补。
提示:用 Microsoft Store 装的 Python 打包经常出怪问题,安装目录被放在
WindowsApps下,权限和路径都很别扭。常见做法是去官网下安装包,勾选「Add to PATH」,装到C:\Python310这类干净路径。
2.2 venv 建在哪:给每个打包项目一个独立环境
全局环境里装了几百个包,PyInstaller 分析依赖时会把能扫到的都扫进去,产物体积能翻几倍,还会莫名其妙带进冲突的 DLL。最省事的做法是一个打包项目一个虚拟环境,环境目录就放在项目根目录下的.venv,跟着项目一起走。
# 在项目根目录建独立环境,避免全局包污染打包结果 python -m venv .venv # Windows 激活 .venv\Scripts\activate # macOS / Linux 激活 source .venv/bin/activate # 升级基础工具链,老版本 pip 装 wheel 容易失败 python -m pip install -U pip wheel setuptoolspython -m venv的第一个参数是环境目录名,用.venv是惯例,编辑器一般会自动识别。激活成功后命令行前面会出现(.venv),这一步很关键——后面所有pip install和pyinstaller命令都必须在这个前缀存在的前提下执行,否则装到全局去了,打包时用的还是那个脏环境。
有个细节容易翻车:虚拟环境不要建在中文路径或带空格的目录下,比如D:\我的项目\打包试验。PyInstaller 在某些版本里对这类路径的处理不稳,生成的 spec 文件里路径会带引号导致解析失败。我一般把项目放在D:\work\demo-app这种纯英文短路径下,能省掉一半莫名其妙的报错。
2.3 装 PyInstaller 与冻结依赖:三条命令的顺序
装 PyInstaller 本身只有一行,但真正决定成败的是「先装项目依赖,再装 PyInstaller」。顺序反了,PyInstaller 早期版本的 hook 机制可能在分析时漏掉后装的包。
# 第一步:先装项目自身依赖 pip install -r requirements.txt # 第二步:装打包工具,锁大版本避免 CI 上次次装出不同结果 pip install "pyinstaller>=6.0,<7.0" # 第三步:把当前环境的完整依赖冻结下来,供打包机和交付验证复用 pip freeze > requirements-lock.txt # 确认装上了,并看它落在哪个环境里 pyinstaller --version python -c "import PyInstaller, sys; print(PyInstaller.__file__, sys.prefix)""pyinstaller>=6.0,<7.0"这种写法锁的是大版本区间,允许 6.x 的小版本升级修 bug,但不会哪天突然跳到 7.x 改掉参数语义。真正要求可复现的场景,直接写死pyinstaller==6.x.y,具体小版本以你本地pip index versions pyinstaller查到的为准。
最后一行输出里的sys.prefix是判断环境是否干净的照妖镜:如果它指向C:\Python310而不是项目下的.venv,说明虚拟环境根本没生效,前面所有隔离工作白做。
| 检查项 | 期望值 | 不对时的处理 |
|---|---|---|
python -V | 3.8 至 3.13 | 换解释器,别硬撑 |
| 架构 | 与交付机一致 | 重装对应位数的 Python |
sys.prefix | 项目内.venv路径 | 重新激活虚拟环境 |
pyinstaller --version | 6.x | 检查是否装到了全局 |
这张表我每次换机器都会对一遍,三条不对就别往下走,后面全是白费功夫。
3. 第一轮打包:命令行参数与 spec 文件的分工
装好之后第一件事不是追求完美产物,而是先打出一个能跑起来的最小包,确认整条链路通了,再往上叠资源、叠图标、叠压缩。很多人一上手就把所有参数写全,结果报错时根本分不清是哪一项引起的。
3.1 最小可用命令与六个必调参数
最小可用命令其实只有pyinstaller main.py,但它会生成一个目录、带控制台窗口、用默认名字。实际交付时至少要调六项:
# 最常用的一版打包命令,参数逐项说明见下文 pyinstaller main.py \ --name demo-tool \ --onefile \ --console \ --icon assets/app.ico \ --clean \ --noconfirm--name决定产物名和 spec 文件名,别用中文,某些解压工具处理中文文件名会乱码。--onefile打成单文件,--console保留控制台窗口——第一次打包一定保留它,报错信息全靠这个黑框输出。--icon指定图标,只接受.ico格式,PNG 要转换。--clean清掉上次构建缓存的build/目录,避免旧对象文件混进新包。--noconfirm跳过覆盖确认,脚本化构建必备。
如果你要的是带界面的工具,把--console换成--windowed(等价于-w)。但请记住这条血泪经验:先用--console把功能跑通,最后一步再换--windowed。反过来的话,程序一崩就是静默退出,你连异常栈都看不到。
构建完成后目录结构是固定的:
project/ ├── build/ # 中间产物,可随时删 ├── dist/ # 最终产物在这里 │ └── demo-tool.exe ├── demo-tool.spec # 自动生成的构建描述文件 └── main.pybuild/是黑匣子,出问题时可以删掉重建;dist/才是要交付的东西;.spec是下一次构建的入口,改完它就不用再敲一长串参数了。
3.2 onefile 与 onedir 的取舍:启动速度、体积、误报率
这是打包里最需要提前决策的一项,因为改起来要重新构建。
--onefile把所有依赖塞进一个 exe,交付时只有一个文件,很讨喜。代价是运行时它要把内容解压到系统临时目录再执行,每次启动都多几百毫秒到几秒。程序稍微大一点(带 Qt、带 numpy),冷启动三秒以上很常见,用户会以为卡死了。
--onedir生成一个目录,exe 和_internal依赖目录并列。启动快,杀毒软件误报率低,缺点是交付时要打包整个目录,用户可能只拷走 exe 导致跑不起来。
| 维度 | onefile | onedir |
|---|---|---|
| 交付形态 | 单个 exe | exe + 依赖目录 |
| 冷启动 | 慢,需解压到临时目录 | 快,直接加载 |
| 体积 | 略小(压缩过) | 略大 |
| 杀毒误报 | 偏高 | 偏低 |
| 适合场景 | 小工具、临时分发 | 正式交付、体积大、频繁启动 |
我的取舍习惯是:交付给非技术用户、单次运行的批处理工具走--onefile;带 GUI、会被反复打开、依赖较重的走--onedir,最后用安装包工具打成安装程序。别为了「只有一个文件」这点体面,换来每次启动三秒的等待。
3.3 spec 文件:从自动生成到手工维护
第一次构建后生成的.spec文件,本质是一段 Python 脚本,描述「分析哪些文件、收集哪些依赖、怎么打包」。命令行参数只是它的快捷写法,一旦你要加资源、加隐藏导入、排排除模块,就得直接改 spec。
# demo-tool.spec 关键片段,改完用 pyinstaller demo-tool.spec 构建 a = Analysis( ['main.py'], # 入口脚本,多入口可写成列表 pathex=[], # 额外搜索路径,通常留空 binaries=[], # 需要强制带入的二进制文件 datas=[('assets', 'assets')], # 源路径 -> 包内目标路径 hiddenimports=['pkg_resources'],# 动态导入、分析不到的模块 hookspath=['hooks'], # 自定义 hook 目录 runtime_hooks=[], excludes=['tkinter', 'matplotlib', 'tests'], # 明确排除,压体积 noarchive=False, ) pyz = PYZ(a.pure) exe = EXE( pyz, a.scripts, a.binaries, a.datas, name='demo-tool', console=True, # 对应 --console / --windowed icon='assets/app.ico', upx=False, # 是否启用 UPX 压缩 )datas里每一项是二元组,左边是构建机上的相对路径,右边是产物包内的目标路径——右边这个值必须和你代码里读取资源的路径完全一致,这是新手最容易写错的地方。excludes是体积优化的主力,明确写出来的模块不会被分析,像tkinter这种你没用但某些库偷偷 import 的,排除掉能省几十兆。upx=False建议保持关闭,UPX 压缩会显著提高被杀毒软件误判的概率,省下的那点体积不值得。
改完 spec 后,构建命令变成pyinstaller demo-tool.spec,此时命令行上的--onefile、--icon等参数会被忽略,一切以 spec 为准。这一点不搞清楚,就会出现「我明明加了 --add-data 却不生效」的迷惑现场。
4. 补依赖与补资源:让 exe 在别人的机器上活下来
在自己机器上跑通只完成了一半。PyInstaller 的依赖分析是基于静态扫描的,它看不到importlib.import_module('xxx')、__import__(name)、插件式加载这些动态行为,也看不到你代码里open('config.json')读的那个文件。这两类东西必须手工补。
4.1 数据文件、图标与 add-data 的路径写法
需要跟着程序一起走的非代码文件,统称数据文件:配置模板、字体、模型权重、前端静态资源。命令行写法是--add-data,注意分隔符在不同版本和平台上不统一。
# 老版本用分号,6.x 推荐用冒号;Windows 下两种都兼容 pyinstaller main.py --add-data "assets:assets" # 多个资源重复该参数 pyinstaller main.py --add-data "assets:assets" --add-data "conf:conf" # 整个目录带进去时不要写成 assets/*,会丢失子目录结构写成二元组形式后,产物内部就有一个assets/目录,和你在项目里的结构一致。这比对每个文件单独写一行要好维护得多。如果想连目录带子目录一次带全,PyInstaller 6.x 还提供了--collect-data 包名,用来收集某个已安装包的附属数据文件,比如某些库自带的.json模板。
注意:
--add-data的源路径是相对执行命令时的工作目录,不是相对 spec 文件。在build/子目录里执行构建脚本时,路径全部要重算。我习惯在项目根目录执行一切构建命令。
4.2 hiddenimports 与第三方库 hook 的处理
报ModuleNotFoundError: No module named 'xxx'是打包后最高频的错误,而xxx往往是你requirements.txt里明明装了的包。原因是它被动态导入了,静态分析没扫到。
# 在 spec 的 Analysis 里补隐藏导入 hiddenimports=[ 'pkg_resources', # setuptools 相关,动态加载常被漏 'charset_normalizer', # requests 的编码探测依赖 'sqlalchemy.sql.default_comparator', # 某些 ORM 的隐式子模块 ],补的时候别瞎猜,用运行日志定位:先打--console版,跑到报错,把模块名原样抄进hiddenimports。补完重新构建再看,往往要迭代两三轮才干净。
对于知名第三方库,社区已经写好了 hook,PyInstaller 装完自带一批,位于安装目录的PyInstaller/hooks/下。如果你用的库比较小众,可以自己写一个 hook 文件放进项目的hooks/目录,再在 spec 里用hookspath指过去:
# hooks/hook-mylib.py,文件名必须是 hook-包名.py from PyInstaller.utils.hooks import collect_submodules, collect_data_files hiddenimports = collect_submodules('mylib') # 把这个包的所有子模块都带上 datas = collect_data_files('mylib') # 连同它的数据文件collect_submodules会递归导入包内所有子模块,适合插件式架构;collect_data_files处理库自带的配置、模板。代价是可能带进一堆用不上的东西,体积会涨,所以要配合excludes一起用。
4.3 运行时路径:sys._MEIPASS 与资源定位
这是打包后最隐蔽的坑:代码里写open('assets/app.ico'),开发时正常,打包后报文件不存在。因为打包后进程的当前工作目录变了,onefile 模式下资源还被解压到了系统临时目录。
import sys from pathlib import Path def resource_path(rel: str) -> Path: """兼容源码运行与打包运行两种形态的资源定位""" # 打包后 PyInstaller 注入 _MEIPASS,指向解压出的临时目录 base = Path(getattr(sys, '_MEIPASS', Path(__file__).resolve().parent)) return base / rel # 用法:读取图标、配置、模型文件都走这个函数 icon_file = resource_path('assets/app.ico') print(icon_file.exists())getattr(sys, '_MEIPASS', ...)的意思是:如果打包环境注入了_MEIPASS就用它,否则回退到当前文件所在目录。这样同一份代码在源码运行和打包运行下都能找到资源。
另一类路径问题是读写目录。程序要写日志、写缓存、写用户配置,这些绝对不能写到_MEIPASS里,那是临时目录,程序退出就没了。写文件统一走用户目录:
import os from pathlib import Path # 用户级可写目录,Windows 落在 AppData\Roaming 下 data_dir = Path(os.getenv('APPDATA') or Path.home() / '.config') / 'demo-tool' data_dir.mkdir(parents=True, exist_ok=True) log_file = data_dir / 'app.log'读资源用_MEIPASS,写数据用用户目录,这两条分开记,能避开打包后 80% 的「文件找不到」和「配置不保存」。
5. 打包避坑排查:五类翻车现场与救法
下面这五条,是我在交付十几个打包项目后攒下来的高频故障。每一条都按「现象、原因、解决」写,遇到了直接对号入座。
5.1 双击闪退,控制台什么都没有
现象:exe 双击后窗口一闪就消失,没有任何提示,任务管理器里也看不到进程残留。
原因:构建时用了--windowed,Python 的异常栈被丢弃了,而启动阶段就抛了异常,比如缺 DLL、缺模块、配置文件读不到。
解决:先用--console重新构建一版,在命令行里执行 exe,异常栈会完整打出来。定位并修完之后,再切回--windowed。如果不想改构建参数,也可以临时在入口脚本最外层套一层文件日志:
# 入口最外层兜底,把未捕获异常写进用户目录 import traceback, sys from pathlib import Path log = Path.home() / 'demo-tool-crash.log' try: import main_app main_app.run() except Exception: log.write_text(traceback.format_exc(), encoding='utf-8') raise这样即便没有控制台,也能拿到demo-tool-crash.log里的完整栈。
5.2 报 ModuleNotFoundError,但包明明装了
现象:打包成功,运行时报No module named 'xxx',回到项目里pip show xxx又能查到。
原因:模块被动态导入,静态分析没扫到;或者打包机的环境里装了这个包,但打包时被excludes误排除。
解决:把模块名加进 spec 的hiddenimports;同时检查excludes列表里有没有误伤。如果整个包的子模块都要,用--collect-submodules 包名一次带全。改完清build/重建,别用缓存。
5.3 杀毒软件报毒,exe 直接被隔离
现象:产物拷到目标机器上,还没运行就被杀毒软件删掉,或者提示「疑似木马」。
原因:onefile 模式的行为特征——运行时把自己解压到临时目录再执行——和某些恶意软件的自解压手法相似,启发式引擎会报毒。程序没有数字签名时概率更高。
解决:优先换--onedir,触发率明显下降;别用 UPX 压缩,压缩过的二进制更容易被误判;有条件的给 exe 加代码签名;实在不行走杀毒厂商的误报提交通道。这是概率问题,不是「改个参数就彻底消失」。
5.4 体积暴涨到两三百兆
现象:一个几百行的脚本,打出来的包两百多兆。
原因:打包机的环境里装了大量无关包,分析时全扫了进去;或者 conda 环境的依赖被整个拖进来。
解决:在干净的 venv 里打包,只装项目实际依赖;用excludes排除matplotlib、tkinter、pandas这类用不到的大块头;用pyinstaller --log-level=DEBUG看分析日志里谁在贡献体积,Ctrl+F 找numpy、scipy之类的意外出现。
# 看看产物目录里谁最占地,按大小倒序 du -sh dist/demo-tool/* | sort -rh | head -205.5 中文路径或编码导致的启动失败
现象:在某些机器上启动报编码错误,或者读配置文件时抛UnicodeDecodeError。
原因:Windows 控制台默认 GBK,代码里open()没指定encoding时按系统编码走;产物路径里有中文时,某些旧版本的解压逻辑也会出问题。
解决:所有文件读写显式指定encoding='utf-8';构建目录和产物名全部用英文;控制台输出前先设PYTHONIOENCODING=utf-8或用--console观察实际报错。这条在交付给中文用户名机器时特别容易踩。
6. 装完怎么验证:三层验证法确认产物真能跑
打包成功不等于能交付。我见过太多次「在我机器上好好的」,所以出一版产物后,我会强制走三层验证,每层目标不同,缺一层就可能把问题带到用户面前。
第一层,脱离开发环境验证。把dist/目录拷到一个干净的临时目录,关掉编辑器,清掉PYTHONPATH,命令行直接执行:
# Windows:临时清掉环境变量,模拟一台没装 Python 的机器 set PYTHONPATH= cd D:\tmp\smoke-test .\demo-tool\demo-tool.exe --version # macOS / Linux:用 env -i 起一个近乎空的环境 env -i HOME=$HOME ./demo-tool/demo-tool --version目的很单纯:确认程序没有偷偷依赖开发机上的某个绝对路径。如果这一步就报错,后面的验证都不用做。
第二层,干净机器验证。找一台没装过 Python 的 Windows 虚拟机,或者交付目标同版本的系统环境,把产物拷进去跑主流程。重点不是看能不能启动,而是看三个具体动作:读一个资源文件(图标、模板)、写一个文件(日志、导出)、调用一次网络或数据库。这三样分别对应资源路径、写权限、外部依赖,是打包产物在陌生环境里最容易翻车的地方。
第三层,回归清单。交付前对着表过一遍,比凭感觉靠谱得多:
| 检查项 | 判定标准 | 说明 |
|---|---|---|
| 架构一致 | 产物位数与目标机一致 | 32 位产物在 64 位系统能跑,反之不行 |
| 冷启动耗时 | 单文件包 3 秒内 | 超了考虑改 onedir |
| 主流程可跑通 | 核心功能完整走一遍 | 别只测启动 |
| 资源加载 | 图标、模板正常显示 | 验证_MEIPASS逻辑 |
| 写入权限 | 日志、导出成功 | 验证用户目录写法 |
| 签名与体积 | 符合交付要求 | 体积异常查 excludes |
| 报错可追溯 | 崩溃有日志可看 | 验证兜底日志 |
第三层我一般写成一段 PowerShell 或 shell 脚本,把能自动化的项跑掉,跑完输出 PASS/FAIL 列表,附在交付邮件里。这样下次谁改了什么导致回归,一跑就知道。
三层验证听起来麻烦,实际跑下来不到十分钟。真正费时间的是被用户退回来之后,在一台你够不着的机器上远程猜问题——那才是真的血泪。
从那以后,我每次出包前都强制把这三层走一遍,哪怕这次只改了一行日志代码,因为翻车往往就发生在「这次应该没事」的那一版上。希望帮到你。
本文还有配套的精品资源,点击获取