用过
python打包生成Windows可执行程序的同学,大概率都用过大名鼎鼎的pyinstaller库吧。
pyinstaller 如果想最终生成的可执行程序,包括只限定的 python 指定模块或者要额外的静态资源文件,写执行指令一般都很长的。
shell
pyinstaller --noconfirm --log-level=WARN \
--onefile --nowindow \
--add-data="README:." \
--add-data="image1.png:img" \
--add-binary="libfoo.so:lib" \
--hidden-import=secret1 \
--hidden-import=secret2 \
--upx-dir=/usr/local/share
而有没有一种方式可以更好地进行 pyinstaller 的构建管理呢?看 官网文档:Using Spec Files --- PyInstaller 6.17.0 documentation,那就是使用 spec 文件进行构建管理。
PS:如果对 pyinstaller 基础使用不太了解,也可以看看小编的往期文章。 Pyinstaller - Python桌面应用打包的首选工具 - 掘金
1. spec文件的构成和使用
Q1:怎么使用 spec 文件呢?很简单:执行 pyinstaller 指令时带上对应的 spec 文件即可。
shell
pyinstaller --clean --noconfirm xxx.spec
Q2:spec 文件是怎么做到有效构建和管理模块和资源文件的呢?
主要通过下面几个核心的配置步骤:Analysis-分析依赖 → PYZ-打包模块 → EXE-生成exe → [COLLECT-收集文件]
💡温馨提示:单文件模式下,不需要额外进行第四步骤收集文件。

下面具体展开进行介绍。
2. Analysis 类
2.1 作用场景
依赖分析器,这是打包过程的核心步骤。它会:
- 分析你的Python脚本
- 递归查找所有导入的模块
- 收集所有需要的文件(Python模块、扩展模块、数据文件等)
✍️ 开头说的管理模块和数据文件,就主要在这里进行配置的。
2.2 参数配置详解
这里我用的比较多的就 excludes 、hiddenimports 和 datas,其他默认填写或按照模板即可。
python
a = Analysis(
# ============ 必需参数 ============
scripts=['main.py'], # 主脚本列表
# ============ 路径相关 ============
pathex=['/path/to/project', '/another/path'], # 模块搜索路径
hookspath=['custom_hooks'], # 自定义hook文件路径
runtime_hooks=['runtime_hooks/hook-qt.py'], # 运行时hook脚本
# ============ 模块处理 ============
excludes=[ # 排除的模块(减少包体积)
'tkinter',
'unittest',
'pydoc',
'pdb',
'matplotlib'
],
hiddenimports=[ # 隐藏导入(动态导入的模块)
'PIL._imaging',
'sklearn.utils._weight_vector',
'pandas._libs.tslibs.timedeltas'
],
win_no_prefer_redirects=False, # Windows专用:DLL重定向
win_private_assemblies=False, # Windows专用:私有程序集
# ============ 文件收集 ============
datas=[ # 非Python文件(配置文件、图片等)
('config.json', '.'), # (源文件, 目标目录)
('images/*.png', 'images'),
('data/*.csv', 'data')
],
binaries=[ # 二进制文件(.so, .dll等)
('/usr/lib/libssl.so', 'lib'), # (源文件, 目标目录)
('C:/Windows/System32/msvcp140.dll', '.')
],
# ============ 高级选项 ============
cipher=block_cipher, # 加密字节码的密钥
noarchive=False, # True=不解压到临时目录
optimize=0, # 字节码优化级别:-1, 0, 1, 2
upx=False, # 是否用UPX压缩(已弃用,在EXE中设置)
upx_exclude=[], # 排除UPX压缩的文件
strip=False, # 是否去除符号信息
prefetch=False, # 预取模式(实验性)
name=None, # 分析名称(用于多程序打包)
)
2.3 实际示例
python
# 复杂的Analysis配置
block_cipher = pyi_crypto.PyiBlockCipher(key='my-secret-key')
a = Analysis(
['app_main.py', 'helper.py'], # 多个入口文件
pathex=[os.getcwd(), '../shared_libs'],
binaries=[
('lib/ffmpeg.exe', 'bin'),
('lib/iconv.dll', 'bin')
],
datas=[
('ui/*.ui', 'ui'), # Qt Designer文件
('locales/*.qm', 'locales'), # 翻译文件
('*.ini', '.'), # 配置文件
('docs/*.md', 'docs')
],
hiddenimports=[
'win32timezone', # pywin32相关
'sqlalchemy.sql.default_comparator',
'pkg_resources.py2_warn'
],
hookspath=['hooks'], # 自定义hook目录
runtime_hooks=['hooks/runtime/rthook_pyside6.py'],
excludes=['scipy', 'numpy.testing'],
cipher=block_cipher,
optimize=1 # 字节码优化
)
3. EXE 类
3.1 作用场景
可执行文件构建器,将分析结果打包成最终的可执行文件。
3.2 参数配置详解
python
exe = EXE(
# ============ 必需参数 ============
pyz, # PYZ对象(包含所有Python模块)
a.scripts, # Analysis的scripts结果
a.binaries, # Analysis的binaries结果
a.zipfiles, # Analysis的zipfiles结果
a.datas, # Analysis的datas结果
# ============ 输出配置 ============
name='MyApp', # 输出文件名(不加.exe)
debug=False, # True=包含调试信息
strip=False, # True=去除符号信息(减小体积)
upx=True, # True=使用UPX压缩(需安装UPX)
# ============ 控制台/窗口设置 ============
console=True, # True=控制台程序,False=窗口程序(Windows)
disable_windowed_traceback=False, # 禁用窗口程序错误跟踪
argv_emulation=False, # macOS: 模拟命令行参数
# ============ 图标和元数据 ============
icon='app.ico', # 程序图标(Windows: .ico, macOS: .icns)
# ============ 运行时行为 ============
bootloader_ignore_signals=False, # 忽略信号(Unix)
runtime_tmpdir=None, # 临时目录路径
# ============ 重定向设置 ============
exclude_binaries=False, # True=不包含二进制依赖
code_signing_identity=None, # macOS代码签名标识
# ============ 加密和优化 ============
entitlements_file=None, # macOS授权文件
embed_manifest=True, # Windows: 嵌入manifest
target_arch=None, # 目标架构:'x86', 'x64', 'arm64'
# ============ 其他选项 ============
ascii=False, # 弃用
uac_admin=False, # Windows: 以管理员运行
uac_uiaccess=False, # Windows: UI访问权限
# ============ 调试相关 ============
bootloader_ignore_signals=False,
strict_arch=False,
)
3.3 实际示例
这里还真没怎么差异化使用过,一般都是配置 win 平台。
python
# 不同平台配置示例
if sys.platform == 'win32':
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='MyApp',
debug=False,
strip=False,
upx=True,
console=False, # 无控制台窗口
icon='app.ico',
uac_admin=True, # 需要管理员权限
embed_manifest=True,
version='version_info.txt' # Windows版本资源
)
elif sys.platform == 'darwin':
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='MyApp',
debug=False,
strip=True,
upx=False,
console=False,
icon='app.icns',
entitlements_file='app.entitlements',
code_signing_identity='Developer ID Application: Your Name (XXXXXX)'
)
else: # Linux
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='myapp',
debug=False,
strip=True,
upx=True,
console=True # Linux通常用控制台
)
4. todo-list应用完整文件示例
以最近我在撰写的 todo-list 应用的打包配置文件为例,这个主要是 AI 编写的,我就改了几行代码而已。
python
# -*- mode: python ; coding: utf-8 -*-
import sys
from pathlib import Path
# 获取项目根目录
project_root = Path(SPECPATH).parent
frontend_dir = project_root / 'frontend'
data_dir = project_root / 'data'
# 收集前端文件
frontend_files = [
('frontend/index.html', 'frontend/'),
('frontend/css/*', 'frontend/css'),
('frontend/js/*', 'frontend/js')
]
# 收集数据文件
data_files = []
if data_dir.exists():
for item in data_dir.rglob('*'):
if item.is_file():
relative_path = item.relative_to(data_dir)
data_files.append((str(item), 'data'))
block_cipher = None
a = Analysis(
['run.py'],
pathex=[str(project_root), str(project_root / 'backend'), str(project_root / 'TodoList' / 'backend')],
binaries=[],
datas=frontend_files + data_files,
hiddenimports=[
'webview',
'webview.platforms',
'webview.platforms.cef',
'webview.platforms.edgechromium',
'webview.platforms.mshtml',
'webview.platforms.gtk',
'webview.platforms.cocoa',
'webview.platforms.qt',
'sqlite3',
'json',
'threading',
'datetime',
'http.server',
'socketserver',
'urllib.parse',
'pathlib',
'os',
'sys',
'shutil',
'subprocess',
're',
'uuid',
'hashlib',
'base64',
'html',
'webbrowser',
'tkinter',
'tkinter.messagebox',
'tkinter.filedialog',
'tkinter.simpledialog',
# 后端模块
'database.operations',
'database.models',
'api.todo_api'
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[
'matplotlib',
'numpy',
'scipy',
'pandas',
'PIL',
'cv2',
'PyQt6'
],
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='TodoList',
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='todo_icon.ico' if Path('todo_icon.ico').exists() else None,
version='version_info.txt' if Path('version_info.txt').exists() else None
)
示例上可以看出实际上,spec 文件执行时是可以识别 python 代码的。
5. 补充说明
5.1 针对特定库的配置
python
# 打包PyQt6/PySide6应用
a = Analysis(
['app.py'],
hiddenimports=[
'PySide6.QtXml',
'PySide6.QtNetwork'
],
binaries=[
('/path/to/qt/plugins/platforms/*', 'platforms')
],
datas=[
('/path/to/qt/translations/*', 'translations')
]
)
# 打包机器学习应用(排除大型库)
a = Analysis(
['ml_app.py'],
excludes=[
'torch', # 太大,让用户自己安装
'tensorflow',
'jax'
],
hiddenimports=[
'sklearn.utils._cython_blas',
'numpy.core._multiarray_umath'
]
)
5.2 调试技巧
python
# 调试模式配置
exe = EXE(
# ...
debug=True, # 包含调试信息
strip=False, # 不去除符号
upx=False, # 不压缩(便于调试)
console=True # 显示控制台看错误
)
# 运行时查看缺失模块
import sys
if hasattr(sys, '_MEIPASS'):
print(f"临时目录: {sys._MEIPASS}")
print(f"模块路径: {sys.path}")
5.3 其他说明
除了用于 pyinstaller 的打包外,spec 文件也在其他场景有相关使用的:
-
RPM包管理:Linux系统软件打包
-
其他构建工具:作为配置文件使用
另外有类似作用的文件还有:setup.cfg、pyproject.toml 等
好啦,今天的分享就到这里了,感谢阅读,如果觉得文章对你有用的话,欢迎三连、欢迎关注!