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:排除不需要的模块,能减小体积。
我的小技巧: 把 spec 文件纳入版本控制。这样团队其他人拉下来,直接 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',
    ],
    # ...
)
注意: 别一股脑把所有模块都加进去。我曾经见过有人把整个 site-packages 都列进去,结果打包出来 2GB。只加你确认缺失的模块。

如果你不确定缺了啥,有个笨办法:在打包后的 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()
我踩过的坑: 有一次我把一个 500MB 的模型文件打包进去了,结果每次启动都要解压到临时目录,慢得要命。后来改用 --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。
避坑指南: 我曾经直接把一个 1024x1024 的 PNG 强行改成 .ico 后缀,结果打包后图标显示不出来。一定要用真正的 .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 文件看看,八成能找到原因。