目录
[什么是 PyInstaller?](#什么是 PyInstaller?)
[核心文件:.spec 文件](#核心文件:.spec 文件)
前言
本篇文章旨在完整的介绍如何用pyinstaller打包一个GUI项目,虽然网上不乏pyinstaller的介绍,但是能讲清楚,拿着项目实践的不多。其次,本篇文章亦可用来做个记录,备忘!仅此!
Pyinstaller简单介绍
关于pyinstaller的介绍网上很多,这里我就不过多赘述了,只做简单概念介绍。
什么是 PyInstaller?
PyInstaller 是一个将 Python 应用程序打包成独立可执行文件 的工具。它能够自动分析你的 Python 代码,检测所有 import 依赖(包括递归依赖),并将 Python 解释器、第三方库、数据文件等全部打包在一起。最终用户无需安装 Python 环境 ,双击 .exe 即可运行你的程序。
支持平台
PyInstaller 支持 Windows (8 及以上)、macOS 和 Linux 三大平台。在 Windows 上生成 .exe,在 macOS 上生成 .app bundle,在 Linux 上生成标准可执行文件。需要注意的是,不能跨平台打包------在 Windows 上只能打包 Windows 的 exe。
两种打包模式
PyInstaller 提供两种打包模式:
| 模式 | 参数 | 说明 | 适用场景 |
|---|---|---|---|
| 单目录模式 (onedir) | --onedir(默认) |
生成一个文件夹,包含 exe 和所有依赖文件 | 启动速度快,适合大型项目 |
| 单文件模式 (onefile) | --onefile 或 -F |
生成单个 exe 文件,运行时自解压到临时目录 | 分发方便,适合小工具 |
今天我们介绍的就是第一种,onedir模式!
工作原理
PyInstaller 的打包过程可以分为三个核心步骤:
- 依赖分析 (Analysis) :递归扫描入口脚本的所有
import语句,找到所有需要的模块和库文件。对于一些使用importlib等动态导入的模块,可能需要手动指定hiddenimports。 - 创建 Bootloader:生成一个引导加载程序,负责在运行时初始化 Python 解释器并加载你的代码。运行 exe 时实际上会启动两个进程------先是 bootloader,然后是你的 Python 程序。
- 打包输出 :将 Python 解释器、字节码、依赖库、数据文件等打包到
dist/目录下。
核心文件:.spec 文件
当你第一次运行 pyinstaller your_script.py 时,会自动生成一个 .spec 文件。这个文件本质上是一个 Python 脚本,定义了打包的所有配置------入口文件、隐藏导入、数据文件、排除项等。对于复杂项目,通常需要手动编辑 .spec 文件来精确控制打包行为。
我们今天的项目就是根据.spec文件的规则自定义的。
基本用法
# 安装
pip install pyinstaller
# 最简单的打包(单目录模式)
pyinstaller your_script.py
# 打包成单个 exe,不显示控制台窗口(适合 GUI 程序)
pyinstaller --onefile --windowed your_script.py
# 使用 .spec 文件打包(适合复杂项目)
pyinstaller your_build.spec
常见注意事项
- Hidden Imports :如果代码中有动态导入(如
__import__()、importlib.import_module()),PyInstaller 无法自动检测,需要通过--hidden-import参数或在.spec文件中手动指定。 - 数据文件 :非
.py的资源文件(如 xml 模板、图片、配置文件)需要通过--add-data参数或.spec文件的datas列表显式包含。 - 虚拟环境:建议在虚拟环境中运行 PyInstaller,确保只打包项目实际需要的依赖,避免打包体积过大。
打包的项目介绍
现在介绍一下我们今天要打包的项目,该项目是一个用于辅助5G NR测试的工具,主要功能是辅助生成NR的test plan,以及在NR测试中抓取电流数据等功能。
工具界面大概如下:

项目的架构示意图如下:
python
NR_Test_Helper_Tool_v0.2/
├── .venv/ # Python 虚拟环境(Tool运行的环境)
├── 3rdParty/
│ └── python-3.11.5.exe # Python 安装包
├── 3rdParty-PythonLibs/ # 第三方 .whl 库文件
├── lib/
│ ├── __init__.py
│ ├── frame.py # GUI 主入口
│ ├── log_helpers.py # 日志工具
│ ├── monitor.py # 连接监控
│ ├── powersupply.py # 直流电源控制 (PyVISA)
│ ├── serialext.py # 串口扩展
│ ├── utils.py # 通用工具函数
│ └── xml_handler.py # XML 转换处理
├── output/ # Log 输出目录
├── setup/ # 安装脚本 (.bat)
└── RunTool.bat # 启动入口 → 执行 lib/frame.py
该项目是一个用pysimpleGUI库编写的带界面的测试工具,入口是lib/frame.py
该工具已经完成测试,其实直接通过RunTool.bat打开也是可以的;但是现在为了便于加密和分发,所以需要将整个工具进行打包成exe的形式。故而考虑用pyinstaller进行打包!
打包前准备
打包前代码检查
在对项目代码打包前需要注意以下问题:
2.相对导入 使用import时需要显示导入,不要用from xx import * 这些用法
Pyinstaller配置文件(build.spec)准备
先准备一个build.spec 文件, 该文件是PyInstaller 打包的配置文件,本质上就是一个 Python 脚本。PyInstaller 读取它来决定"怎么打包"。整个流程可以理解为一条流水线:
python
Analysis(分析依赖)→ PYZ(打包字节码)→ EXE(生成exe)→ COLLECT(收集到文件夹)
下面是build.spec脚本内容,仔细看注释,注释对每个部分做了详细的解释:
python
# -*- mode: python ; coding: utf-8 -*-
import os
import sys
from PyInstaller.utils.hooks import collect_submodules, collect_data_files
# ============================================================
# 1. 基本路径
# ============================================================
# 作用:后面所有路径都基于这两个变量,避免硬编码路径
SPEC_DIR = os.path.abspath(SPECPATH) # 项目根目录,内置变量 SPECPATH 的值就是 .spec 文件所在的目录路径
LIB_DIR = os.path.join(SPEC_DIR, 'lib')
# ============================================================
# 2. 收集 lib/ 下所有 .py 模块作为 hidden imports, 确保 frame.py 的同级 import 都能被找到
# 为什么需要这个? PyInstaller 从 frame.py 入口开始分析 import,但如果你的代码用了动态导入(如 # importlib 或 __import__),PyInstaller 就检测不到。这里相当于"兜底",把 lib/ 下所有 .py 都告诉 PyInstaller
# ============================================================
lib_modules = []
for f in os.listdir(LIB_DIR):
if f.endswith('.py') and f != '__init__.py':
mod_name = f[:-3] # 去掉 .py
lib_modules.append(mod_name)
print(f"[build.spec] Found lib modules: {lib_modules}")
# ============================================================
# 3. 第三方库 hidden imports (最关键的部分)
# PyInstaller 有时无法自动检测的模块
# 作用:手动在这里"告诉" PyInstaller:这些模块你也得打包进来
# ============================================================
hidden_imports = [
# --- lib/ 下的自定义模块 ---
*lib_modules,
# --- PySimpleGUI ---
'PySimpleGUI',
# --- PyVISA 后端 ---
'pyvisa',
'pyvisa_py', # 如果用纯 Python 后端
'pyvisa.resources',
'pyvisa.resources.tcpip',
'pyvisa.resources.gpib',
'pyvisa.resources.serial',
# --- pywinauto ---
'pywinauto',
'pywinauto.application',
'pywinauto.timings',
'pywinauto.controls',
'pywinauto.controls.win32_controls',
'pywinauto.win32_hooks',
# --- serial ---
'serial',
'serial.tools',
'serial.tools.list_ports',
'serial.tools.list_ports_windows',
# --- PIL / Pillow ---
'PIL',
'PIL.Image',
'PIL.ImageDraw',
# --- logging ---
'colorlog',
'logging.handlers',
# --- 其他 ---
'csv',
'io',
'base64',
'pathlib',
'threading',
'dataclasses',
]
# ============================================================
# 4. 数据文件
# 如果 lib/ 下有非 .py 的配置文件/模板,也要带进去
# 将 lib/ 下所有 .py 文件作为数据文件也复制一份到根目录
# (因为 frame.py 用的是 from xxx import 的方式,
# PyInstaller 会把它们编译进 exe,这里只是备份)
# 如果 lib/ 下有 .json / .xml / .ini 等配置文件,取消下面代码段的注释:
# ============================================================
added_datas = []
# for ext in ['*.json', '*.xml', '*.ini', '*.yaml', '*.yml']:
# import glob
# for fp in glob.glob(os.path.join(LIB_DIR, ext)):
# added_datas.append((fp, '.'))
# ============================================================
# 5. Analysis(核心)
# 参数解释 作用
# '第一个参数' 入口脚本,就是你的 frame.py
# 'pathex' 告诉 PyInstaller 去哪里找模块(相当于 sys.path)
# 'binaries' 需要额外打包的二进制文件(.dll 等)
# 'datas' 非代码的数据文件
# 'hiddenimports' 手动指定的隐藏依赖
# 'excludes' 排除不需要的库,减小打包体积
# ============================================================
a = Analysis(
# 入口脚本
[os.path.join(LIB_DIR, 'frame.py')],
pathex=[LIB_DIR, SPEC_DIR],
binaries=[],
datas=added_datas,
hiddenimports=hidden_imports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[
'matplotlib', # 如果没用到,排除以减小体积
'numpy', # 如果没用到,排除以减小体积
'scipy',
'pandas',
# 'tkinter', # PySimpleGUI 基于 tkinter,不要排除!
# 'unittest',
# 'test',
],
noarchive=False,
optimize=0,
)
# ============================================================
# 6. 注意:PySimpleGUI 依赖 tkinter,不能排除 tkinter!
# 从 excludes 中移除 tkinter
# ============================================================
# 上面 excludes 中的 'tkinter' 已注释掉
# ============================================================
# 7. PYZ - 打包 Python 字节码
# 把所有 Python 模块编译成 .pyc 字节码,压缩打包成一个 .pyz 归档文件
# ============================================================
pyz = PYZ(a.pure, a.zipped_data)
# ============================================================
# 8. EXE
# ============================================================
exe = EXE(
pyz,
a.scripts,
[], # onedir 模式,不把所有东西塞进 exe
exclude_binaries=True, # onedir 模式
name='WNC_NR_Test_Helper_Tool',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True, # 如果安装了 UPX,可以压缩
console=False, # False = 不显示控制台窗口(GUI 程序)
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
# icon='your_icon.ico', # 如果有图标文件,取消注释并指定路径
)
# ============================================================
# 9. COLLECT - onedir 模式,把 exe + 所有依赖文件收集到文件夹中
# ============================================================
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='WNC_NR_Test_Helper_Tool',
)
pyinstaller打包的整体流程可以总结如下:
python
frame.py (入口)
│
▼
Analysis ──→ 分析所有 import 依赖
│ + hiddenimports 补充
│ - excludes 排除
▼
PYZ ──────→ 编译 .py → .pyc,压缩打包
│
▼
EXE ──────→ 生成 .exe 主程序
│
▼
COLLECT ───→ 收集 exe + dll + 数据文件 → dist/文件夹
打包脚本(build.bat)准备
所谓的打包脚本build.bat就是在python环境中运行pyinstaller调用buid.spec来进行打包。
由于执行pyinstaller也是需要python环境的,本项目就直接利用项目根目录的 .venv
也可以在其他python环境中运行pyinstaller 。
针对我的项目,为了简化类似结构的项目打包工作,所以编写了一个打包脚本build.bat,简化打包流程。
bat中打包步骤按照如下流程进行:
python
开始
│
├── 1. 设置编码 & 获取路径
├── 2. 检查 .venv → 不存在则报错退出(打包也是需要python环境的,直接利用Tool根目录的虚拟环境)
├── 3. 激活虚拟环境
├── 4. 检查 PyInstaller → 没有则自动安装
├── 5. 清理旧的 build/ 和 dist/
├── 6. 执行 pyinstaller 打包(基于上面定义配置文件build.spec)
└── 7. 提示成功
完整的build.bat内容如下:
bash
@echo off
chcp 65001 >nul 2>&1
title NR Test Helper Tool - Build
REM ============================================
REM Get project root (where this .bat lives)
REM ============================================
set "PROJECT_ROOT=%~dp0"
set "PROJECT_ROOT=%PROJECT_ROOT:~0,-1%"
REM ============================================
REM Activate virtual environment
REM ============================================
if not exist "%PROJECT_ROOT%\.venv\Scripts\activate.bat" (
echo [ERROR] .venv not found! Please run setup\Install.bat first.
pause
exit /b 1
)
call "%PROJECT_ROOT%\.venv\Scripts\activate.bat"
REM ============================================
REM Install PyInstaller if not installed
REM ============================================
pip show pyinstaller >nul 2>&1
if errorlevel 1 (
echo [INFO] Installing PyInstaller...
pip install pyinstaller
if errorlevel 1 (
echo [ERROR] Failed to install PyInstaller.
pause
exit /b 1
)
)
REM ============================================
REM Clean previous build
REM ============================================
echo.
echo [1/2] Cleaning previous build...
if exist "%PROJECT_ROOT%\build" rmdir /s /q "%PROJECT_ROOT%\build"
if exist "%PROJECT_ROOT%\dist" rmdir /s /q "%PROJECT_ROOT%\dist"
REM ============================================
REM Build EXE (using build.spec)
REM ============================================
echo [2/2] Building EXE...
echo.
pyinstaller --noconfirm --clean ^
--distpath "%PROJECT_ROOT%\dist" ^
--workpath "%PROJECT_ROOT%\build" ^
"%PROJECT_ROOT%\build.spec"
if errorlevel 1 (
echo.
echo ============================================
echo BUILD FAILED! Check the log above.
echo ============================================
pause
exit /b 1
)
echo.
echo ============================================
echo BUILD SUCCESS!
echo Output: dist\WNC_NR_Test_Helper_Tool\
echo ============================================
echo.
pause
该打包脚本build.bat的核心是这段:
python
pyinstaller --noconfirm --clean --distpath "%PROJECT_ROOT%\dist" --workpath "%PROJECT_ROOT%\build" "%PROJECT_ROOT%\build.spec"
# 参数 作用
# --noconfirm 覆盖已有输出时不询问确认
# --clean 打包前清理 PyInstaller 缓存
# --distpath 指定输出 exe 的目录 → dist/
# --workpath 指定临时编译文件目录 → build/
# build.spec 使用 .spec 配置文件打包(里面定义了入口文件、附加数据、图标等所有打包细节)
开始打包
把build.bat 和 build.spec 两个文件放入要打包的项目的根目录,双击build.bat脚本即可开始打包,等待数十秒后即可打包成功:

打包成功后项目根目录会自动常见一个dist目录,dist目录中便是生成的exe文件

_internal中是exe运行依赖的文件(包含很多编译过的.pyd文件),其中 xx_Helper_Tool.exe 就是最终的exe文件。双击该.exe文件即可成功运行测试工具!
由于我们是使用Onedir模式打包的,所以分发软件时需要将exe 和 _internal文件夹一起分发。
结语
因为我的工具运行时需读写 output/ 目录的 log,而且依赖较多,体积大,所以最终使用pyinstaller的onedir模式,这样启动更快,且看着更直观! 后面有遇到小工具开发的项目再分享一篇onefile 模式的打包教程!
需要技术交流的伙伴可以直接私聊!