简介PyInstaller 3.2.1是一份面向Python开发者的跨平台打包工具资源旨在解决Python程序在无解释器环境下的分发难题适合需要将脚本封装为独立可执行文件的入门及进阶用户。压缩包内含790个文件约3.01MB以py源码、c/c扩展实现、h头文件、rst文档、spec打包配置及exe可执行程序等类型为主便于从源码与配置层面理解打包机制。已有599人参与学习。资源保留了PyInstaller-3.2.1的完整工程结构涵盖核心C模块如pyi_launch、pyi_archive与构建脚本同时也包含spec示例、图标资源和说明文档读者可据此研究依赖分析、单文件/多文件发布及跨平台构建的具体实现或直接提取工具进行本地打包测试。对于想深入掌握PyInstaller工作原理或定制打包流程的开发者具有不错的参考与实用价值。1. PyInstaller 3.2.1老版本打包器凭什么还值得上手PyInstaller 3.2.1 是 Python 2.7/3.5 时代流行的打包器版本现在你会搜到它多半不是追新而是手里压着一个只能用 Python 3.5 解释器跑的旧工程。它解决的核心问题很具体把脚本连同依赖解释器一起打成可执行文件目标机器不用装 Python。实际场景里很多人是先装新版 PyInstaller 被版本兼容性卡住换回 3.2.1 反而一步到位。它旧却因为默认行为更直白更适合把打包这件事讲清楚。下面从环境对齐讲到交付验证连带把最容易踩的几个坑标好定位。2. 把 3.2.1 用起来环境兼容性核对与最小打包命令2.1 环境对齐3.2.1 能管哪些 Python 版本PyInstaller 3.2.1 那代支持的 Python 是 2.7、3.3、3.4、3.54.0 之后的解释器它不认识。换句话说如果你机器上装的是 Python 3.8 或更高第一优先要做的事不是调参数而是给这个旧工程单独准备一个干净环境。先把当前环境看清python --version pip --version输出确认 Python 版本后再执行安装pip install pyinstaller3.2.13.2.1把版本号锁死避免 pip 因为依赖解析给你装出一个 3.3、4.x 的新版。我一般会顺手pip show pyinstaller看一眼安装路径确认它和你正在用的解释器属于同一个虚拟环境。很多人翻车在“命令能跑但跑的是另一个环境里的 PyInstaller”导致打出来的包和分析结果对不上。注意Python 3.5 太老新版 pip 可能拒绝安装。遇到这种情况先在老环境里用python -m pip install pip19.3.1把 pip 拉回老版本线再去装 PyInstaller 3.2.1。别先把 pip 升到最新否则 pip 自己就会因为解释器太老而罢工。环境对齐这一步没有技巧可言纯粹是版本匹配问题。记住一个原则项目解释器是什么年代打包器就用什么年代跨一个大的 Python 主版本去强行兼容后面所有 hook、隐藏导入都会变成玄学问题。2.2 最小打包命令从脚本到可执行文件的第一次成功先建一个最小脚本专门用来验证 3.2.1 在当前环境能不能正常完成采集、归档、链接三步# hello.py -- 最小验证脚本 import sys import time def main(): print(hello from PyInstaller 3.2.1) time.sleep(2) # 双击运行时别让窗口秒退 input(press enter to exit...) if __name__ __main__: main()time.sleep(2)是给控制台运行留出观察时间input()让窗口停在那边等你按下回车在双击 exe 的场景里这两行能避免“看起来像没反应”的误会。接着打包cd /path/to/hello pip show pyinstaller pyinstaller -F -c -n hello hello.py-F表示打成单个可执行文件-c表示使用控制台窗口-n hello指定产物名字。命令执行完Windows 下会在dist\hello.exe找到产物Linux/macOS 下则是dist/hello没有扩展名。如果这步能跑通说明这个 3.2.1 和你手里的 Python 环境没有版本冲突接下来才有资格讨论数据文件、隐藏导入这些进阶话题。有两个细节值得解释。-F在 3.2.1 里不是默认行为默认是目录模式你显式写-F后面换到别的机器、别的平台行为才会一致。-c在打包 GUI 程序时通常要换成-w但第一次跑一定要保留控制台因为 Python 的报错默认只进控制台窗口关掉你就看不见了。跑完控制台会输出一堆INFO: Analyzing...、INFO: Building EXE...日志看到completed successfully就算过了。此时在命令行里执行dist\hello.exe如果打印出hello from PyInstaller 3.2.1并停在“press enter”基础链路就通了。2.3 生成物拆解spec、build、dist、warn 文件各自是什么一次打包之后工程目录里会多出几样东西先认识它们再往后走文件/目录作用该不该进版本库dist/hello.exe最终交付物不提交产物build/hello/打包中间缓存可整目录删除不提交hello.spec下次打包的配置输入提交作为唯一构建入口warn-hello.txt模块收集警告提交到 CI 日志排查用hello.spec是命令行参数被 PyInstaller 展开后的 Python 脚本。以后改数据文件、加隐藏导入可以直接改 spec 再执行pyinstaller hello.spec比每次敲一长串参数可靠。build/目录是分析过程留下的中间产物删掉不影响结果但它能帮你确认收集了哪些文件遇到“打包后变慢”时可以看它。warn-hello.txt是排查缺少模块的第一现场。打包完成后打开它如果里面出现你代码里明明 import 过却标着Module not found的提示说明这模块是静态扫描漏掉或者被条件导入绕过了后面要按下一章的hidden-import补。这个文件不算强制输出但 3.2.1 默认会生成留着它等于给现场留了黑匣子。从这章开始我的习惯是先跑一次最小命令验证环境再决定要不要上 spec。环境不通时改再多参数都是白费环境通了后面加数据文件、补隐藏导入才有意义。在客户端现场看到报错就乱调参数是大多数人卡住半天的真正原因。3. 打包真实工程必调的三个东西spec、数据文件、动态导入3.1 数据文件怎么带--add-data 的分隔符和运行时路径脚本里读配置文件、图片、模板时开发环境用的是相对路径打包后 exe 的工作目录和文件位置都变了。3.2.1 处理数据文件的办法是--add-data把源文件打进包里运行时由sys._MEIPASS指向解压目录。命令长这样# Linux/macOS 用冒号分隔源路径和目标目录 pyinstaller -F --add-data res/config.ini:res app.py # Windows 路径分隔符是分号整段参数用双引号包住 pyinstaller -F --add-data res\config.ini;res app.py3.2.1 的行为是源路径:目标目录目标目录是相对运行时根目录的。这里的坑在分隔符Linux 和 macOS 路径用冒号Windows 路径用分号不少人在 Windows 上照抄 Linux 写法的结果是res\config.ini:res被解析成C:\...\res\config.ini:res直接报找不到路径。我一般会在pyinstaller命令后面加--log-level WARN专门看它有没有丢掉数据文件。运行时在代码里取这个文件的路径不能写死绝对路径# app.py import os import sys def resource_path(relative): # 打包后 sys._MEIPASS 指向临时解压目录源码运行返回当前目录 base getattr(sys, _MEIPASS, os.path.abspath(.)) return os.path.join(base, relative) cfg_path resource_path(res/config.ini)getattr(sys, _MEIPASS, ...)是关键打包后的解释器环境里有_MEIPASS源码环境没有这一行同时兼容两种运行方式。注意这个函数要写成模块级函数不要在内部又依赖__file__因为 PyInstaller 启动后__file__指向的是临时目录里的解压文件不稳定。数据文件的另一个注意事项是别把只读模板和运行时日志混在一个目录。常见做法是模板、默认配置打进包内日志、用户配置写到系统应用数据目录。否则每次更新程序旧日志会被新包覆盖用户配置也容易被误删。3.2 动态导入怎么补hidden-import 和 hook 文件PyInstaller 通过静态扫描import语句来收集模块但遇到按名字拼接、运行时才确定的模块名它就扫不到。典型场景是插件系统# 运行时按配置选择引擎模块模块名是拼出来的 plugin_name engine_ os.environ.get(ENGINE_MODE, fast) engine __import__(engine. plugin_name, fromlist[*])这类代码在源码里没有任何字面上的engine_fast、engine_standbyPyInstaller 分析阶段根本不知道它们存在。打包后程序一运行就ModuleNotFoundError。解决办法是显式告诉它pyinstaller -F --hidden-importengine_fast --hidden-importengine_standby app.py--hidden-import可以反复出现每个模块名写一次。当模块数量多、还带数据文件时更稳的做法是写 hook。hook 是一个 Python 文件文件名约定为hook-包名.py放在专用目录# hooks/hook-engine.py from PyInstaller.utils.hooks import collect_submodules # 收集 engine 包下所有子模块不用一个个手写 hiddenimports collect_submodules(engine)然后在打包时指定这个目录pyinstaller -F --additional-hooks-dir./hooks app.pycollect_submodules(engine)会递归收集包内全部子模块适合插件成堆的工程它返回一个列表会自动合并到最终模块列表里。这里有个边界collect_submodules只收集模块不收集非.py资源文件资源还得靠--add-data或 hook 里返回datas列表。怎么判断一个模块是不是“藏起来”了最简单的方法是打包之前先把程序用源码方式跑一遍如果源码环境能过、打包后不能过第一反应就是查warn-*.txt里有没有对应模块名。高手和新手的差距也在这里新手一遍一遍重打包老手先看 warn 再补 hidden import。3.3 spec 文件手工调从命令行参数到唯一构建入口命令行参数适合第一次快速验证工程一复杂参数会越来越长、越来越难维护。3.2.1 在第一次打包后自动生成工程名.spec我习惯把它改成唯一构建入口。一个整理过的 spec 像这样# app.spec -- 对应 -F 单文件模式 a Analysis( [app.py], pathex[], binaries[], datas[(res/config.ini, res)], hiddenimports[engine_fast, engine_standby], hookspath[./hooks], runtime_hooks[], excludes[pydoc, test, pytest, unittest], noarchiveFalse, ) pyz PYZ(a.pure) exe EXE( pyz, a.scripts, a.binaries, a.datas, nameapp, debugFalse, stripFalse, upxTrue, consoleTrue, )datas对应命令行里的--add-datahiddenimports对应--hidden-importhookspath指明 hook 目录。excludes里列出确定用不到的模块明确告诉 PyInstaller 不要打包它们noarchiveFalse表示把 Python 字节码打进 PYZ 归档这是默认的压缩方式。这个 spec 的EXE段把binaries和datas直接并进来所以对应的是-F单文件模式如果你打包时用的是默认目录模式生成的 spec 里还会有COLLECT段把依赖收成目录。改完 spec 后再打包就简单了pyinstaller app.spec注意一个行为当输入是 spec 时命令行里的-F、-n这些选项很多会失效真正起作用的是 spec 里的字段。所以不要在敲pyinstaller app.spec -F之后发现没变单文件而困惑去看 spec 里的EXE(...)那段怎么写的。这就是为什么 spec 必须要进版本库——它是整个打包过程的唯一事实来源。另外补一个 spec 时代常见的后悔药如果发现 spec 被改乱不必手工还原回到命令行参数重新生成一份就行。PyInstaller 每次打包都会用命令行参数重建 spec。所以“改乱了就删掉 spec 重新生成”这招在 3.2.1 里照样适用。4. 构建模式与运行行为onefile 与 onedir 到底差在哪4.1 四种构建模式怎么选PyInstaller 3.x 时代构建模式的核心维度到今天也没变-F/--onefile与-D/--onedir决定产物形态-c/--console与-w/--windowed决定是否弹出控制台窗口。两者组合就得到了实际使用中最常见的四种参数组合产物形态适用场景-F -c单文件 控制台命令行工具交给运维直接跑-F -w单文件无控制台GUI 程序对外交付-D -c目录 控制台调试、内部脚本分发-D -w目录无控制台GUI 程序内部部署3.2.1 的默认行为是-D -c也就是生成一个包含 exe 和一堆依赖库的目录运行时从目录里直接加载启动快、安全软件误报率低。很多教程上来就写-F把单文件当作默认这是误解。我一般会建议项目调试阶段用-D -c正式交付时如果客户只接受一个文件再换-F千万别一开始就被“单文件崇拜”带偏。单文件模式看着干净代价在运行时每次启动都要先把归档里的文件解压到系统临时目录程序退出后再清理。文件越多、越大启动越慢某些环境里安全软件监控临时目录还会再拖慢一次。目录模式下这些依赖就在 exe 旁边操作系统直接加载启动速度和调试体验都更接近源码运行。选择的关键不是“单文件看起来更专业”而是交付现场允不允许你带一个目录。只给运维一个dist/app.exe很爽口但前提是你要接受启动慢、误报概率更高的现实。4.2 控制台与窗口模式调试期保留 console 是底线一个 GUI 程序如果打包成-w双击时不再弹出黑框看着很干净。但代价是 Python 的print、traceback也无处可去程序在启动阶段异常你连个报错都看不见只能猜。不少人是靠-w打包后拖进命令行执行用重定向的方式看输出绕一大圈。我的建议很简单开发测试阶段永远用-c只有交付前才切-w。切-w之前先回到源码目录用python app.py跑一遍确认没有打印依赖、没有需要控制台交互的逻辑。如果程序里有input()在-w模式下它不会等到输入而是直接抛EOFError这个行为 3.2.1 也一样属于经典翻车点。如果你必须用-w且需要看报警可以在入口函数包一层日志捕获把未捕获异常写进文件# app.py import traceback def run(): # 你的程序主体 pass if __name__ __main__: try: run() except Exception: with open(app_error.log, w, encodingutf-8) as f: traceback.print_exc(filef) raise这里的关键是with open(app_error.log, w, ...)的路径。它写的是进程当前工作目录不是 exe 所在目录如果程序在任务计划或快捷方式里以其他目录为工作目录启动日志可能跑到你找不到的地方。稳妥做法是把日志路径也交给resource_path()那种基于_MEIPASS或系统应用数据目录的解析而不是裸写文件名。4.3 运行时临时目录sys._MEIPASS 与启动行为的黑匣子-F模式下打包的 exe 启动时bootloader 会把归档里的所有文件释放到临时目录目录名通常以_MEI开头。Python 侧看到的sys._MEIPASS就是那个路径。这段机制在 3.2.1 里是黑匣子你不必完全理解它但必须知道两个事实路径是临时的下次启动可能换程序崩溃时残留文件不会自动清理。验证当前进程用的是哪个临时目录可以这样看import sys import os if hasattr(sys, _MEIPASS): print(unpack dir:, sys._MEIPASS) print(exe path:, os.path.abspath(sys.executable)) else: print(source mode)sys.executable在打包前后也有变化源码模式下指向 python.exe打包后指向你的 app.exe。利用这一点可以判断代码是运行在打包环境还是开发环境比硬编码判断平台更可靠。提示单文件程序的临时目录路径由系统 TEMP 环境变量决定空间不足时程序会在启动阶段就失败表现是“双击没反应”。先查环境变量TEMP、TMP指向的盘符剩余空间往往比看代码跑得快。明白了_MEIPASS的临时性再去理解“为什么打包后配置文件改了不生效”就容易了。很多人把可写配置也打进包内程序每次启动都从临时目录读初始值用户改的配置写在别处看起来就像程序“不受控”。正确做法是静态资源进包运行时可变数据走外部目录。5. PyInstaller 3.2.1 高频坑位与排查手册5.1 启动即闪退窗口还没看清就没了现象双击 exe控制台窗口一闪而过程序直接消失。原因打包后的异常信息打印在控制台里窗口关闭就看不到了。在-w模式控制台都没有更是无从下手。解决第一件事是改回-c重新打包再到命令行里执行dist\app.exe让异常信息留在窗口里。如果还是闪退给入口函数加全局异常捕获并写入文件按上一章的代码改。要注意traceback.print_exc(filef)写的是当前工作目录抓到日志后先确认它的落盘位置别一上来就怀疑代码逻辑。这个坑里最容易被忽视的是“打包机正常、目标机闪退”。原因往往是目标机缺 Visual C 运行库3.2.1 时代的产物依赖 VC 2015 Redistributable目标机没装就可能在启动阶段直接退。这种闪退连异常文件都来不及写因为 Python 环境还没完全启动。区分方法是看报错弹窗缺 DLL 的报错来自系统加载器缺模块的报错来自 Python两者文字不同。5.2 打包后提示找不到模块源码里明明有 import现象开发环境python app.py一切正常打包后的 exe 运行时报ModuleNotFoundError: No module named xxx。原因静态分析只能识别显式的import xxx语句__import__、importlib.import_module、模块名拼接、藏在try/except里的导入都可能被漏掉。解决先打开warn-app.txt搜模块名确认它被标成 missing然后补--hidden-importxxx或者为整个包写 hook 按第 3 章的方式收集。补完重新打包再看 warn 文件确认不再出现。这个坑里还有一类特殊场景你的代码里 import 了某个库但这个库自身又用 C 扩展加载子模块PyInstaller 收集不到 C 扩展里的依赖表现同样是不停报“找不到模块”。处理方式是逐个补--hidden-import没有更快的捷径。模块收集本质上是个递归猜解的过程warn 文件就是你调解步骤的地图。5.3 目录模式拷到别的机器依然崩缺运行库和系统组件现象同一份dist目录开发机运行良好拷到一台干净的目标机上启动报缺少VCRUNTIME140.dll类的错误。原因目录模式虽然把所有 Python 依赖带上了但 VC 运行库属于系统组件PyInstaller 不负责替你安装目标机缺它exe 在走完启动流程之前就挂了。解决在目标机安装对应版本的 VC 运行库再试。如果交付现场不允许装系统组件就换成-F单文件试试——单文件模式有时能把需要的运行时 DLL 一并收进来但并不能保证 100% 收齐所以要在干净环境里验证。用 PE 查看工具检查 exe 的 DLL 依赖列表能精确看出缺的是什么比瞎试快得多。这类问题的排查顺序应该是先看系统事件日志里的错误模块名区分是缺 DLL 还是缺 Python 模块再在命令行下运行 exe 拿完整报错最后才动打包参数。跳步乱试是大多数人在这里耗费一下午的原因。5.4 安全软件把打包产物当风险文件隔离现象exe 刚生成安全软件就提示检测到风险直接隔离或删除客户现场也复现。原因onefile 自解压、UPX 加壳、无数字签名的程序都容易触发启发式规则。PyInstaller 本身没问题但“自解压 修改临时目录 网络请求”这些行为叠加在一起恰好命中安全软件的敏感模式。解决先关掉 UPX 试一次用upxFalse或命令行去掉--upx如果还误报把交付形态从-F换成-D目录模式的启动行为更接近普通程序误报率明显低。有条件的话正式对外分发用代码签名证书做签名能显著降低主流安全软件的拦截率但签名解决不了所有问题要结合前三步一起处理。误报现场最容易翻车的一点开发机用某款安全软件不报客户那款报于是回来反复调参数。实际上应该第一时间拿到客户安全软件的确切检测名按特征词判断命中原因是“自解压”“加壳”还是“可疑行为”再针对性处理。没有检测名就改参数等于盲调。5.5 体积异常大或启动慢排除模块与构建模式之间的平衡现象一个打印 “hello world” 的程序用-F打出很大体积的 exe运行时首启还慢一个很大的工程反而打出了更夸张的体积。原因PyInstaller 会把 import 到的库整包收集很多库连带收集了大批用不到的子模块onefile 的启动慢则是每次都要解压归档。解决针对不必要的库执行排除pyinstaller -F \ --exclude-moduletkinter \ --exclude-modulepydoc \ --exclude-modulematplotlib \ --exclude-modulepytest \ app.py--exclude-module的意思是“即使扫描到了也不打包”适合确定用不到的测试框架和可视化库。体积降下来之后启动速度自然会好一点但如果还在追求单文件的低调启动就得考虑换-D模式。很多项目最终选择“对外给单文件、内部部署用目录”两头都占。在 3.2.1 上UPX 压缩也能明显缩小体积但要注意UPX 压缩某些库之后运行时会因为解压加载不兼容而报错。遇到莫名的内存错误或 DLL 加载失败先怀疑 UPX用upxFalse重打一次对比。压缩不是免费午餐它是在体积和稳定性之间做取舍。6. 交付前的最后一步验证清单与体积瘦身技巧6.1 在干净环境验证产物的最小检查单打包机跑通不算完成交付现场的行为才是真实行为。我习惯在交付前用一台没有 Python、没有开发工具的干净测试机跑一份检查单检查项操作期望结果启动双击 exe正常出现界面或控制台输出无弹错依赖命令行运行并查看退出码退出码 0或按业务正常退出数据文件进入需要数据文件的功能页配置、图片、模板能读无中文乱码写盘执行业务预期会写日志/配置的操作文件写到了外部目录没有落在临时目录清理反复启动/退出 5 次临时目录没有残留_MEI开头的文件堆积如果有自动化条件把这 5 项写成一个脚本放到目标机跑一遍比人工点十次靠谱。没有条件的话至少把“启动”“写盘”两项做成必检这两个是翻车率最高的。6.2 让 3.2.1 的产物跑得更稳的两个收尾技巧第一个技巧能目录交付就不要单文件交付。目录模式启动快、临时目录不用解压、误报率低代价只是交付物从一个文件变成一个文件夹。对内部工具这个代价几乎可以忽略。第二个技巧把容易变化的数据从包里拿出来。版本更新时依赖库和代码逻辑变了才需要重新打包配置文件、模板如果外置运维可以直接替换而不动 exe改动范围小、风险低。我现在处理遗留项目的习惯是先花十分钟把warn文件过一遍再决定用哪种模式交付包里只放代码和静态资源运行时数据一律外置。这个习惯让我少踩了无数次“打包一时爽现场火葬场”的坑。希望帮到你。本文还有配套的精品资源点击获取