当你辛辛苦苦写完一个 Python 工具,兴冲冲地发给同事使用时,对方的第一句话往往是:
"Python 环境怎么装?依赖是什么?"
这就是 Python 分发之痛。PyInstaller 能将 Python 程序打包成独立的可执行文件(.exe / .app / ELF),让没有 Python 环境的用户也能直接运行。
本文从安装到进阶,全面覆盖 PyInstaller 的使用技巧和常见坑。
1. 为什么需要打包?
| 场景 | 问题 | 打包后的效果 |
|---|---|---|
| 分发给非开发者 | 对方没装 Python | 双击即运行 |
| 企业内部工具 | 环境配置繁琐 | 一个文件,拷贝即用 |
| 交付客户 | 源码泄露风险 | 不暴露源码(注意:并非绝对安全) |
| CI/CD 部署 | 依赖版本冲突 | 自包含,无依赖冲突 |
2. 安装与基本使用
2.1 安装
pip install pyinstaller验证安装:
pyinstaller --version
# 输出类似: 6.5.02.2 最简示例
假设有一个 hello.py:
# hello.py
import time
def main():
print("Hello, PyInstaller!")
print("程序将在 3 秒后退出...")
time.sleep(3)
if __name__ == "__main__":
main()打包:
pyinstaller hello.py生成的目录结构:
hello/
├── build/ # 编译临时文件
├── dist/ # 最终的可执行文件在这里
│ └── hello/
│ ├── hello # 可执行文件 (Linux/Mac)
│ ├── hello.exe # 可执行文件 (Windows)
│ └── _internal/ # 依赖库和运行时
└── hello.spec # 打包配置文件2.3 常用参数速查
| 参数 | 说明 | 示例 |
|---|---|---|
-F / --onefile |
打包成单个文件 | pyinstaller -F hello.py |
-D / --onedir |
打包成目录(默认,启动更快) | pyinstaller -D hello.py |
-w / --noconsole |
不显示控制台窗口(GUI 程序) | pyinstaller -w -F hello.py |
-c / --console |
显示控制台窗口(默认) | pyinstaller -c -F hello.py |
-i / --icon |
指定图标文件 | pyinstaller -i app.ico -F hello.py |
-n / --name |
指定输出文件名 | pyinstaller -n MyApp -F hello.py |
--add-data |
添加额外的数据文件 | pyinstaller --add-data "config.json:." |
--hidden-import |
手动指定隐式导入的模块 | pyinstaller --hidden-import pandas |
--clean |
打包前清理临时文件 | pyinstaller --clean -F hello.py |
3. 深入理解 spec 文件
每次运行 pyinstaller 都会生成 .spec 文件,它是打包的核心配置文件。直接编辑 .spec 然后执行 pyinstaller hello.spec 可以实现更精细的控制。
3.1 spec 文件结构
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['hello.py'], # 入口脚本列表
pathex=[], # 额外的搜索路径
binaries=[], # 要包含的二进制文件
datas=[], # 数据文件元组列表
hiddenimports=[], # 隐式导入
hookspath=[], # hook 路径
hooksconfig={}, # hook 配置
runtime_hooks=[], # 运行时 hook
excludes=[], # 排除的模块
noarchive=False, # 是否不打包进 archive
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
name='hello', # 输出文件名
debug=False,
strip=False,
upx=True, # 是否使用 UPX 压缩
console=True, # 是否显示控制台
icon=None, # 图标路径
)
# 单文件模式额外需要的部分
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='hello',
)3.2 datas 详解:如何携带资源文件
# spec 文件中的 datas 配置
datas=[
('config.json', '.'), # 将 config.json 复制到根目录
('assets/', 'assets/'), # 将整个 assets 目录复制进去
('README.md', '.'), # 文档也可以带
]格式说明:('源路径', '目标目录'),目标目录是相对于可执行文件的路径。
3.3 运行时读取打包资源
import sys
import os
def resource_path(relative_path):
"""获取资源文件的绝对路径,兼容开发环境和 PyInstaller 打包后"""
if getattr(sys, 'frozen', False):
# 打包后的环境
base_path = sys._MEIPASS
else:
# 开发环境
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
# 使用
config_path = resource_path('config.json')
with open(config_path, 'r', encoding='utf-8') as f:
config = f.read()4. 常见问题与解决方案
4.1 杀毒软件误报
这是 PyInstaller 打包最头疼的问题。原因:
- PyInstaller 的 bootloader 被某些恶意软件利用
- 单文件模式会在临时目录解压,触发行为检测
缓解措施:
# 1. 使用目录模式而非单文件模式
pyinstaller -D hello.py
# 2. 代码签名(Windows)
# 购买代码签名证书后用 signtool 签名
# 3. 使用 --clean 避免残留
pyinstaller --clean -F hello.py
# 4. 向杀软厂商提交白名单申请4.2 文件路径问题
# ❌ 错误写法
with open('data.csv', 'r') as f:
...
# ✅ 正确写法
import os
import sys
if getattr(sys, 'frozen', False):
base_dir = os.path.dirname(sys.executable)
else:
base_dir = os.path.dirname(os.path.abspath(__file__))
with open(os.path.join(base_dir, 'data.csv'), 'r') as f:
...4.3 动态导入的模块找不到
# 有些库使用 __import__ 或 importlib 动态导入
# 例如:importlib.import_module(f'some.module.{name}')
# 解决方法1:在代码中添加显式导入
import some.module.a
import some.module.b
# 解决方法2:在 spec 文件中添加 hiddenimports
# hiddenimports=['some.module.a', 'some.module.b']
# 解决方法3:使用 --hidden-import 参数
# pyinstaller --hidden-import some.module.a --hidden-import some.module.b4.4 打包后体积过大
# 1. 先分析体积分布
pip install pyinstaller-hooks-contrib
# 2. 使用 UPX 压缩(需先安装 UPX)
# 下载 UPX: https://upx.github.io/
# spec 文件中设置: upx=True
# 3. 排除不需要的模块
# spec 文件中:
# excludes=['tkinter', 'unittest', 'email', 'pydoc']
# 4. 使用虚拟环境
python -m venv venv
source venv/bin/activate
pip install only-what-you-need
pyinstaller -F hello.py4.5 添加版本信息(Windows)
创建 version.txt:
VSVersionInfo(
ffi=FixedFileInfo(
filevers=(1, 0, 0, 0),
prodvers=(1, 0, 0, 0),
mask=0x3f,
flags=0x0,
OS=0x40004,
fileType=0x1,
subtype=0x0,
date=(0, 0)
),
kids=[
StringFileInfo([
StringTable(
u'040904B0',
[
StringStruct(u'CompanyName', u'My Company'),
StringStruct(u'FileDescription', u'My Application'),
StringStruct(u'FileVersion', u'1.0.0'),
StringStruct(u'ProductName', u'MyApp'),
])
]),
VarFileInfo([VarStruct(u'Translation', [0x409, 1200])])
]
)pyinstaller --version-file version.txt -F hello.py5. 与其他方案对比
| 特性 | PyInstaller | Nuitka | cx_Freeze | py2exe |
|---|---|---|---|---|
| 跨平台 | ✅ Win/Mac/Linux | ✅ Win/Mac/Linux | ✅ Win/Mac/Linux | ❌ 仅 Windows |
| 单文件输出 | ✅ | ✅ (需 --onefile) |
❌ (仅目录) | ✅ |
| 编译为 C | ❌ (仅打包) | ✅ (编译优化) | ❌ | ❌ |
| 启动速度 | 中等 | 快 | 快 | 快 |
| 混淆/保护 | 弱 | 较强 (C 编译) | 弱 | 弱 |
| GUI 支持 | ✅ | ✅ | ✅ | ✅ |
| 活跃度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐ |
| 打包体积 | 大 (50MB+) | 较小 | 中等 | 中等 |
选型建议
- 通用场景:PyInstaller,生态最成熟、社区最活跃
- 性能优先 / 代码保护:Nuitka,将 Python 编译为 C 再编译为机器码
- 简单分发:cx_Freeze,配置简单但功能有限
6. 实战:打包一个完整的 CLI 工具
# 项目结构
my-cli/
├── mycli/
│ ├── __init__.py
│ ├── __main__.py
│ ├── cli.py
│ └── utils.py
├── config.yaml
├── requirements.txt
└── mycli.specmycli.spec:
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(
['mycli/__main__.py'],
pathex=[],
binaries=[],
datas=[
('config.yaml', '.'),
],
hiddenimports=[
'mycli.utils',
'mycli.cli',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[
'tkinter',
'matplotlib',
'PIL',
],
noarchive=False,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='mycli',
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,
icon='mycli.ico',
)打包命令:
pyinstaller mycli.spec7. CI/CD 自动化打包
GitHub Actions 示例
name: Build Executables
on:
push:
tags: ['v*']
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install pyinstaller
- name: Build
run: pyinstaller --clean -F mycli/__main__.py -n mycli
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: mycli-${{ matrix.os }}
path: dist/8. 小结
PyInstaller 是 Python 生态中最成熟的打包工具,但用好它需要掌握以下要点:
- 优先使用 spec 文件,而不是命令行参数,方便版本控制和团队协作
- 处理好路径问题,始终使用
sys._MEIPASS判断运行环境 - 缩减打包体积,使用虚拟环境 + excludes + UPX 三管齐下
- 提前考虑动态导入,在 spec 的
hiddenimports中显式声明 - 注意分发安全,PyInstaller 打包 ≠ 源码加密,敏感逻辑考虑 Nuitka 或 Cython
希望这篇指南能帮你顺利把 Python 程序分发给任何人!