pyinstaller打包GUI项目实践

目录

前言

Pyinstaller简单介绍

[什么是 PyInstaller?](#什么是 PyInstaller?)

支持平台

两种打包模式

工作原理

[核心文件:.spec 文件](#核心文件:.spec 文件)

基本用法

常见注意事项

打包的项目介绍

打包前准备

打包前代码检查

Pyinstaller配置文件(build.spec)准备

打包脚本(build.bat)准备

开始打包

结语


前言

本篇文章旨在完整的介绍如何用pyinstaller打包一个GUI项目,虽然网上不乏pyinstaller的介绍,但是能讲清楚,拿着项目实践的不多。其次,本篇文章亦可用来做个记录,备忘!仅此!

Pyinstaller简单介绍

关于pyinstaller的介绍网上很多,这里我就不过多赘述了,只做简单概念介绍。

什么是 PyInstaller?

PyInstaller 是一个将 Python 应用程序打包成独立可执行文件 的工具。它能够自动分析你的 Python 代码,检测所有 import 依赖(包括递归依赖),并将 Python 解释器、第三方库、数据文件等全部打包在一起。最终用户无需安装 Python 环境 ,双击 .exe 即可运行你的程序。

支持平台

PyInstaller 支持 Windows (8 及以上)、macOSLinux 三大平台。在 Windows 上生成 .exe,在 macOS 上生成 .app bundle,在 Linux 上生成标准可执行文件。需要注意的是,不能跨平台打包------在 Windows 上只能打包 Windows 的 exe。

两种打包模式

PyInstaller 提供两种打包模式:

模式 参数 说明 适用场景
单目录模式 (onedir) --onedir(默认) 生成一个文件夹,包含 exe 和所有依赖文件 启动速度快,适合大型项目
单文件模式 (onefile) --onefile-F 生成单个 exe 文件,运行时自解压到临时目录 分发方便,适合小工具

今天我们介绍的就是第一种,onedir模式!

工作原理

PyInstaller 的打包过程可以分为三个核心步骤:

  1. 依赖分析 (Analysis) :递归扫描入口脚本的所有 import 语句,找到所有需要的模块和库文件。对于一些使用 importlib 等动态导入的模块,可能需要手动指定 hiddenimports
  2. 创建 Bootloader:生成一个引导加载程序,负责在运行时初始化 Python 解释器并加载你的代码。运行 exe 时实际上会启动两个进程------先是 bootloader,然后是你的 Python 程序。
  3. 打包输出 :将 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 模式的打包教程!

需要技术交流的伙伴可以直接私聊!

相关推荐
qq_189807032 小时前
c++怎么解决ifstream在读取UTF-16文件时的乱码_imbue用法【避坑】
jvm·数据库·python
青苔猿猿2 小时前
【3】jupyter单元格Cell操作
python·jupyter·单元格
2301_777599372 小时前
Golang怎么实现微服务日志聚合_Golang如何将多个服务的日志统一收集到一处查询【指南】
jvm·数据库·python
Vanranrr2 小时前
从图形化到命令行:一次 SVN 工程化能力补齐与流程治理实践(Windows)
windows·svn
花间相见2 小时前
【AI私人家庭医生day01】—— 项目介绍
大数据·linux·人工智能·python·flask·conda·ai编程
m0_747854522 小时前
MySQL如何缓解热点数据的更新瓶颈_合并更新请求与排队控制
jvm·数据库·python
zhangchaoxies2 小时前
React Flow 边缘丢失与错位问题的根源及 Hooks 重构方案
jvm·数据库·python
Wyz201210242 小时前
如何在 React 中正确绑定 onClick 事件避免字符串赋值错误
jvm·数据库·python
m0_377618232 小时前
如何在 Node.js 服务器间正确配置 CORS 实现跨子域资源访问
jvm·数据库·python