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 及以上)、macOS 和 Linux 三大平台。在 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 模式的打包教程!

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

相关推荐
BD_Marathon26 分钟前
消息对象中字段的说明
java·前端·python
Java后端的Ai之路32 分钟前
Python进阶探索23 - Python中的Time与Datetime模块
开发语言·数据库·python·datetime·time
Java后端的Ai之路1 小时前
Python进阶探索24_应用案例_linux系统的监控
linux·开发语言·python·应用·探索
happylifetree1 小时前
Python22-26:核心语法-流程控制语句-if条件判断
python
程序员清风2 小时前
PydanticAI 实战:用类型安全构建可靠的 Python Agent
开发语言·python·安全
梦想画家2 小时前
SQLMesh Python 模型入门(三):前后置语句、蓝图建模与避坑指南
大数据·python·sqlmesh
迅猛龙办公室2 小时前
Python实现绘制同切圆
开发语言·python
Vex2une3 小时前
Windows 任务管理器发展史:从 80KB 救急工具到系统级监控平台
windows·进程管理·性能监控·任务管理器·winui·cpu使用率·taskmgr
言乐63 小时前
Python语音检索
开发语言·python·django·virtualenv·pygame
泡茶喝茶写代码3 小时前
A股量化数据工程:从 REST 接口到策略信号(第 1 篇):指数列表与实时行情接入
java·python·股票数据api·股票数据·股票数据api接口·股票api数据接口·股票量化数据接口