PyInstaller 打包注意事项:为什么你的包会意外变大

0 阅读9分钟

PyInstaller 打包注意事项:为什么你的包会意外变大

类型:通用经验 + 匿名案例
适用:Python 桌面应用、带原生依赖(OpenCV / ONNX / 音视频)的项目
案例来源:某多媒体桌面工具的实战排障(已脱敏)
姊妹文档(项目内维护):docs/packaging-notes.md


1. 写在前面

用 PyInstaller 打出来的目录,有时比「心里预期」大一个数量级——例如从「几百兆」变成 1 GB 以上。这往往不全是「依赖本来就大」,而是 打包策略把同一份东西装了两次,再叠上一些本不该进发行包的文件。

本文重点讲 体积问题 的来龙去脉,并给出一套可复用的检查方法。文中的真实案例已脱敏:产品名、仓库、路径、API 提供商均用泛称代替。


2. 匿名案例:从 1.3 GB 到约 800 MB 发生了什么

2.1 背景(脱敏)

Python 桌面多媒体工具(下称「本项目」)具备:

  • 本地 Web UI(pywebview)
  • 视频下载、抽帧、硬字幕 OCR、语音转写
  • 调用云端大模型 API 做内容生成(Key 由用户自行配置)

发行形态为 onedir:一个 exe + _internal 目录,整文件夹拷贝分发。

2.2 现象

阶段发行目录总体积用户感受
初版打包约 1.25–1.35 GB「怎么这么大?」
排障精简后约 750–800 MB仍偏大,但可解释

精简后仍无法压到百兆级——这是 业务能力决定的下限,后文会说明。

2.3 体积解剖(精简前)

以下为构建机上的 量级参考,非精确值:

组成部分约占用里面是什么
外置 libs/site-packages/~640 MBOpenCV、视频解码、ONNX Runtime、推理引擎、下载器等
外置 libs/models/~140 MB离线语音模型(base 档)
外置 libs/bin/~84 MB额外复制的 FFmpeg
PyInstaller _internal/~450–550 MB与上面高度重叠的同一批库
UI、配置、业务规则等<5 MB可忽略

关键发现: 真正「多出来」的约 500 MB,不是新功能,而是 重复打包


3. PyInstaller 体积从哪来:先建立正确心智模型

3.1 onedir 结构(PyInstaller 6.x 常见)

<发行目录>/
├── YourApp.exe
├── _internal/          ← PyInstaller 收集的运行时(字节码、扩展模块、DLL)
├── (可选)外置资源目录
└── ...

很多人只盯着 exe 几十 MB,但 体积几乎都在 _internal 和外置资源里

3.2 分析阶段决定了「会带上谁」

PyInstaller 在 Analysis 阶段会:

  1. 从入口脚本做静态依赖追踪
  2. 读取 hiddenimports 强制纳入的模块
  3. 读取 pathex 扩展搜索路径,从更多 site-packages 里「发现」模块
  4. 执行 hook,可能拖入整棵依赖树(如 hook-cv2hook-PyQt

结论: spec 里多写一行 hiddenimports,发行包就可能多出 数百 MB

3.3 「运行时能 import」≠「磁盘上只有一份」

Python 的 sys.path 决定 先从哪里加载;并不自动删除其他路径上的副本。

若你采用「exe 壳 + 外置 libs」架构,却在 PyInstaller 里又把 libs 里的重型库打进 _internal,则:

  • 运行时:可能只用到外置 libs 那一份
  • 磁盘上:两份都在 → 用户/compress 看到的就是双倍
flowchart TB
    subgraph problem [典型体积陷阱]
        A[入口脚本] --> B[PyInstaller Analysis]
        B --> C[_internal 含 OpenCV/ONNX/...]
        D[构建脚本 copy 外置 libs] --> E[libs 含同样依赖]
        C --> F[发行目录 ≈ 2x]
        E --> F
    end

4. 包太大的六大原因(按优先级)

4.1 重复打包(本案主因,最常见也最容易忽视)

典型组合:

错误做法后果
hiddenimports 列出 cv2onnxruntime、自定义 C 扩展包全部进 _internal
pathex 指向 libs/site-packages分析阶段主动「捞」重型库
构建后又 shutil.copytree(整个 libs)外置再来一份

修复思路(通用):

  • hiddenimports 只保留引导层:UI 桥、业务包、确实无法自动发现的轻量模块
  • 对确定由外置 libs 提供的包,使用 excludes 显式排除
  • pathex 不要包含外置 site-packages(只保留项目源码根)
  • 外置 libs 用 staging 脚本精简复制,而非无脑整目录拷贝

本案精简后 _internal~500 MB → ~50 MB,总包体下降约 40%


4.2 把「开发依赖」打进发行包

pip install --target libs/site-packages 准备离线依赖时,常会带入:

  • pipsetuptoolswheel
  • pygments、测试框架等

它们 运行时不需要,但每个占数 MB 到十余 MB。

建议: staging 时维护 EXCLUDE_TOP_LEVEL 名单,复制 site-packages 时过滤。


4.3 同功能资源重复(如双份 FFmpeg)

本案中外置 libs/bin/ffmpeg(~84 MB)与 imageio_ffmpeg 内置二进制功能重叠;下载模块已有回退逻辑,不必再复制 bin 目录

通用检查: 在发行目录里对 ffmpeg*.dll 同名文件做搜索,看是否多份。


4.4 模型与缓存被打进包

离线 AI 工具常见坑:

内容是否应随包分发
生产所需的 一个 模型权重是(按需选一个档位)
开发机预热的 多个 模型(tiny + base + …)否(除非产品承诺可切换)
Hugging Face / 训练框架 缓存、日志
构建机 output/、用户任务结果绝不可

本案默认只 staging 一个 base 档模型;改用小档模型可再省 约 70–100 MB


4.5 hiddenimports 滥用:「怕缺就全写上」

遇到 ModuleNotFoundError 时,新手容易把整份 requirements.txt 写进 hiddenimports

正确顺序:

  1. 确认模块是否 懒加载(函数内 import)—— Analysis 可能本就不会打进包
  2. 确认是否应由 外置 libs 提供——修 sys.path,不要修 hiddenimports
  3. 仅对 确认缺失且体量小 的纯 Python 模块添加 hiddenimports
  4. 重型库用 excludes + 外置部署,而不是 hiddenimports

4.6 业务本身依赖重(不是配置 bug)

即便做到「只装一份」,本案 libs 仍有 约 700 MB,因为能力栈决定:

能力依赖类型体积特点
视频抽帧原生解码库数十 MB 级 DLL
图像 / OCROpenCV + ONNX Runtime百 MB 级
本地语音转写推理引擎 + 权重文件引擎 + 模型
媒体下载转码下载器 + FFmpeg近百 MB

这不是 PyInstaller 配置能「优化没」的,而是 产品形态 问题。要再缩小,只能:

  • 首次启动在线下载模型(bootstrap)
  • 重计算放云端,本地只留 UI
  • 砍掉 OCR 或本地转写等能力

5. 推荐架构:「轻壳 + 外置 libs」(适合重型依赖)

若项目必须离线跑 OpenCV / ONNX / 音视频栈,建议明确分层:

_internal/     →  exe 引导、UI 桥、业务 Python 源码(目标:尽量 < 100 MB)
libs/          →  site-packages + models(接受它是大头)
外置 ui/config →  可独立更新、便于脱敏

5.1 启动时注入 sys.path(通用模式)

import sys
from pathlib import Path

def configure_runtime() -> None:
    if getattr(sys, "frozen", False):
        root = Path(sys.executable).resolve().parent
    else:
        root = Path(__file__).resolve().parent.parent

    site_packages = root / "libs" / "site-packages"
    if site_packages.is_dir():
        path = str(site_packages)
        if path not in sys.path:
            sys.path.insert(0, path)

要点:

  • 冻结模式下 PROJECT_ROOTexe 目录为准,不要写死开发机路径
  • 在导入重型库之前 调用 configure_runtime()(可在包 __init__ 最早执行)

5.2 spec 文件对照表

配置项重型依赖项目建议
hiddenimports仅 UI 桥 + 业务包
excludes列出所有外置提供的重型包名
pathex仅项目根,不含外置 site-packages
datas只打必要静态资源,模型走外置 libs
onefile重型项目 不推荐(解压慢、杀毒误报、难排障)

5.3 excludes 示例(按技术栈裁剪)

EXCLUDED_HEAVY_MODULES = [
  "cv2",
  "onnxruntime",
  "torch",
  "tensorflow",
  "faster_whisper",
  # ... 由外置 libs 提供的包名
]

包名须与 import 时一致;排除后务必保证外置目录在运行时可被 sys.path 找到。


6. 打包体积自查清单(发布前必做)

6.1 构建后看目录,不要只看 exe

# 示例:统计发行目录各一级子目录(Linux/macOS)
du -sh dist/YourApp/* | sort -h

# Windows 可在资源管理器属性中查看 _internal 与 libs 分别占用

健康信号(本案量级):

目录精简后约警报线
_internal50–80 MB> 300 MB 很可能又把重型库打进去了
libs600–750 MB视功能而定;突然暴涨先查是否 copy 了缓存/多模型

6.2 在发行目录内搜「不该重复」的文件

  • ffmpeg:是否出现两次以上
  • cv2onnxruntime:是否同时在 _internallibs
  • model.bin:是否有多套模型目录

6.3 干净环境冒烟测试

  1. 未安装 Python 的机器解压发行目录
  2. 勿使用开发机上的 config(可能含 API Key
  3. 跑一条端到端任务,确认外置 libs 加载正常

若只有开发机正常、干净机 ModuleNotFoundError,优先查 sys.path 与外置 libs 是否随包分发完整,不要先把重型库加回 hiddenimports。

6.4 脱敏检查(对外分发必做)

要求
API Key / Token默认配置为空;文档示例用 sk-****
用户生成内容output/、日志、任务队列勿打入包
构建机路径日志与截图不出现真实用户名、公司目录
第三方服务文档用「兼容 OpenAI 的 API 端点」等泛称

7. 常见误区(体积相关)

误区事实
「PyInstaller 打成 onefile 就更小」onefile 是压缩打包,首次运行解压;总占用往往 更大
「hiddenimports 越多越好」每多一个重型库,_internal 可能 +几十到几百 MB
「pip list 里有的都要带上」开发工具、测试库、缓存不必进发行包
「包太大一定是 PyInstaller 的锅」先区分 重复打包 vs 业务确实需要
「体积优化 = 删功能」第一阶段往往是 去重,不是砍能力

8. 本案精简手段小结(可复用到其他项目)

手段作用本案约节省
重型库移出 hiddenimports,加入 excludes消除 _internal 重复~450 MB
pathex 不再指向外置 site-packages减少 Analysis 误收间接
staging 过滤 pip/setuptools 等去掉开发垃圾~25 MB
不复制冗余 bin/ffmpeg去掉重复二进制~84 MB
只打一个语音模型档位避免多模型叠加每个模型 40–140 MB
小档模型可选精度换体积~70–100 MB

未解决部分: 约 700 MB 的 libs 是 单一副本 下的能力成本,需产品层决策是否在线化。


9. 延伸阅读与工具

资源说明
PyInstaller 官方文档 — What to bundle分析 / 收集机制
pyinstaller --exclude-module命令行排除(spec 中 excludes 等价)
build/<name>/warn-<name>.txt构建后查看未解析 import
pyinstaller --debug=imports运行时追踪导入来源(排障)

10. 与项目内文档的关系

  • 本文:面向「PyInstaller + 体积排障」的 通用写法,案例已脱敏,可对外分享经验。
  • docs/packaging-notes.md:同一案例的 项目内操作手册(含具体脚本名、命令、目录约定)。

若你维护类似「桌面 + 本地多媒体 + 云端 API」的 Python 应用,建议:

  1. 先按本文 第 6 节清单 做体积解剖
  2. 再对照项目内手册落地 spec 与 staging 脚本

11. 一句话总结

包太大,先看是不是装了两遍;再看是不是装了不该装的;最后才承认是业务真的需要这么大。

PyInstaller 不会自动帮你去重——外置 libs 与 _internal 的职责边界,必须在 spec 和构建脚本里写清楚。