Python 软件打包总结:PyInstaller vs Nuitka


Python 软件打包总结:PyInstaller vs Nuitka

在基于 Python(如 PyQt / PySide)开发科研仪器上位机、实验数据采集与桌面工具时,软件打包与发布是经常需要面对的环节。

初学者往往习惯直接使用 auto-py-to-exe 或执行 pyinstaller -F main.py 生成单个 exe 文件。但在稍复杂的项目中,这种方式常会带来启动变慢、动态链接库(DLL)加载失败、配置文件或模型路径找不到、Qt 插件报错等问题。

本文结合实际开发经验,整理 PyInstaller 与 Nuitka 的区别、打包模式的选择,以及从独立目录打包到制作安装包的标准发布流程。


一、为什么不建议优先追求单文件(onefile)?

很多人打包时第一反应是追求生成“单个独立的 exe”,但在实际项目中,更推荐优先输出自包含的目录(onedir 模式),再根据需要打包为安装程序。

1.1 单文件(-F)模式的常见问题

PyInstaller 的 -F 模式本质上是一个自解压过程:

  1. 启动较慢:每次双击运行时,程序都需要先在系统临时目录(AppData\Local\Temp\_MEIxxxxxx)解压 Python 解释器、依赖库和静态资源,体积较大时启动会有明显延迟。
  2. 资源路径容易出错:程序运行在临时目录下,如果代码中依赖相对路径读取配置文件、AI 模型权重或本地数据,容易出现“开发时正常、打包后找不到文件”的情况。
  3. 依赖问题较难排查:所有文件混在一起,一旦缺少系统运行库或硬件 SDK 导致闪退,不容易直接定位具体缺失了哪个文件。

1.2 推荐思路:目录分发 + 安装包制作

更稳妥的做法是:

  1. 优先生成完整的软件目录(onedir 模式):
    确保软件的所有依赖项都在同一个文件夹内自包含,可以直接在本地运行,不需要每次启动都进行解压。
    MySoftware/
    ├── MySoftware.exe
    ├── Qt DLL
    ├── plugins/
    ├── models/
    ├── config/
    ├── drivers/
    └── resources/
    
  2. 需要分发给最终用户时,使用 Inno Setup 制作安装包:
    使用 Inno Setup 或 NSIS 把上述目录打包为单个 Setup.exe。用户安装时会自动解压到指定目录、创建桌面快捷方式,并提供规范的卸载支持。

二、PyInstaller 与 Nuitka 的区别与选型

PyInstaller 与 Nuitka 是目前最常用的两种 Python 打包方案,二者的核心思路不同:

graph LR
    subgraph PyInstaller [PyInstaller: 冻结式打包]
        A1[Python 源码] --> B1[打包解释器 + 依赖库 + 字节码]
        B1 --> C1[生成可直接运行的可执行文件与目录]
    end

    subgraph Nuitka [Nuitka: 编译式发布]
        A2[Python 源码] --> B2[转译为 C/C++ 源码]
        B2 --> C2[调用 C 编译器生成原生机器码]
    end

2.1 方案对比

对比项 PyInstaller Nuitka
核心思路 把 Python 解释器、依赖库、DLL 和资源一起“冻结/打包” 先将 Python 代码编译为 C/C++,再生成可执行程序
打包速度 快(一般几十秒到两分钟) 较慢(涉及完整的 C++ 编译,需数分钟以上)
源码保护 相对一般(基于字节码,若有强需求需配合代码混淆) 更好(转为原生机器码,反编译难度高)
使用与调试 资料多、生态成熟,排错方便 构建与环境配置相对复杂
适用倾向 偏向开发效率与快速发布 偏向编译式发布与正式产品化

2.2 简单选择原则

  • 科研开发 / 实验室内部使用:

    • 技术栈:Python + PyQt/PySide
    • 方案:PyInstaller $\longrightarrow$ onedir $\longrightarrow$ 维护好 app.spec
    • 原因:代码迭代频繁、排错方便、构建速度快,非常适合实验和内部测试。
  • 正式交付 / 商业软件:

    • 技术栈:Python + PyQt/PySide
    • 方案:Nuitka standalone $\longrightarrow$ Inno Setup $\longrightarrow$ Setup.exe
    • 原因:源码保护更严密,整体分发形态更贴近传统原生软件。

注:PyInstaller 并不是“不专业”,Nuitka 也不是“商业专用”。更准确地说,PyInstaller 更注重开发效率与快速交付,Nuitka 更注重编译式保护与产品化。


三、PyInstaller 标准开发方式:维护 app.spec

尽量不要长期依赖 auto-py-to-exe 这种图形化一键工具。在实际项目中,建议正式维护项目根目录下的 **app.spec**。

spec 文件相当于软件的“打包说明书”,主要负责处理四项核心配置:

  1. datas:添加图片、配置文件、UI 文件、模型文件等静态资源。
  2. binaries:添加设备 SDK、C/C++ 动态链接库(DLL)、pyd 等二进制依赖。
  3. hiddenimports:补全动态导入(未显式写在 import 中的模块)。
  4. excludes:排除不需要的库(如测试库、不用的 GUI 框架),减小体积。

3.1 典型项目结构

project/
├── main.py               # 程序启动入口
├── app.spec              # 打包规格配置文件
├── requirements.txt      # 依赖包列表
├── gui/                  # 界面相关代码
├── modules/              # 业务逻辑与算法模块
├── resources/            # 静态资源 (图标、图片等)
├── models/               # 模型权重文件
├── config/               # 配置文件
└── vendor/               # 第三方 DLL、设备驱动库

3.2 典型 spec 配置示例

通过 pyinstaller -D -w main.py 可以生成初始 spec 文件,之后可在此基础上根据项目调整:

# -*- mode: python ; coding: utf-8 -*-
import sys
import os

block_cipher = None
project_root = os.path.abspath(os.curdir)

# 1. datas: 静态数据文件 (源路径, 打包后目标子目录)
datas = [
    (os.path.join(project_root, 'resources'), 'resources'),
    (os.path.join(project_root, 'config'), 'config'),
    (os.path.join(project_root, 'models'), 'models'),
]

# 2. binaries: 二进制依赖、设备 SDK 的 DLL 等 (源路径, 目标子目录)
binaries = [
    (os.path.join(project_root, 'vendor', 'device.dll'), '.'),
]

# 3. hiddenimports: 动态导入的 Python 模块
hiddenimports = [
    'scipy.special.cython_special',
    'sklearn.utils._typedefs',
]

# 4. excludes: 排除不需要的库
excludes = [
    'tkinter',
    'pytest',
    'notebook',
]

a = Analysis(
    ['main.py'],
    pathex=[project_root],
    binaries=binaries,
    datas=datas,
    hiddenimports=hiddenimports,
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=excludes,
    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,
    [],
    exclude_binaries=True,
    name='MySoftware',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    console=False,          # 是否显示控制台黑框 (调试时可设为 True)
    icon=os.path.join(project_root, 'resources', 'app.ico'),
)

coll = COLLECT(
    exe,
    a.binaries,
    a.zipfiles,
    a.datas,
    strip=False,
    upx=True,
    upx_exclude=[],
    name='MySoftware',       # 生成 dist/MySoftware 目录
)

后续更新打包时,直接执行:

pyinstaller app.spec --clean

3.3 运行时资源路径兼容写法

为了让代码在“本地开发运行”和“打包后运行”时都能正确找到文件,建议使用统一的路径解析函数:

import sys
import os

def get_resource_path(relative_path: str) -> str:
    """
    获取资源文件的绝对路径
    兼容本地开发与打包环境
    """
    if getattr(sys, 'frozen', False):
        # 打包环境
        if hasattr(sys, '_MEIPASS'):
            base_path = sys._MEIPASS
        else:
            base_path = os.path.dirname(sys.executable)
    else:
        # 开发调试环境
        base_path = os.path.abspath(".")

    return os.path.normpath(os.path.join(base_path, relative_path))

# 示例
config_path = get_resource_path("config/settings.json")

四、Nuitka 编译式发布参考

当项目对源码保护有要求,或者需要编译成更贴近原生的二进制文件时,可以选用 Nuitka。

4.1 环境准备

Nuitka 依赖 C++ 编译器(如 Windows 下的 MSVC 或 MinGW64):

pip install nuitka zstandard ordered-set

# 验证编译器环境
nuitka --version

4.2 常用打包命令示例

针对 PyQt / PySide 项目,采用 standalone 模式生成自包含目录:

python -m nuitka \
  --standalone \
  --enable-plugin=pyqt5 \
  --windows-disable-console \
  --windows-icon-from-ico=resources/app.ico \
  --include-data-dir=resources=resources \
  --include-data-dir=models=models \
  --include-data-dir=config=config \
  --include-data-files=vendor/device.dll=./device.dll \
  --output-dir=dist_nuitka \
  main.py
  • --standalone:生成包含运行环境的完整目录。
  • --enable-plugin=pyqt5(或 pyside6):自动包含所需的 Qt 插件(platforms、imageformats 等)。
  • --windows-disable-console:隐藏终端控制台窗口。
  • --include-data-dir:复制静态资源文件夹。

五、推荐的标准发布流程

一个规范的软件发布过程,建议按以下四阶段推进:

graph TD
    A[第一阶段: 开发与环境隔离] --> B[第二阶段: onedir 打包]
    B --> C[第三阶段: 干净环境测试]
    C --> D[第四阶段: 制作安装包]
    D --> E[最终交付: Setup.exe]

第一阶段:开发

  • 使用独立的虚拟环境(venv 或 uv),避免在装满第三方库的全局环境打包;
  • 维护好 requirements.txt 或依赖锁定文件,确保依赖版本可复现。

第二阶段:打包

  • 按照前文所述,优先采用 onedir / standalone 模式输出完整的软件目录;
  • 维护好 app.spec 或 Nuitka 编译脚本。

第三阶段:测试

  • 在没有安装 Python 的干净 Windows 电脑(或全新虚拟机)上测试;
  • 重点检查:
    1. 系统是否缺少 Visual C++ 运行库(如缺少 MSVCP140.dll 等);
    2. Qt 平台插件是否正常加载(避免 could not find the Qt platform plugin "windows");
    3. 硬件设备驱动、串口或 SDK 是否能正常通信;
    4. 读写本地配置、输出数据和日志是否有权限问题。

第四阶段:发布

  • 使用 Inno Setup 制作 Setup.exe 安装包。

Inno Setup 极简脚本示例(installer.iss)

[Setup]
AppName=MySoftware
AppVersion=1.0.0
DefaultDirName={autopf}\MySoftware
DefaultGroupName=MySoftware
OutputDir=.\Output
OutputBaseFilename=MySoftware_Setup
Compression=lzma2/ultra64
SolidCompression=yes
SetupIconFile=.\resources\app.ico

[Files]
; 复制 dist/MySoftware/ 目录下的所有文件及子文件夹
Source: "dist\MySoftware\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs

[Icons]
; 创建开始菜单与桌面快捷方式
Name: "{group}\MySoftware"; Filename: "{app}\MySoftware.exe"; IconFilename: "{app}\resources\app.ico"
Name: "{autodesktop}\MySoftware"; Filename: "{app}\MySoftware.exe"; Tasks: desktopicon; IconFilename: "{app}\resources\app.ico"

[Tasks]
Name: "desktopicon"; Description: "创建桌面快捷方式"; GroupDescription: "附加图标:"; Flags: unchecked

最终交付给用户只需一个 Setup.exe,安装后即可在桌面生成快捷方式并直接使用。


六、总结

  1. 发布原则:不要优先追求单个 exe。优先保证生成一个完整、自包含、稳定、可复现的软件目录,后续再通过 Inno Setup 制作安装包交付。
  2. 工具选型:
    • 科研软件 / 内部工具:PyInstaller + app.spec + onedir(开发速度快、易于调试与配置);
    • 正式产品 / 商业软件:Nuitka standalone + Inno Setup(编译式发布、代码保护更好)。
  3. 流程规范:坚持使用干净的独立虚拟环境,规范维护 spec 配置文件与资源路径解析函数,并在没有开发环境的干净机器上做好验收测试。

文章作者: BITBCI
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 BITBCI !
  目录