Python打包方法

本文全面对比了PyInstaller和Nuitka两种Python打包工具的优劣势,帮助开发者选择最适合自己项目的打包方案。

PyInstaller 与 Nuitka 对比

基本介绍

在Python开发中,将代码打包成可执行文件是一个常见需求。目前市场上有多种打包工具,其中PyInstallerNuitka是两个广受欢迎的选择。

PyInstaller

PyInstaller是一个成熟的Python打包工具,它通过分析Python脚本的导入和依赖关系,将所有必要的文件打包到一个目录或单个可执行文件中。

  • 工作原理:PyInstaller使用Python的inspectmarshal模块分析代码依赖,然后将Python解释器和所有依赖打包在一起。

Nuitka(推荐)

Nuitka是一个相对较新的Python编译器,它将Python代码转换为C++代码,然后编译成本地可执行文件。

  • 工作原理:Nuitka将Python代码转换为C++代码,然后使用C++编译器(如GCC、MSVC)编译成本地可执行文件。

共同目标

这两个工具都能实现以下目标:

  1. 隐藏源码

    • PyInstaller:通过设置key对源码进行加密,但仍可被反编译工具破解
    • Nuitka:将Python源码转成C++(生成二进制的.pyd文件),大幅提高反编译难度
  2. 方便移植:用户无需安装Python或第三方包即可运行程序

  3. 跨平台支持:都支持在Windows、macOS和Linux上打包和运行

性能对比

方面 PyInstaller Nuitka
文件体积 😞 较大 🚀 较小
打包速度 ⏱️ 较慢 ⚡ 较快
启动速度 🐢 慢 🐇 快
运行性能 与Python解释器相同 有一定性能提升

实际体验对比

工具 体验 特点
PyInstaller 😞 很差,但是兼容性好 • 深度学习项目打包后的exe文件体积巨大(接近3GB)
• 打包速度慢(可能需要30分钟以上)
• 启动速度慢(冷启动可能需要10秒以上)
• 对复杂依赖处理能力有限
• streamlit打包支持良好
Nuitka 🚀 真香,但是兼容性差 • 同样的项目,生成的exe文件仅7MB
• 打包速度快(10分钟以内)
• 启动速度快(几乎瞬时启动)
• 运行性能有10-30%的提升
• streamlit打包支持很差

PyInstaller 的安装及使用

🔧 安装步骤

只需一行命令即可安装 PyInstaller:

1
pip install pyinstaller

📂 项目结构示例

以下是一个典型的 Python 项目结构:

1
2
3
4
├─utils        // 自定义工具模块
├─src // 核心源代码
├─logo.ico // 应用程序图标
└─demo.py // 主入口文件

🚀 打包命令

推荐使用多文件分发模式--onedir)生成可执行文件,这种模式下依赖文件单独存放,具有多项优势:

1
2
3
4
5
pyinstaller --onedir \
--icon=logo.ico \
--name=demo \
--clean \
demo.py

⚙️ 参数详解

参数 说明
--onedir 推荐选项:将所有文件打包到一个文件夹中
• ✅ 便于管理和更新依赖
• ✅ 启动速度更快
• ✅ 支持动态加载模块
--onefile 不推荐:将所有文件打包成单个可执行文件
• ❌ 文件体积较大
• ❌ 启动速度慢
• ❌ 不支持动态加载
--icon=logo.ico 指定生成的可执行文件图标
--name=demo 指定输出的可执行文件名称
--clean 在构建前清理 PyInstaller 缓存和临时文件
--noconsole 运行时不显示控制台窗口(适用于 GUI 程序)
--add-data="source:dest" 添加额外的非 Python 文件到包中
--hidden-import=pkg 添加 PyInstaller 无法自动检测到的导入
--additional-hooks-dir=path 指定自定义钩子脚本目录

📦 打包结果

打包过程通常需要 5-30 分钟(取决于项目大小),完成后的目录结构如下:

1
2
3
4
5
6
7
8
9
├─utils        // 原始源码文件夹(不变)
├─src // 原始源码文件夹(不变)
├─build // 构建过程中生成的临时文件
│ └─demo // 临时文件夹
├─dist // 📁 最终输出目录
│ └─demo.exe // ✨ 生成的可执行文件
├─demo.spec // PyInstaller 规范文件
├─logo.ico // 项目图标
└─demo.py // 主文件

🔍 高级用法

使用 spec 文件进行自定义配置

对于复杂项目,可以通过 spec 文件进行更精细的控制:

  1. 生成 spec 文件

    1
    pyi-makespec --onefile demo.py
  2. 编辑配置:修改 demo.spec 文件,添加自定义配置

  3. 使用 spec 文件构建

    1
    pyinstaller demo.spec

加密打包的 Python 字节码

可以使用 --key 参数对打包的 Python 字节码进行简单加密:

1
pyinstaller --onefile --key=your_secret_key demo.py

注意:这种加密方式只能提供基本保护,不能完全防止逆向工程。

🌟 Streamlit 应用打包指南

Streamlit 应用需要特殊处理才能正确打包。完整步骤如下:

1. 创建启动脚本

由于 PyInstaller 无法直接启动 Streamlit 应用,是通过streamlit run <你的脚本>.py实现,因此需要创建一个中间启动脚本 run.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import streamlit.web.cli as stcli
import os, sys
import <你的脚本>

if __name__ == "__main__":
if getattr(sys, 'frozen', False):
current_dir = sys._MEIPASS
else:
current_dir = os.path.dirname(os.path.abspath(__file__))
file_path = os.path.join(current_dir, "<你的脚本>.py")

sys.argv = ["streamlit", "run", file_path,
"--server.enableCORS=true", "--server.enableXsrfProtection=false",
"--global.developmentMode=false", "--client.toolbarMode=minimal"]
sys.exit(stcli.main())

关键点

  • 正确处理路径,判断当前路径是打包后启动的临时文件路径,还是平时直接运行脚本的路径,否则打包后启动exe程序会报错说找不到<你的脚本>.py文件,那样只能复制<你的脚本>.py跟exe在同个目录才能正常运行。
  • 禁用开发模式,developmentMode设置为false,否则启动的时候浏览器会自动跳到3000端口,而不是streamlit服务的端口

2. 创建 hooks 文件

创建目录和文件 ./hooks/hook-streamlit.py

1
2
from PyInstaller.utils.hooks import copy_metadata
datas = copy_metadata("streamlit")

3. 生成spec文件

  1. 方法一(仅生成spec,推荐!)
    1
    pyi-makespec run.py
  2. 方法二(生成spec同时打包,速度慢)
    1
    pyinstaller --additional-hooks-dir=./hooks run.py --clean

这一步的目的是生成 run.spec 文件,为下一步编辑做准备。

4. 编辑 spec 文件

修改生成的 run.spec 文件,确保包含所有必要的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['run.py'],
pathex=[],
binaries=[],
datas=[
('<你的脚本>.py', '.'), # 主脚本文件,应用程序的入口点
("./你的虚拟环境路径/Lib/site-packages/streamlit/static", "./streamlit/static"), # Streamlit 的静态资源文件(如 CSS、JS),用于前端界面渲染
("./你的虚拟环境路径/Lib/site-packages/streamlit/runtime", "./streamlit/runtime"), # Streamlit 的运行时模块,包含核心运行逻辑和组件
("icon.ico",".") # 应用程序图标文件,放置在输出根目录
],
hiddenimports=['xlrd'], # 隐式依赖
hookspath=['./hooks'], # 自定义 PyInstaller hooks 路径,用于处理特定模块的打包逻辑
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)

exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='run',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='icon.ico',
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='run',
)

重要配置

  • datas:声明所有静态资源和必要文件
  • hiddenimports:添加隐式导入的模块,打包后报错缺少的模块都添加在这里
  • hookspath:自定义 PyInstaller hooks 路径,用于处理特定模块的打包逻辑

5. 执行最终打包

1
pyinstaller run.spec --clean

完成上述步骤后,你将获得一个可以独立运行的 Streamlit 应用程序。

Nuitka 的安装及使用

安装步骤

  1. 使用 pip 安装 Nuitka:

    1
    pip install nuitka
  2. 下载并安装 C++ 编译器

使用指南

对于依赖较多第三方库的项目(如 torch, cv2, numpy, pandas, onnxruntime 等),**建议只将自定义代码转为 C++**,而保留这些大型第三方库。

示例项目结构

1
2
3
4
├─utils        // 源码 1 文件夹
├─src // 源码 2 文件夹
├─icon.ico // 图标
└─demo.py // 主文件

打包命令

使用以下命令生成 exe 文件(调试模式):

1
2
3
4
5
6
7
8
9
nuitka --standalone \
--nofollow-imports-to=torch,numpy,pandas,<所需忽略的包> \
--follow-import-to=utils,src \
--include-package=tqdm,xml,ctypes,logging,shapely,<一些必须一起打包才能正常运行的包> \
--output-dir=out \
--include-data-dir=./assets=assets \
--include-data-file=xxxx.pth=xxx.pth,<你要打包但不编译的文件> \
--windows-icon-from-ico=./icon.ico \
demo.py

参数说明

参数 说明
--standalone 生成独立的可执行文件,方便移植到其他机器,无需安装 Python
--nofollow-imports-to 指定不需要被编译的第三方库,通常是一些大型依赖库。多个包用逗号分隔
--follow-import-to 指定需要编译成 C++ 的源码文件夹或模块,这些通常是你自己编写的代码
--include-package 指定需要完整打包但不编译的包,这些包通常是运行必需但无法自动检测到的
--output-dir 指定编译输出的目录路径
--include-data-dir 指定需要打包的数据文件夹,格式为源路径=目标路径
--include-data-file 指定需要打包的单个数据文件,如模型文件、配置文件等,格式为源路径=目标路径
--windows-icon-from-ico 指定生成的可执行文件图标路径
--windows-disable-console 运行可执行文件时不弹出控制台窗口,适用于GUI程序
--enable-plugin 启用特定插件支持,如qt-plugins用于Qt应用
--show-progress 显示编译进度
--show-memory 显示编译过程的内存使用情况
--python-flag=no_site 禁用site模块,可以减少一些不必要的依赖

打包结果

经过约 1 分钟的编译后,目录结构如下:

1
2
3
4
5
6
7
8
├─utils        // 源码 1 文件夹
├─src // 源码 2 文件夹
├─out // 生成的 exe 文件夹
│ ├─demo.build // 生成的中间件,编译完成后可删除
│ └─demo.dist
│ └─demo.exe // 生成的 exe 文件
├─logo.ico // demo 的图标
└─demo.py // 主文件

🌟 Streamlit 应用打包指南

1. 创建启动脚本

由于 nuitka 无法直接启动 Streamlit 应用,是通过streamlit run <你的脚本>.py实现,因此在<你的脚本>.py后添加以下代码:

1
2
3
4
5
6
7
8
9
10
import streamlit.config
import streamlit.web.bootstrap
if __name__ == "__main__":
if "__streamlitmagic__" not in locals():
streamlit.config.set_option('server.fileWatcherType', 'none')
streamlit.config.set_option('server.enableCORS', True)
streamlit.config.set_option('server.enableXsrfProtection', False)
streamlit.config.set_option('global.developmentMode', False)
streamlit.config.set_option('client.toolbarMode', 'minimal')
streamlit.web.bootstrap.run(__file__, False, [], {})

2. nuitka打包

1
2
3
4
5
6
7
nuitka --standalone \
--nofollow-imports-to=streamlit \
--follow-import-to=. \
--output-dir=out \
--include-data-dir=./assets=assets \
--windows-icon-from-ico=./icon.ico \
helloworld.py

3.复制依赖库

  1. 运行helloworld.exe查看报错提示的缺失库
  2. 在everything下搜索对应环境的缺失库文件,复制到out/dist/文件夹下
    完成上述步骤后,你将获得一个可以独立运行的 Streamlit 应用程序。

常见问题及解决方案

第三方库缺失问题

  • 问题:运行生成的 exe 文件时,可能会报错:no module named torch, cv2
  • 解决方法:找到这些库的安装路径(例如 python3.7\Lib\site-packages),将对应的文件夹(如 numpy, cv2)复制到 demo.dist 目录下,其中该库的dll和dist都需要一起复制进来

完成上述步骤后,exe 文件即可正常运行!

总结

  • 如果你需要快速、高效地将 Python 项目打包为 exe 文件,Nuitka 是一个非常优秀的工具
  • 相比于 PyInstaller,Nuitka 在文件体积打包速度运行效率上都有显著优势

实用工具推荐

  • 将图片转为 .ico 格式的网站:比特虫

参考文献

  1. 将Streamlit应用打包成单个exe程序的几种方法
  2. Nuitka打包exe-2023疑难杂症综合版