PyInstaller 3.5 打包 Python 脚本实战:兼容旧系统与避坑指南

发布时间:2026/10/15 8:17:49
PyInstaller 3.5 打包 Python 脚本实战:兼容旧系统与避坑指南
简介PyInstaller v3.5 源码包面向需要将 Python 程序打包为独立可执行文件的开发者尤其适合希望摆脱目标机器 Python 环境依赖、简化分发部署流程的中高级使用者。压缩包共 1307 个文件约 3.93MB以 873 个 py 源码文件为主体辅以 txt、rst 文档、toc 目录、h 与 c 底层源码、spec 打包配置、ico/icns 图标资源及 exe、egg、dll、so、pyd 等二进制与依赖文件覆盖从核心打包逻辑到跨平台运行库的完整结构。资源内含 setup.py 安装脚本与 PyInstaller 主程序可支持单文件、多文件打包自动处理动态链接库并允许自定义启动图标与隐藏脚本兼容 Windows、macOS 与 Linux。目前已有 2235 人学习下载适合需要研究打包机制、排查依赖缺失或构建独立发布版本的开发者参考使用。1. 为什么我还在用 PyInstaller 3.5 打包 Python 脚本上周帮一个做工业数据采集的朋友处理一个老项目环境是 Windows 7 加 Python 3.6现场机器不让装 Python只能给一个 exe。他试了新版 PyInstaller打包出来的程序在目标机上直接报api-ms-win-crt-runtime-l1-1-0.dll缺失换了几台机器都一样。我让他退回 PyInstaller 3.5 重新打问题消失。这不是玄学是 3.5 对老系统的 CRT 依赖处理得更保守。PyInstaller 3.5 是 2019 年发布的版本支持 Python 2.7 到 3.7能把 Python 脚本连同解释器、依赖库一起塞进单个可执行文件。它解决的核心问题就一个目标机器没有 Python 环境、没有 pip、没有网络你也能把程序跑起来。适合谁做桌面小工具交付的、给工厂产线写辅助脚本的、需要把内部工具发给非技术同事的。如果你在维护一个跑在旧 Windows 上的 Python 项目或者你的依赖里有大量 C 扩展3.5 的兼容性反而比新版更稳。这份资源就是 PyInstaller 3.5 的完整源码包解压后可以直接python setup.py install也可以改源码里的 spec 模板来定制打包行为。2. PyInstaller 3.5 的打包机制与 spec 文件结构2.1 从脚本到 exe分析、打包、引导三个阶段PyInstaller 的工作流程分三步。第一步是分析它从入口脚本开始递归扫描所有import找到对应的.py文件和 C 扩展.pyd/.so同时解析出模块之间的依赖关系。第二步是打包把分析结果写进一个 PKG 归档里面包含字节码、二进制文件和资源。第三步是生成引导程序Windows 下是一个小的 exe 加载器运行时把 PKG 解压到临时目录设置sys.path然后执行入口脚本。关键点在于PyInstaller 不是把 Python 编译成机器码它只是把解释器和你的代码捆在一起。所以打包后的体积至少 8MB 起步因为python36.dll和标准库就在里面。3.5 版本用的是bootloader的 C 代码编译出的引导程序这个引导程序负责在运行时定位解压目录。如果你在 spec 里改了runtime_tmpdir引导程序就会把文件解到指定位置而不是默认的%TEMP%。2.2 spec 文件里四个必须改的字段spec 文件是 PyInstaller 的核心配置文件第一次运行pyinstaller your_script.py时会自动生成。很多人直接删掉 spec 重新生成但真正要定制打包行为必须手改 spec。下面是一个典型的 spec 结构我加了注释说明每个字段的作用。# my_tool.spec # -*- mode: python ; coding: utf-8 -*- block_cipher None # 3.5 支持字节码加密但需要单独安装 tinyaes a Analysis( [main.py], # 入口脚本可以写多个 pathex[D:\\projects\\mytool], # 搜索路径解决 import 找不到模块 binaries[], # 手动添加的二进制文件如 .dll datas[(config/*.json, config)], # 资源文件元组是(源路径, 目标目录) hiddenimports[pkg_resources.py2_warn], # 动态导入的模块必须写这里 hookspath[], # 自定义 hook 目录 runtime_hooks[], excludes[tkinter, matplotlib], # 排除不需要的大库减小体积 win_no_prefer_redirectsFalse, win_private_assembliesFalse, cipherblock_cipher, noarchiveFalse ) pyz PYZ(a.pure, a.zipped_data, cipherblock_cipher) exe EXE( pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [], nameMyTool, # 生成的 exe 名称 debugFalse, # 调试模式会输出大量日志 bootloader_ignore_signalsFalse, stripFalse, # 是否剥离符号表Windows 下一般 False upxTrue, # 是否用 UPX 压缩后面会讲坑 upx_exclude[], runtime_tmpdirNone, # None 表示用系统临时目录 consoleTrue, # True 是控制台程序False 是窗口程序 iconapp.ico # 图标文件路径 )Analysis里的datas字段最容易翻车。比如你的程序要读一个config.json代码里写的是open(config.json)打包后 exe 运行时工作目录可能不是 exe 所在目录而是临时解压目录。正确做法是用sys._MEIPASS拼接路径或者在 spec 里把资源文件放到datas并配合--add-data的运行时逻辑。hiddenimports是第二个高频坑任何用__import__()、importlib.import_module()动态加载的模块PyInstaller 静态分析都扫不到必须手动列进去。2.3 单文件模式与单目录模式的取舍pyinstaller -F main.py生成单个 exepyinstaller -D main.py生成一个目录。单文件模式启动慢因为每次运行都要把内容解压到临时目录大项目可能要 3 到 5 秒。单目录模式启动快但交付时得打包整个文件夹。我一般这样选给内部同事用、追求启动速度用-D给外部客户、要求一个文件搞定用-F。3.5 版本在单文件模式下对临时目录的清理不如新版积极如果程序异常退出%TEMP%里会残留_MEIxxxxx文件夹时间长了占空间。可以在入口脚本里加一段清理逻辑或者用runtime_tmpdir指定一个固定目录自己管理。3. 从源码包到可执行文件完整操作流程3.1 安装 3.5 版本并验证环境拿到pyinstaller-pyinstaller-v3.5.zip后先解压到一个不带中文和空格的路径比如D:\tools\pyinstaller-3.5。然后确认你的 Python 版本在 2.7 到 3.7 之间。3.5 不支持 Python 3.8 及以上因为 3.8 改了字节码格式和sys模块的一些内部结构强行装会报AttributeError: module sys has no attribute setdefaultencoding之类的错。# 进入解压目录 cd D:\tools\pyinstaller-3.5 # 用当前 Python 安装注意不要用 pip install pyinstaller那样会装最新版 python setup.py install # 验证版本输出应该是 3.5 pyinstaller --version如果pyinstaller --version输出不是 3.5说明系统里已经有其他版本用pip uninstall pyinstaller先卸掉再重新执行python setup.py install。安装完成后PyInstaller命令会被放到 Python 的Scripts目录下确保这个目录在PATH里。3.2 用命令行参数快速打包一个 Demo先写一个最简单的脚本验证流程。新建demo.py内容如下# demo.py import sys import os def resource_path(relative_path): 兼容 PyInstaller 打包后的资源路径获取 if hasattr(sys, _MEIPASS): # 打包后资源被解压到 sys._MEIPASS 指向的临时目录 base_path sys._MEIPASS else: # 正常运行时用脚本所在目录 base_path os.path.abspath(.) return os.path.join(base_path, relative_path) if __name__ __main__: print(PyInstaller 3.5 demo running) print(Python version:, sys.version) # 假设有一个 data.txt 要读 data_file resource_path(data.txt) if os.path.exists(data_file): with open(data_file, r, encodingutf-8) as f: print(data.txt content:, f.read().strip()) else: print(data.txt not found, but program still runs) input(Press Enter to exit...)然后在同目录建一个data.txt随便写一行字。打包命令# -F 单文件-c 控制台--add-data 把 data.txt 加进去 # Windows 下源路径和目标路径用分号隔开Linux/macOS 用冒号 pyinstaller -F -c --add-data data.txt;. demo.py执行完后dist目录下会出现demo.exe。把它复制到另一个没有 Python 的目录双击运行应该能看到输出和data.txt的内容。这里的关键是--add-data的格式源路径;目标路径目标路径.表示放到解压目录的根下。代码里用sys._MEIPASS拼接才能正确找到文件。3.3 用 spec 文件做精细化打包命令行参数适合快速验证真正交付要用 spec。先生成 specpyinstaller -F demo.py然后编辑demo.spec把datas改成[(data.txt, .)]把console改成False就是窗口程序。改完后用pyinstaller demo.spec重新打包。注意一旦用了 spec 文件命令行里的-F、--add-data这些参数会被忽略所有配置以 spec 为准。下面是一个更贴近实际项目的 spec 片段处理了动态导入和资源目录# 在 Analysis 的 hiddenimports 里补充动态导入的模块 hiddenimports[ pkg_resources.py2_warn, # setuptools 相关3.5 常见缺失 sqlite3, # 如果用了 sqlite 但没显式 import myapp.plugins.loader, # 自己项目里动态加载的模块 ], # datas 里把整个资源目录加进去 datas[ (assets, assets), # 把 assets 文件夹整个复制到解压目录的 assets 下 (config/default.ini, config), ],打包后程序运行时sys._MEIPASS下就会有assets和config目录。代码里统一用resource_path(assets/logo.png)来访问。如果漏了某个资源程序会在目标机上报FileNotFoundError这时候回到 spec 检查datas是否覆盖了所有非 Python 文件。4. 避坑指南PyInstaller 3.5 的五个高频翻车点4.1 打包成功但运行报 ModuleNotFoundError现象exe 在开发机上跑得好好的到目标机双击就弹ModuleNotFoundError: No module named xxx。原因通常是动态导入的模块没被静态分析扫到或者某个依赖库在目标机上不存在。解决在 spec 的hiddenimports里加上缺失的模块名重新打包。如果不知道缺哪个用pyinstaller --debugimports demo.py打包运行时会在控制台打印导入过程能看到具体卡在哪个模块。4.2 单文件模式启动极慢或杀毒软件报毒现象-F打包的 exe 启动要 10 秒以上或者被某数字杀毒软件直接隔离。原因有两个一是单文件模式每次都要解压文件越大越慢二是 PyInstaller 的引导程序行为类似自解压木马容易被误报。解决改用-D单目录模式启动速度立刻正常。如果必须单文件在 spec 里关掉 UPX 压缩upxFalseUPX 压缩后的二进制更容易触发杀软。另外给 exe 加上公司签名或者用--key加密字节码也能降低误报率但 3.5 的加密需要额外装tinyaes。4.3 路径拼接用了相对路径导致找不到文件现象代码里写open(config.ini)开发时正常打包后报文件不存在。原因打包后 exe 的工作目录是启动时的目录不是 exe 所在目录更不是解压目录。解决所有资源访问都走sys._MEIPASS拼接封装成resource_path函数在代码里统一调用。不要用os.getcwd()那个返回的是命令行当前目录不可靠。4.4 打包时报 UnicodeDecodeError 或路径含中文现象pyinstaller执行到一半报UnicodeDecodeError: gbk codec cant decode byte...。原因项目路径或某个依赖文件的路径里有中文3.5 在 Windows 下默认用 GBK 读文件遇到 UTF-8 内容就崩。解决把项目移到纯英文路径下比如D:\work\myproject。如果依赖库装在中文路径的 Python 里考虑重装 Python 到英文路径。这是 3.5 的已知问题新版 PyInstaller 已经改成 UTF-8 优先。4.5 目标机缺少 VC 运行库导致 exe 无法启动现象exe 双击后闪退或者报The program cant start because VCRUNTIME140.dll is missing。原因PyInstaller 打包时不会自动带上 VC 运行库它假设目标机已经装了。解决在交付文档里注明需要安装Visual C Redistributable for Visual Studio 2015或者把vcruntime140.dll和msvcp140.dll手动加到 spec 的binaries里。更稳妥的做法是在目标机上先装一遍 Python 对应的 VC 运行库再运行 exe。5. 进阶技巧用运行时钩子解决隐藏依赖与体积优化5.1 运行时钩子拦截导入过程PyInstaller 的 hook 分两种hookspath里的普通 hook 在分析阶段执行runtime_hooks里的运行时钩子在 exe 启动时执行。运行时钩子适合做环境适配比如某个库在打包后需要改sys.path或者设置环境变量。下面这个例子拦截matplotlib的导入强制使用非交互后端避免打包后弹窗报错# runtime_hook_matplotlib.py # 这个文件要放在 spec 同级目录然后在 runtime_hooks 里引用 import os import sys # 设置 matplotlib 后端为 Agg不依赖 GUI os.environ[MPLBACKEND] Agg # 如果打包后临时目录里没有字体缓存指定一个可写目录 if hasattr(sys, _MEIPASS): os.environ[MPLCONFIGDIR] os.path.join(sys._MEIPASS, mplconfig)在 spec 里这样引用a Analysis( ... runtime_hooks[runtime_hook_matplotlib.py], ... )运行时钩子的执行时机在入口脚本之前所以任何全局环境变量的设置都应该放在这里。注意运行时钩子里的代码也会被打进 exe不要写太重的逻辑否则影响启动速度。5.2 用 excludes 和 UPX 控制体积一个带numpy和pandas的项目打包后 200MB 起步。优化手段有两个一是excludes排除确定不用的库比如tkinter、matplotlib、PyQt5二是用 UPX 压缩二进制文件。UPX 的坑在于某些杀软对 UPX 壳极度敏感而且 UPX 压缩后的 DLL 在部分 Windows 7 机器上加载会失败。我的习惯是先不加 UPX 打一版测通了再加 UPX 打第二版对比体积和稳定性。如果目标环境是 Windows 10 以上UPX 基本没问题如果是 Windows 7 工控机建议关掉 UPX。# 在 spec 里排除不需要的库 excludes[ tkinter, matplotlib, PyQt5, IPython, jupyter, pytest, ],排除后记得重新跑一遍功能测试因为有些库是间接依赖排掉后可能导致运行时缺模块。我一般会写一个简单的冒烟测试脚本打包后自动运行几个核心功能确认没报错再交付。5.3 验证打包结果是否真的独立打包完成后不要只在开发机上测。找一个干净的虚拟机或者另一台没装 Python 的机器把dist目录复制过去运行。重点检查三件事程序能否启动、核心功能是否正常、退出后临时目录有没有残留。如果条件有限至少用where python确认当前命令行没有 Python 在PATH里然后直接双击 exe。另外用dumpbin /dependents demo.exe需要 VS 工具链可以查看 exe 依赖了哪些系统 DLL如果看到非系统 DLL 不在打包目录里说明漏了文件。从那以后我每次打包完都会在一台旧笔记本上跑一遍完整流程那台机器没装 Python、没装 VC 运行库能跑通才算过。希望帮到你。本文还有配套的精品资源点击获取