PyInstaller 干的事很具体:把你写的 Python 脚本连解释器一起塞进一个 exe,别人拿到之后不用装 Python 就能双击。这是 Python 项目最容易遇到的需求,也是最容易踩坑的一步。

一条命令打包

装完之后在脚本目录敲一行,它自己找依赖、自己收集、自己链接:

pip install pyinstaller
pyinstaller --onefile --noconfirm --name demo demo.py

--onefile 是打成单个 exe,不加这个选项出来是一个文件夹(exe 加一堆 dll)。两种模式的差别后面单独说。

打包过程会打十几行 INFO,从 Copying bootloader EXE 到 Appending PKG archive,最后一行 Build complete 就结束了。产物在 dist 目录里。

25.9 秒,出来 11.3 MB

测试脚本很简单:import rich,打印一个三行表格。就这么点功能,打包结果:

项目数值
打包耗时25.9 秒
exe 体积11,895,050 字节(11.3 MB)
打包内容Python 3.13.14 解释器 + rich 及其依赖

11.3 MB 里绝大部分是解释器和标准库,你的代码只有几 KB。这也是 Python 打包 exe 体积下不来的根本原因,任何工具都绕不开。

每次启动都要解压一遍

onefile 模式有个容易被忽略的代价:exe 每次运行时会先把内部压缩包解到一个临时目录,再启动解释器。测试结果:

运行方式耗时说明
exe 首次运行927 毫秒含解压
exe 连续 5 次(中位)840 毫秒稳定在 830 到 843 之间
python demo.py 直接跑680 毫秒同一脚本,不打包

多了 160 毫秒。单次看不出来,但如果你的工具是被别的程序反复调用的(比如当命令行子命令用),这 160 毫秒会变成实实在在的开销。

要避开这个代价就用 --onedir(默认模式):产物是一个文件夹,exe 和依赖分开放,启动时不需要解压。缺点是分发给别人要打包成 zip,对方得解压整个文件夹,不能只丢一个 exe 过去。

中文输出会乱码,这个坑几乎人人踩

打包后的 exe 在控制台直接运行时中文正常,但一旦输出被重定向到管道或文件,就会变成这样:

      ������ exe Ҳ���õ�������
| ��Ŀ        | ���                  |

原因是 Windows 下 Python 的 stdout 默认按系统代码页(简体中文是 GBK)编码,而读到管道里的程序(CI 日志、父进程、日志收集器)通常按 UTF-8 解码。两边不一致就成了乱码。

两个解法,选一个:脚本开头加 sys.stdout.reconfigure(encoding='utf-8'),或者在 bat 里先 chcp 65001 再启动 exe。前者更可靠,因为改的是程序自己的行为,不依赖外部环境。

顺带一个判断技巧:如果同一段输出用 GBK 解码正常、用 UTF-8 解码乱码,说明程序输出的是 GBK。反过来就是 UTF-8。

打包体积怎么压

几个能立刻见效的手段:用 --exclude-module 排掉用不上的大库(tkinter、numpy 这类经常被误收集);用 venv 建一个干净环境再打包,别在装了几百个包的全局环境里跑,否则它会把整个 site-packages 都扫一遍;--strip 能去掉符号信息,但 Windows 上效果有限。

还有一个反直觉的点:打包时间 25.9 秒里,大部分花在分析 import 依赖上,不是压缩。依赖树越干净,打包越快,体积也越小。所以前面说的「干净 venv」对速度和体积是双重收益。

什么时候别用它

如果你的程序要处理大量数据或者依赖 numpy、pandas、torch 这些带原生扩展的库,打包体积会到几百 MB,启动解压时间也会明显变长,用户体验还不如给一个安装包。

杀毒软件误报也是常事。PyInstaller 的 bootloader 结构和某些恶意软件的打包方式类似,报毒了要提交白名单,这个过程你控制不了。

如果目标机器上本来就装了 Python,直接发源码加一个 requirements.txt 更省事,没必要为了「双击就能用」付出 11 MB 和 160 毫秒。

没测的部分列一下,3 项:onedir 模式的实际启动耗时 / 带 numpy 的打包体积 / 杀毒软件误报率。