简介:这份 PyInstaller 安装包面向需要将 Python 脚本打包为独立可执行文件的开发者,尤其适合希望脱离 Python 解释器分发程序、了解源码包结构与基础用法的初、中级用户。压缩包为 gz 格式,共 19 个文件,约 37KB,主体由 5 个 py 脚本构成,并配有 10 个 txt 测试/说明文本、2 个 pkg-info 元数据、1 个 cfg 配置及 regen-docs 文档生成脚本,体积轻量,便于快速浏览包内组织。已有 590 人学习下载。读者可据此了解 PyInstaller 的安装入口、依赖分析与打包流程,熟悉单文件与目录模式、spec 配置及简单测试思路,也可把它作为研究 Python 程序冻结与可执行文件分发的基础样本。
1. 先别急着 pip install:PyInstaller 装在哪个 Python 里,决定了后面一半的坑
某开发者把 Python 脚本发给同事,双击 exe 没反应,命令行一跑报ModuleNotFoundError。问题不在脚本,在于 PyInstaller 安装包做成了半成品。PyInstaller 安装包不是把.py拖进压缩包,它要把解释器、依赖、资源路径和启动参数一起封进去。Python 3.11 与 PyInstaller 6.x 的组合最常见,但装到全局环境、漏掉hiddenimports、onefile 路径写死,都会让包在别人机器上翻车。这套流程适合要把内部工具交付给非开发同事、又不想让对方装 Python 的人。
2. 把打包环境钉死:Python 3.11 与 PyInstaller 6.x 的隔离安装步骤
2.1 选版本:3.8/3.11/3.12 与 PyInstaller 5.x/6.x 的取舍
打包工具最怕“本机跑得通,目标机跑不起来”。PyInstaller 不是跨平台交叉编译工具:在 Windows 上打出来的是 Windows 可执行文件,在 Linux 上打出来的是 Linux 可执行文件,不能拿 Windows 的包去 Linux 上双击。所以第一件事不是装 PyInstaller,而是把“在哪个系统上打、给哪个系统用”定下来。常见做法是交付 Windows 工具就在 Windows 虚拟环境里打,交付 Linux 服务就在对应发行版的容器或虚拟机里打,别在 macOS 上给 Windows 用户出包。
Python 版本也不要随手拿最新版。PyInstaller 6.x 对 Python 3.8 到 3.12 的支持比较成熟,3.13 要看具体依赖的 C 扩展有没有对应轮子;PyInstaller 5.x 适合老项目,但新库的 hook 和打包行为会和 6.x 有差异。我的取舍是:新项目默认 Python 3.11 + PyInstaller 6.x;老项目如果依赖锁在 3.8,就继续用 3.8 + PyInstaller 5.x,不要为了追新把依赖全升一遍;3.12 可以用,但打包前先确认numpy、pandas、cryptography这类带二进制扩展的库能在目标 Python 上正常导入。
| 组合 | 适用场景 | 注意点 |
|---|---|---|
| Python 3.8 + PyInstaller 5.x | 老项目、旧依赖锁死 | 新库 hook 少,升级要谨慎 |
| Python 3.11 + PyInstaller 6.x | 新项目默认选择 | 优先选,兼容性和工具链较均衡 |
| Python 3.12 + PyInstaller 6.x | 需要新语法或新标准库 | 先验证 C 扩展轮子 |
| Python 3.13 + PyInstaller 6.x | 尝鲜、内部测试 | 依赖滞后时不要用于交付 |
注意:PyInstaller 版本不要写死到某个补丁号再到处复制。项目里用
pyinstaller>=6,<7这类范围约束,锁版本交给requirements.txt或锁文件,避免不同机器装出不同小版本后 hook 行为不一致。
2.2 建虚拟环境与安装 PyInstaller 的完整命令
装 PyInstaller 本身不复杂,复杂的是它会把当前环境里“看得见”的包一起分析。全局环境里装过多少东西,打包时就可能带进去多少东西,体积和误报都从这里来。我一般先建独立虚拟环境,再装项目依赖,最后装 PyInstaller。顺序不要反:先装 PyInstaller 再装依赖也能用,但依赖安装过程中如果改了 PyInstaller 的依赖,排查起来多一层干扰。
Linux 或 macOS 下可以这样建环境:
# 在项目根目录执行,Python 3.11 按本机实际命令替换 python3.11 -m venv .venv source .venv/bin/activate # 先升级 pip,再装 PyInstaller 6.x python -m pip install --upgrade pip python -m pip install "pyinstaller>=6,<7" # 确认当前环境里的 PyInstaller 版本 python -m PyInstaller --versionWindows PowerShell 下换成:
# 在项目根目录执行 py -3.11 -m venv .venv .\.venv\Scripts\Activate.ps1 python -m pip install --upgrade pip python -m pip install "pyinstaller>=6,<7" python -m PyInstaller --version这两段命令的关键点不在pip install,而在三个位置。第一,python -m venv .venv把环境放在项目目录下,后面清理、迁移、复现都只看这个目录,不会污染系统 Python。第二,激活后一定用python -m PyInstaller而不是直接敲pyinstaller,因为 PATH 里可能同时存在多个pyinstaller.exe,直接敲容易跑到另一个 Python 的脚本上,出现“明明装了却提示找不到模块”的玄学问题。第三,"pyinstaller>=6,<7"用引号包住,避免 shell 把>和<当成重定向符号。
如果 PowerShell 提示脚本执行策略阻止激活,不必改系统策略,直接用 CMD 执行.venv\Scripts\activate.bat即可。激活成功的标志是命令行提示符前面出现(.venv)。没有这个前缀时,后面所有python -m pip install都可能装到全局环境,打包时就会把全局包带进来。
提示:项目根目录不要用中文名和带空格的路径。PyInstaller 对路径的处理在大多数情况下没问题,但某些第三方 hook 和资源复制逻辑在空格路径下会出问题,排错成本远高于改目录名。
2.3 第一次打包前先跑一个最小样例
在装完依赖、还没碰业务代码之前,先拿一个最小脚本验证打包链路。这一步很像装机后先跑hello world,不是多余,而是把“PyInstaller 本身能不能用”和“业务代码有没有问题”分开。建一个main.py:
# main.py import sys def main(): print("hello from package") print("python:", sys.version) print("executable:", sys.executable) if __name__ == "__main__": main()然后用控制台模式打一个单文件包:
python -m PyInstaller --noconfirm --clean --onefile --console --name HelloPack main.py参数含义如下:--noconfirm表示覆盖dist和build目录时不交互询问,适合脚本化;--clean清掉 PyInstaller 缓存和临时文件,避免旧 hook 残留;--onefile打成单个 exe;--console保留控制台窗口,第一次打包一定用它,否则报错信息会被窗口程序吞掉;--name HelloPack指定产物名。执行后看dist/HelloPack.exe(Windows)或dist/HelloPack(Linux/macOS),双击或在终端运行,能打印出三行内容才算链路通。
如果这一步就失败,先看build/HelloPack/warn-HelloPack.txt。这个文件是 PyInstaller 的“黑匣子”之一,里面记录缺失模块、可选导入和 hook 警告。很多ModuleNotFoundError不是打包命令写错,而是这个文件里早就写了缺哪个包,只是没人看。第一次打包就把这个文件的位置记住,后面排查会省很多时间。
3. 配置 spec 与资源路径:把 --add-data、hiddenimports 和图标一次写对
3.1 命令行参数与 spec 文件的对应关系
小项目可以用一条pyinstaller命令打完,但只要涉及数据文件、动态导入、图标、版本信息,命令行就会越写越长,而且 Windows 和 Linux 的路径分隔符还不一样。这时候应该让 PyInstaller 生成 spec 文件,把配置固化下来。spec 文件本质是一个 Python 脚本,PyInstaller 读它来执行分析、收集和生成。命令行参数和 spec 字段的对应关系大致如下:
| 命令行参数 | spec 中的位置 | 作用 |
|---|---|---|
--add-data "src;dest" | Analysis.datas | 把文件或目录复制进包 |
--hidden-import xxx | Analysis.hiddenimports | 强制加入静态分析没发现的模块 |
--collect-submodules pkg | 通常写入hiddenimports | 收集某个包下的子模块 |
--exclude-module xxx | Analysis.excludes | 排除不需要的模块,减小体积 |
--icon app.ico | EXE.icon | 设置 Windows 可执行文件图标 |
--name MyApp | EXE.name/COLLECT.name | 设置产物名 |
--onefile | EXE直接包含 binaries 和 datas | 单文件模式 |
--onedir | EXE加COLLECT | 目录模式,启动更快 |
先生成一份 spec:
python -m PyInstaller --noconfirm --clean --onedir --console --name MyApp main.py执行后项目根目录会出现MyApp.spec。之后改配置就改这个文件,再用python -m PyInstaller MyApp.spec重新打包。注意:用 spec 打包时不要再同时传--onefile、--add-data这类参数,容易和 spec 里的配置打架。我的习惯是命令行只用于第一次生成 spec,后续全部走 spec,把 spec 和项目代码一起提交,换机器也能复现。
3.2 数据文件、动态导入和 sys._MEIPASS 的写法
数据文件是打包翻车的高发区。开发时open("config/config.yaml")能读到,是因为当前工作目录就是项目根目录;打包后双击 exe,当前工作目录可能变成桌面或C:\Windows\System32,相对路径立刻失效。PyInstaller 的解决办法是把资源文件通过datas复制进包,运行时用sys._MEIPASS找到解压目录或_internal目录。下面这个函数可以直接抄:
# path_helper.py import os import sys def resource_path(relative_path: str) -> str: """返回打包后或开发环境下的资源绝对路径""" base = getattr(sys, "_MEIPASS", os.path.abspath(".")) return os.path.join(base, relative_path)sys._MEIPASS是 PyInstaller 运行时注入的属性。onefile 模式下它指向临时解压目录,onedir 模式下它指向_internal目录;开发环境没有这个属性,就回退到当前工作目录。用getattr而不是直接访问,是为了让同一份代码在开发环境和打包环境都能跑。读取配置时写open(resource_path("config/config.yaml"), "r", encoding="utf-8"),不要写死./config/config.yaml。
对应的 spec 片段如下:
# MyApp.spec 的关键片段 from PyInstaller.utils.hooks import collect_submodules # 替换成你自己的包名;动态导入多时很有用 hiddenimports = collect_submodules("mypackage") datas = [ ("config/config.yaml", "config"), ("assets", "assets"), ] a = Analysis( ["main.py"], pathex=[], binaries=[], datas=datas, hiddenimports=hiddenimports, hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=[], noarchive=False, )datas里每一项是(源路径, 包内目标目录)。Windows 上命令行写法是--add-data "config;config",Linux 和 macOS 用冒号--add-data "config:config",这个分隔符差异是血泪经验,复制命令时一定要看系统。collect_submodules("mypackage")会把该包下所有子模块加入隐藏导入,适合插件式加载、importlib.import_module动态导入的场景。代价是可能多带一些用不到的模块,打包体积会涨,但比运行时报ModuleNotFoundError更容易接受。
注意:
sys._MEIPASS目录是只读的临时目录,onefile 退出后可能被清理。配置文件不要往这里写,日志也不要往这里写。可写数据放用户目录,例如os.path.expanduser("~")下的应用目录。
3.3 窗口程序、图标与版本信息
交付给普通用户的工具通常不需要黑窗口,这时用--windowed或 spec 里的console=False。但窗口程序没有控制台,异常信息看不到,所以必须提前把日志写出来。下面这段日志初始化可以放在入口最前面:
# log_setup.py import logging import os def setup_logging(): log_dir = os.path.join(os.path.expanduser("~"), "MyAppLogs") os.makedirs(log_dir, exist_ok=True) log_file = os.path.join(log_dir, "app.log") logging.basicConfig( filename=log_file, level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s", encoding="utf-8", ) return log_file日志目录放在用户目录下,而不是 exe 所在目录,是因为 Program Files 这类位置通常没有写权限;也不要放sys._MEIPASS,onefile 模式下临时目录会被清掉。窗口程序启动后如果闪退,先让用户把~/MyAppLogs/app.log发回来,比远程猜快得多。
图标用.ico文件,里面至少包含 16、32、48、256 几种尺寸,否则任务栏和资源管理器里可能显示模糊。版本信息可以用pyi-grab_version从一个已有 exe 抓模板,再改成自己的产品名、版本号、公司名(这里写虚构占位即可),不要手写二进制资源。打包命令类似:
python -m PyInstaller --noconfirm --clean --onedir --windowed \ --name MyApp \ --icon assets/app.ico \ --version-file version_info.txt \ --noupx \ main.py--windowed去掉控制台,--icon设置图标,--version-file写入 Windows 版本资源,--noupx关闭 UPX 压缩。UPX 能减小体积,但也更容易触发杀毒软件误报,交付包我一般默认关掉。版本资源不是装饰,用户右键属性能看到版本号,出了问题也能确认对方跑的是不是旧包。
4. 打包翻车排查:五个从日志和现象里能定位的问题
4.1 现象:双击没反应,命令行运行报 ModuleNotFoundError
现象很典型:资源管理器里双击 exe 没窗口,打开 CMD 执行同一个 exe,最后一行是ModuleNotFoundError: No module named 'xxx'。原因通常是动态导入没被静态分析发现,比如importlib.import_module(name)、插件目录扫描、__import__拼接模块名。PyInstaller 分析的是import语句,看不到运行时才决定的模块名。
解决顺序是:先在命令行模式运行,拿到完整 traceback;再在打包命令里加--hidden-import;如果是一整个包,用--collect-submodules。例如:
python -m PyInstaller --noconfirm --clean --onefile --console \ --name MyApp \ --hidden-import pandas._libs.tslibs \ --collect-submodules mypackage \ main.py--hidden-import后面写完整模块路径,不是包名;--collect-submodules mypackage会递归收集子模块。改完重新打包后,仍然要看build/MyApp/warn-MyApp.txt,确认缺失模块列表是否清空。如果这个文件里还有missing module named xxx,但程序能跑,可能是可选依赖,不必强行补;如果程序报错,就按报错模块补。
4.2 现象:打包成功但找不到配置文件
现象是开发环境正常,打包后启动报FileNotFoundError: config/config.yaml。原因有两个:一是--add-data没加,资源根本没进包;二是代码用相对路径读文件,运行时工作目录不是项目根目录。onefile 模式还会多一层临时解压目录,路径更不能写死。
解决分两步。第一步,代码统一走resource_path;第二步,spec 的datas里加资源映射。命令行方式可以这样写:
# Windows 用分号,Linux/macOS 把分号换成冒号 python -m PyInstaller --noconfirm --clean --onefile --console \ --name MyApp \ --add-data "config;config" \ --add-data "assets;assets" \ main.py--add-data的格式是“源路径;目标目录”,目标目录是包内相对路径。config;config表示把项目里的config目录复制到包内config目录。资源加进去后,代码里仍然要用resource_path("config/config.yaml")读取,不能因为加了--add-data就继续写相对路径。另外,配置如果允许用户修改,不要打包进只读资源,应该首次启动时复制一份到用户目录,之后读写用户目录那份。
4.3 现象:exe 被杀毒软件拦截或体积异常
现象是打包完的 exe 刚下载就被杀毒软件删掉,或者体积从几十 MB 涨到几百 MB。原因通常是三个:onefile 自解压行为容易被启发式规则拦截;UPX 压缩壳增加误报概率;打包环境不干净,把全局环境里的科学计算库、测试框架、文档工具全带进去了。
解决从环境开始。用独立虚拟环境,只装运行必需的依赖;打包时加--noupx;加图标和版本信息;交付前做代码签名或至少把文件哈希和说明一起给用户。体积方面先看build/MyApp/Analysis-00.toc,确认哪些大库被收集进来,再用--exclude-module排除确实不用的模块。例如:
python -m PyInstaller --noconfirm --clean --onedir --windowed \ --name MyApp \ --noupx \ --exclude-module matplotlib \ --exclude-module pytest \ main.py--exclude-module只排除确定不用的模块,不要看体积大就乱排。matplotlib、pytest这类如果代码里没有导入,本来也不该出现在干净环境里;出现说明虚拟环境不干净,优先重建环境而不是靠排除。交付包建议用 onedir 加压缩包或安装器,启动快,被杀毒软件误判的概率也低一些。
4.4 现象:多进程程序无限重启
现象是窗口反复弹出,任务管理器里同名进程越来越多,最后卡死。原因在 Windows 的多进程启动方式:spawn会重新导入主模块,如果入口没有freeze_support(),子进程会再次执行打包入口,形成递归启动。开发环境用fork的 Linux 上可能看不出来,一到 Windows 打包就暴露。
解决是在入口第一行加冻结支持:
# main.py import multiprocessing as mp def main(): # 业务入口 pass if __name__ == "__main__": mp.freeze_support() main()freeze_support()必须放在if __name__ == "__main__":里面,并且要在创建任何进程池之前调用。打包前在 Windows 开发环境也要用spawn方式测一遍,不要只在 Linux 上验证。用了concurrent.futures.ProcessPoolExecutor也一样,入口保护不能少。
4.5 现象:onefile 启动慢、临时目录爆满
现象是双击后要等好几秒才出界面,%TEMP%下出现一堆_MEIxxxx目录。原因是 onefile 每次启动都要把全部依赖解压到临时目录,依赖越大启动越慢;程序异常退出时临时目录可能来不及清理。小工具无所谓,带numpy、pandas、PyQt的程序用 onefile 就会明显拖慢。
解决办法是改用 onedir:
python -m PyInstaller --noconfirm --clean --onedir --windowed --name MyApp main.py产物是整个dist/MyApp目录,启动时不需要解压,排错也方便,缺哪个 DLL 一眼能看到。交付时把整个目录压缩,或者用安装器打包。onefile 适合单文件小工具,不适合依赖多、启动频繁的桌面程序。这个取舍没有绝对答案,但交付前一定要在目标机上冷启动测一次,别只看打包成功。
| 模式 | 启动速度 | 排错难度 | 适用场景 |
|---|---|---|---|
| onefile | 较慢,需解压 | 较高 | 小工具、单文件分发 |
| onedir | 较快 | 较低 | 依赖多、桌面程序、服务工具 |
5. 装完怎么验证:在一台没装 Python 的机器上跑通 dist 目录
打包成功不等于交付成功。我见过太多次“本机双击正常,同事那边缺 DLL”的情况,所以验证一定换一台没装 Python、没装项目依赖的机器,虚拟机也行。把dist/MyApp整个目录复制过去,不要只复制 exe;onedir 模式下 exe 旁边的_internal目录缺一个文件都可能启动失败。
验证按下面这张表走,不要只看窗口能不能弹出来:
| 检查项 | 动作 | 通过标准 |
|---|---|---|
| 冷启动 | 在干净机双击 exe | 正常出现界面或控制台输出 |
| 资源读取 | 打开依赖配置的功能 | 不报FileNotFoundError |
| 日志写入 | 触发一个可恢复错误 | 用户目录下生成日志文件 |
| 退出码 | 命令行运行后看返回值 | 正常退出为 0 |
| 依赖完整性 | 断网后再启动 | 不因缺 DLL 或模块崩溃 |
Windows 下可以在 PowerShell 里跑一遍退出码检查:
# 进入 dist\MyApp 目录后执行 .\MyApp.exe echo "exit=$LASTEXITCODE"Linux 或 macOS 下对应:
# 进入 dist/MyApp 目录后执行 ./MyApp echo "exit=$?"退出码不是 0 时,先看日志文件,再看命令行窗口里的 traceback。窗口程序没有控制台,就临时打一个--console版本复现,确认问题后再换回--windowed。资源读取和日志写入要分开测:资源读取验证--add-data和resource_path,日志写入验证用户目录权限和可写路径。断网启动能排除运行时偷偷下载依赖的情况,内部工具尤其要测。
从那以后我每次出包都强制走一遍“干净机冷启动、读配置、写日志、看退出码”这四件事,再发给别人。打包工具能帮你把解释器塞进包里,但塞不对路径、塞漏模块、塞进整个全局环境,它不会替你兜底。希望帮到你。
本文还有配套的精品资源,点击获取