Python 打包成 exe 避坑指南(PyInstaller 实战)
写 Python 的第十五年,我被问得最多的问题之一就是:"我写了个脚本,怎么发给不会装 Python 的同事用?"答案就是 PyInstaller——把 .py 打包成 .exe,双击就能跑。
但 PyInstaller 这个工具,说简单也简单,一行命令就能打包;说坑也真坑——打包出来的 exe 在你电脑上好好的,发给别人就报错;体积动不动就上百 MB;杀软还天天报警。这篇文章我把自己踩过的坑全趟了一遍,下面的截图全是我在本机真实跑出来的。
一、先装 PyInstaller,一行命令的事
装 PyInstaller 很简单,pip 一行:
pipinstallpyinstaller装完后敲pyinstaller --version,看到版本号就说明装好了。我本机用的是 6.16.0,Python 3.12。
二、最小打包:一行命令搞定
我在 demo 目录里放了个最简单的待办清单 GUI 程序(todo.py),只有 960 字节。打包命令就一行:
pyinstaller--onefile--nameTodoApp todo.py等个几十秒,终端刷一大堆 INFO 日志,最后看到Build complete!就成了。
打包完会生成三个东西:
build/:中间文件,可以删dist/:成品目录,exe 就在这里TodoApp.spec:配置文件,一般不用管
三、–onefile vs --onedir:单文件还是文件夹?
上面用的--onefile,打包出来就是一个单独的 exe 文件,11.1 MB。发给别人一个文件就完事了,看着很爽。
如果换成--onedir(或者不写,默认就是这个),打包出来是一个文件夹:exe 本身只有 1.7 MB,但旁边多了个_internal文件夹,所有依赖的 DLL、Python 解释器、库全在里面。整个文件夹加起来其实比 onefile 还大。
你看_internal里塞了 60 个文件:Python 解释器的 .pyd 扩展、tkinter 依赖的 tcl/tk 数据、一堆系统 DLL。这些就是你的程序跑起来需要的全部家当。onedir 模式就是把它们原封不动摊开,onefile 就是把这些全压缩进一个 exe 里。
怎么选?
| 模式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
--onefile | 单文件,方便分发 | 启动慢(每次都要解压到临时目录) | 发给别人用的小程序 |
--onedir | 启动快,体积小(不重复压缩) | 要发一整个文件夹 | 自己用的程序、内部工具 |
我的建议是:分发给别人就用 onefile,自己用就用 onedir。onefile 启动慢的问题,对一个双击就用的 GUI 程序来说,1-2 秒的等待完全可以接受。
四、–noconsole:GUI 程序别带黑框
这是最容易忘的一个参数。你写了个 tkinter GUI 程序,打包完双击运行——先蹦出来一个黑色的命令行窗口,然后才弹出你的 GUI。那个黑框就是控制台窗口。
加个--noconsole(或者简写-w)就没了:
pyinstaller--onefile--noconsole--nameTodoApp todo.py注意:如果你是命令行程序(要 print 输出、要 input 输入),就别加这个参数,加了你就看不到任何输出了。这个参数只给 GUI 程序用。
五、这些坑我替你踩过了
5.1 坑一:hidden-import——你的库它找不到
最常见的报错就是ModuleNotFoundError: No module named 'xxx'。你明明装了这个库,Python 脚本里也 import 了,PyInstaller 打包的时候也没报错,结果一运行就崩。
我写了个用importlib.import_module("requests")动态导入的脚本,打包时没加--hidden-import,运行就报了上面这个错。原因是有些库在运行时才动态导入子模块,PyInstaller 静态分析的时候发现不了。
解决办法就是手动告诉它:
pyinstaller--onefile--hidden-import=requests --hidden-import=requests.packages.urllib3 todo.py加上--hidden-import=requests重新打包,再运行就正常了——能正确输出 requests 版本号,还能成功请求 PyPI API。或者直接在脚本里静态 import 一下那个找不到的模块,PyInstaller 就能扫到了。
5.2 坑二:文件路径——别用相对路径
打包完你会发现,脚本里写的相对路径(比如open("data.txt"))全错了。因为 onefile 模式运行时,程序是解压到一个临时目录里跑的,你的当前工作目录根本不是 exe 所在的目录。
解决办法:用sys._MEIPASS找资源文件的真实路径:
importsys,osdefresource_path(relative_path):ifhasattr(sys,'_MEIPASS'):base=sys._MEIPASSelse:base=os.path.abspath(".")returnos.path.join(base,relative_path)然后读文件就用resource_path("data.txt")。这个写法在开发时和打包后都能正常工作。
5.3 坑三:体积太大——一个 helloworld 凭什么 11 MB?
我那个只依赖 tkinter 的待办程序,打出来 11 MB。这还是因为我用的是 Anaconda 环境,里面带了一堆没用的库。如果你用的是 PyPI 官方源装的干净 Python,打包出来会小不少。
想再压小一点,有几个办法:
- 用干净的虚拟环境打包(别在 Anaconda 大环境里打)
- 加
--exclude-module排除不用的模块 - 用 UPX 压缩(网上有教程,但我个人觉得没必要,11 MB 现在根本不算事)
5.4 坑四:杀毒软件报毒——这个锅不是你的
PyInstaller 打包出来的 exe,经常被 Windows Defender、360、火绒报毒。这不是你的程序有毒,而是 PyInstaller 的打包方式和某些病毒的行为很像(自解压到临时目录、动态加载 DLL)。
解决办法:
- 跟用户解释清楚,加白名单
- 给 exe 做数字签名(这个要花钱买证书)
- 别太慌,只要你代码干净,报毒就是误报
六、给你一个能直接抄的完整命令模板
折腾了这么多,我整理了一个最常用的完整命令,你直接改文件名就能用:
pyinstaller ^--onefile^--noconsole^--nameMyApp ^--iconapp.ico ^ --hidden-import=requests ^ --hidden-import=PIL.Image ^ --add-data"assets;assets"^ main.py每个参数的意思:
--onefile:打单文件--noconsole:GUI 程序去黑框--name:exe 的名字--icon:程序图标(.ico 文件)--hidden-import:手动指定要包含的模块--add-data:把资源文件(图片、配置)一起打包进去,分号前面是源路径,后面是打包后的路径
七、总结:打包就这几步
PyInstaller 不难,记住这几点就够了:
- 一行命令就能跑,先跑通再调参数
- –onefile 给别人,–onedir 给自己
- GUI 程序记得加 --noconsole
- 报 ModuleNotFoundError 就加 --hidden-import
- 读资源文件别用相对路径,用 sys._MEIPASS
- 体积大就换干净虚拟环境,别在 Anaconda 里打
打包不是什么高深技术,就是个体力活。把上面这些坑都避开了,你打包出来的 exe 基本就能在别人电脑上正常跑了。你打包的时候还遇到过什么奇葩报错?评论区聊聊,我看看能不能帮你解决。
-import**
5.读资源文件别用相对路径,用 sys._MEIPASS
6.体积大就换干净虚拟环境,别在 Anaconda 里打
打包不是什么高深技术,就是个体力活。把上面这些坑都避开了,你打包出来的 exe 基本就能在别人电脑上正常跑了。你打包的时候还遇到过什么奇葩报错?评论区聊聊,我看看能不能帮你解决。