第3章:PyInstaller实战:从命令行到Spec文件定制
说实话,PyInstaller 是我用过最顺手的 Python 打包工具。没有之一。
当年我第一次用它打包一个爬虫工具,命令行一敲就出 exe 了,那种感觉——嗯,真香。但后来项目复杂了,光靠命令行就不够用了。今天我就带你从命令行入门,一路走到 Spec 文件定制,把隐藏控制台、加图标、排除模块、打包数据文件这些实战技能全拿下。
3.1 命令行打包:快速上手
先来个最简单的。假设你写了个脚本叫 hello.py:
print("Hello, 跨平台打包!")
input("按回车退出...")
打包命令就一行:
pyinstaller hello.py
跑完之后,dist/hello/ 目录下就会出现 hello.exe。双击就能跑。
但这里有个坑——默认是带控制台的。如果你写的是 GUI 程序,比如用 PyQt 或 Tkinter,那个黑框框会一直跟着,特别碍眼。
--noconsole 或 -w 参数隐藏它。
3.2 隐藏控制台:让 GUI 程序干干净净
我个人习惯,只要确定是 GUI 程序,第一件事就是加 -w:
pyinstaller -w my_gui.py
这样打包出来的 exe 就不会弹出黑框了。用户双击后直接看到你的界面,体验好很多。
我在项目中遇到过一件事:有个同事忘了加 -w,结果交付给客户后,客户说「你这程序怎么还带个命令行窗口?是不是病毒?」——你看,细节决定成败。
3.3 添加图标:给程序穿上衣服
默认的 exe 图标是 PyInstaller 的默认图标,说实话挺丑的。给程序加个图标,用 -i 参数:
pyinstaller -w -i app.ico my_gui.py
注意:图标必须是 .ico 格式。如果你只有 PNG 图片,可以用在线转换工具转一下。
3.4 排除模块:给 exe 瘦身
默认情况下,PyInstaller 会分析你的代码,把用到的模块都打包进去。但它有时候会「过度分析」——把一些根本用不到的模块也塞进来。
比如你用了 requests,它可能把 urllib、http 等一堆相关模块都打包了。这时候可以用 --exclude-module 来排除:
pyinstaller -w --exclude-module tkinter --exclude-module unittest my_app.py
我曾经把一个 50MB 的 exe 通过排除模块瘦身到 18MB。你想想看,用户下载一个 50MB 的工具和 18MB 的工具,体验完全不一样。
怎么知道哪些模块可以排除?我建议你先打包一次,看看 dist/ 目录下的文件列表,然后对照你的代码,把明显没用的模块排除掉。
3.5 数据文件打包:让资源跟着 exe 走
很多程序需要附带数据文件,比如配置文件、图片、音频、数据库文件等。默认情况下,PyInstaller 只打包 Python 代码,数据文件需要手动指定。
命令行方式用 --add-data 参数:
pyinstaller -w --add-data "config.json;." --add-data "images/*.png;images" my_app.py
格式是 源路径;目标路径。注意分号在 Windows 下是分隔符,Linux/macOS 下用冒号。
但这里有个大坑——打包后的路径和开发时的路径不一样。代码里写死的相对路径会找不到文件。
sys._MEIPASS 获取打包后的临时目录路径。写一个通用函数来获取资源路径:
import sys
import os
def resource_path(relative_path):
"""获取资源的绝对路径,兼容开发环境和打包后的 exe"""
try:
# PyInstaller 打包后的临时目录
base_path = sys._MEIPASS
except AttributeError:
# 开发环境下的当前目录
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
# 使用示例
config_path = resource_path("config.json")
这个函数我几乎每个项目都会用。你把它放到工具模块里,所有需要读取数据文件的地方都调用它,一劳永逸。
3.6 Spec 文件:打包的终极武器
命令行参数多了之后,每次打包都要敲一长串命令,容易出错。这时候就该 Spec 文件出场了。
生成 Spec 文件很简单:
pyi-makespec my_app.py
或者从已有的打包命令生成:
pyinstaller -w -i app.ico --add-data "config.json;." my_app.py --specpath ./spec
Spec 文件其实就是一个 Python 脚本,里面定义了打包的所有配置。打开看看:
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['my_app.py'],
pathex=[],
binaries=[],
datas=[('config.json', '.'), ('images/*.png', 'images')],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=['tkinter', 'unittest'],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=None,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=None)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='my_app',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False, # 隐藏控制台
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='app.ico', # 图标文件
)
看到没?之前命令行里的 -w 对应 console=False,-i app.ico 对应 icon='app.ico',--add-data 对应 datas 列表,--exclude-module 对应 excludes 列表。
修改完 Spec 文件后,打包命令就变成了:
pyinstaller my_app.spec
干净利落。而且 Spec 文件可以放到版本控制里,团队协作时大家用同一份配置,不会出现「我电脑上能打包,你电脑上就不行」的尴尬。
3.7 实战:一个完整的 Spec 文件示例
下面是我一个真实项目的 Spec 文件,你可以直接拿来改:
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(
['main.py', 'utils.py'], # 多个入口文件
pathex=['C:\\my_project'], # 项目路径
binaries=[], # 动态链接库
datas=[
('config/*.json', 'config'), # 配置文件
('assets/*.png', 'assets'), # 图片资源
('database/*.db', 'database'), # 数据库文件
],
hiddenimports=[
'PIL._tkinter_finder', # 有时 PyInstaller 找不到的模块
'cffi',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[
'tkinter', # 没用 Tkinter 就排除
'unittest', # 测试框架不需要
'pdb', # 调试器不需要
'email', # 没用邮件功能就排除
],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='MyApp',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True, # 启用 UPX 压缩
upx_exclude=['vcruntime140.dll'], # 某些 DLL 不能压缩
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='app.ico',
)
upx=True 可以显著减小 exe 体积,但需要提前安装 UPX 工具。我一般会把它设为 True,然后手动排除那些压缩后会出问题的 DLL。
3.8 常见问题与避坑指南
打包过程中我踩过的坑,今天一次性告诉你:
- 路径问题:代码里不要用
os.getcwd()获取当前目录,打包后这个路径不对。用前面说的resource_path()函数。 - 动态导入:如果你的代码用了
__import__()或importlib动态导入模块,PyInstaller 可能分析不到。需要在hiddenimports里手动指定。 - UPX 压缩失败:某些 DLL 文件压缩后会损坏。遇到这种情况,在
upx_exclude里排除它们。 - 杀软误报:打包后的 exe 有时会被杀毒软件误报。可以尝试用
--upx-exclude排除某些文件,或者换个打包方式。
我曾经遇到一个项目,打包后 exe 在客户电脑上闪退。查了半天,发现是 config.json 文件没打包进去。从那以后,我每次打包完都会在 dist/ 目录下检查一遍,确保所有数据文件都在。
3.9 小结
这一章的内容,说白了就是让你从「能打包」进化到「会打包」。命令行适合快速验证,Spec 文件适合正式项目。记住几个关键点:
- GUI 程序一定要隐藏控制台
- 图标用
.ico格式,包含多尺寸 - 排除无用模块给 exe 瘦身
- 数据文件用
resource_path()函数处理路径 - Spec 文件是团队协作的最佳实践
下一章我们会深入 Spec 文件的更多高级用法,包括多进程打包、加密、签名等。准备好了吗?