4、PyInstaller进阶:spec文件定制、隐藏导入、数据文件打包、图标设置
说实话,很多人用 PyInstaller 就只会一句 pyinstaller -F main.py。嗯,我以前也是这么干的。直到有一次项目上线前,发现打包出来的 exe 缺了配置文件,用户一启动就报错……那场面,别提多尴尬了。
今天咱们聊聊 PyInstaller 的进阶玩法。说白了,就是让你真正掌控打包过程,而不是靠运气。
4.1 spec 文件:打包的“施工图纸”
你每次执行 pyinstaller main.py,它都会生成一个 main.spec 文件。这个文件,就是 PyInstaller 的配置文件。我个人习惯直接编辑 spec 文件,而不是每次都敲一堆命令行参数。
生成 spec 文件很简单:
pyi-makespec main.py
或者一步到位:
pyinstaller -F main.py --specpath ./spec
打开 spec 文件,你会看到类似这样的结构:
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='main',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
这里每个字段都有讲究。我挑几个重点说说:
- pathex:搜索路径。如果你的项目引用了其他目录的模块,加到这里。
- datas:数据文件列表。后面会细讲。
- hiddenimports:隐藏导入。这个坑最多。
- excludes:排除不需要的模块,能减小体积。
pyinstaller main.spec 就能打包,保证环境一致。
4.2 隐藏导入:解决“找不到模块”的玄学问题
为什么会这样?你本地跑得好好的,打包完一运行,报错 ModuleNotFoundError。我遇到过好几次,尤其是用了 importlib 动态导入,或者某些库用了懒加载。
PyInstaller 默认只会扫描你代码里显式 import 的模块。但有些模块是在运行时才被发现的,比如:
- 使用了
__import__()动态导入 - 框架内部的插件机制(比如 PyQt 的某些组件)
- 通过
pkg_resources或importlib.resources加载的资源
解决办法就是在 spec 文件的 hiddenimports 里手动声明:
a = Analysis(
['main.py'],
hiddenimports=[
'pandas._libs.tslibs.np_datetime',
'pandas._libs.tslibs.np_timedelta',
'cffi',
'PIL._tkinter_finder',
],
# ...
)
如果你不确定缺了啥,有个笨办法:在打包后的 exe 目录下,用 --debug 模式运行,看控制台输出的 import 日志。或者直接跑 pyinstaller --hidden-import=xxx main.py 试错。
4.3 数据文件打包:别让配置文件“失踪”
这是新手最容易翻车的地方。你的程序可能依赖图片、配置文件、模型文件、字体等。默认情况下,PyInstaller 只打包 .py 文件。
在 spec 文件里,datas 字段就是干这个的。格式是 (源路径, 目标目录):
a = Analysis(
['main.py'],
datas=[
('config/settings.ini', 'config'),
('assets/images/', 'assets/images'),
('models/bert.pth', 'models'),
],
# ...
)
这里有个细节:目标目录是相对于打包后的根目录的。比如 ('config/settings.ini', 'config'),打包后文件会放在 _internal/config/settings.ini 里。
那代码里怎么读取呢?不能用相对路径了。要用 PyInstaller 提供的工具函数:
import sys
import os
def resource_path(relative_path):
"""获取打包后的资源文件路径"""
try:
# PyInstaller 打包后,_MEIPASS 是临时解压目录
base_path = sys._MEIPASS
except AttributeError:
# 开发环境,直接用当前目录
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
# 使用示例
config_path = resource_path('config/settings.ini')
with open(config_path, 'r') as f:
config = f.read()
--onedir 模式,把大文件放在外部,启动快多了。
4.4 图标设置:给 exe 换个“脸”
默认的 PyInstaller 图标……嗯,你懂的。给程序换个图标,不仅看着专业,用户也更容易找到你的程序。
命令行方式:
pyinstaller -F -i myicon.ico main.py
在 spec 文件里设置:
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='main',
icon='myicon.ico',
# ...
)
几点注意事项:
- 格式必须为 .ico。别想着用 .png 或 .jpg,PyInstaller 不认。
- 图标尺寸:建议包含 16x16、32x32、48x48、256x256 等多尺寸。Windows 会根据显示场景自动选择。
- 在线转换:如果你只有 PNG,可以用 icoconverter.com 之类的工具转成 .ico。
4.5 综合示例:一个完整的 spec 文件
最后,给你看一个我实际项目里用的 spec 文件。它包含了上面提到的所有技巧:
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['main.py', 'gui.py'],
pathex=['C:\\myproject\\src'],
binaries=[],
datas=[
('config/*.ini', 'config'),
('assets/icons/', 'assets/icons'),
('models/*.pth', 'models'),
],
hiddenimports=[
'PIL._tkinter_finder',
'pandas._libs.tslibs.base',
'sklearn.neighbors._partition_nodes',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=['matplotlib', 'scipy.spatial', 'numpy.testing'],
noarchive=False,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='MyApp',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False, # 隐藏控制台窗口
icon='assets/icons/app.ico',
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='MyApp'
)
这个文件做了几件事:
- 打包了两个入口脚本
main.py和gui.py - 包含了配置文件、图标、模型文件
- 手动声明了三个隐藏导入
- 排除了
matplotlib等大模块(如果没用的话) - 设置了应用图标,并隐藏了控制台窗口
嗯,到这里 PyInstaller 的进阶用法就差不多了。说白了,spec 文件就是你的打包清单,把依赖、数据、图标都写清楚,打包就不会出幺蛾子。下次遇到打包问题,别急着搜百度,先打开 spec 文件看看,八成能找到原因。