pyInstaller-将python打包成 EXE的终极方案

0 阅读29分钟

一、概念

PyInstaller 是一个将 Python 应用程序打包成独立可执行文件的工具,支持 Windows、Linux、macOS 等多平台。它能将 Python 脚本及其依赖项打包成单一的可执行文件,无需目标机器安装 Python 环境。它的特点:

跨平台支持:Windows (.exe)、Linux、macOS (.app)

自动依赖分析:自动检测并打包所需的库和模块

单文件模式:可打包成单个 exe 文件

隐藏源码:保护 Python 源代码

支持第三方库:NumPy、Pandas、PyQt、Tkinter 等

自定义图标:支持自定义应用程序图标

无需修改代码:原生 Python 代码无需改动

1.2 工作原理

Python源码 → PyInstaller分析 → 收集依赖 → 生成引导程序 → 打包资源 → 生成EXE

打包流程:

分析阶段:扫描 Python 脚本,识别所有导入的模块

收集阶段:收集 Python 解释器、依赖库、资源文件

打包阶段:将所有文件打包成可执行文件

引导阶段:创建引导程序,在运行时解压并执行

1.3 打包模式对比

特性单文件模式 (--onefile)单目录模式 (--onedir)
文件数量1个exe文件1个文件夹(含多个文件)
启动速度较慢(需解压)快速
体积较大相对较小
分发方便需打包整个文件夹
调试困难容易
推荐场景简单工具、单机应用复杂应用、企业软件

二、PyInstaller 功能详解

2.1 核心功能

  1. 自动依赖检测

自动分析 import 语句

检测隐式依赖(动态导入)

支持第三方库的钩子(hooks)

  1. 资源文件打包

图片、音频、视频

配置文件(JSON、YAML、INI)

数据文件(CSV、Excel)

DLL 文件、字体文件

  1. 代码保护

将 .py 编译为 .pyc 字节码

打包进可执行文件,难以反编译

支持加密(需额外配置)

  1. 自定义配置

.spec 文件:高级配置

排除不需要的模块

添加隐藏导入

自定义图标和版本信息

2.2 支持的应用类型

应用类型支持程度注意事项
命令行工具✅ 完美支持最简单
GUI应用(Tkinter)✅ 完美支持需注意资源文件路径
GUI应用(PyQt/PySide)✅ 完美支持体积较大
Web应用(Flask/Django)⚠️ 部分支持需特殊配置
数据科学应用✅ 支持NumPy/Pandas体积大
游戏(Pygame)✅ 支持需打包资源文件

三、安装与环境配置

3.1 安装 PyInstaller

方法1:使用 pip 安装(推荐)


pip install pyinstaller

方法2:安装最新开发版

pip install github.com/pyinstaller…

方法3:从源码安装

git clone github.com/pyinstaller…

cd pyinstaller

python setup.py install

验证安装

pyinstaller --version

3.2 环境准备

创建虚拟环境(推荐)

python -m venv myenv

激活虚拟环境# Windows:

myenv\Scripts\activate# Linux/Mac:

source myenv/bin/activate

安装项目依赖

pip install -r requirements.txt

安装 PyInstaller

pip install pyinstaller

3.3 依赖检查工具

"""

检查项目依赖的工具脚本

"""

111

import pkg_resourcesimport subprocess

def check_dependencies():

    """检查已安装的包"""

    installed_packages = [d.project_name for d in pkg_resources.working_set]

    print("已安装的包:")

    for package in sorted(installed_packages):

        print(f"  - {package}")

    def generate_requirements():

    """生成 requirements.txt"""

    result = subprocess.run(['pip', 'freeze'], capture_output=True, text=True)

    with open('requirements.txt', 'w') as f:

        f.write(result.stdout)

    print("requirements.txt 已生成")

def check_import_errors(script_path):

    """检查脚本的导入错误"""

    try:

        with open(script_path, 'r', encoding='utf-8') as f:

            code = f.read()

        compile(code, script_path, 'exec')

        print(f"✅ {script_path} 语法检查通过")

    except SyntaxError as e:

        print(f"❌ 语法错误: {e}")

    except Exception as e:

        print(f"❌ 其他错误: {e}")

if name == 'main':

    check_dependencies()

    generate_requirements()

四、基础打包步骤

4.1 简单示例程序

hello.py - 简单的命令行程序"""

简单的 Hello World 程序

"""

import sysimport datetime

def main():

    print("=" * 50)

    print("欢迎使用 Python 打包程序")

    print("=" * 50)

    

    name = input("请输入你的名字: ")

    current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    

    print(f"\n你好, {name}!")

    print(f"当前时间: {current_time}")

    print(f"Python 版本: {sys.version}")

    

    input("\n按回车键退出...")

if name == 'main':

    main()

4.2 基础打包命令

1. 最简单的打包(生成文件夹)

pyinstaller hello.py

2. 打包成单个exe文件

pyinstaller --onefile hello.py

3. 打包时不显示控制台窗口(GUI程序)

pyinstaller --onefile --noconsole hello.py

4. 自定义输出目录和名称

pyinstaller --onefile --name MyApp hello.py

5. 添加图标

pyinstaller --onefile --icon=app.ico hello.py

6. 完整命令示例

pyinstaller --onefile --noconsole --icon=app.ico --name MyApp hello.py

4.3 打包后的目录结构

项目目录/

├── hello.py              # 源代码

├── app.ico              # 图标文件

├── build/               # 构建临时文件

│   └── MyApp/

│       ├── Analysis-00.toc

│       ├── PYZ-00.pyz

│       └── ...

├── dist/                # 最终输出目录

│   └── MyApp.exe        # 可执行文件

└── MyApp.spec           # 配置文件

五、打包选项详解

5.1 常用选项速查表

选项说明示例
--onefile / -F打包成单个文件pyinstaller -F app.py
--onedir / -D打包成文件夹(默认)pyinstaller -D app.py
--noconsole / -w不显示控制台(GUI)pyinstaller -w app.py
--console / -c显示控制台(默认)pyinstaller -c app.py
--icon= / -i设置图标pyinstaller -i app.ico app.py
--name= / -n设置程序名称pyinstaller -n MyApp app.py
--add-data添加数据文件--add-data "data;data"
--add-binary添加二进制文件--add-binary "lib.dll;."
--hidden-import添加隐藏导入--hidden-import=requests
--exclude-module排除模块--exclude-module=pytest
--clean清理临时文件pyinstaller --clean app.py
--distpath指定输出目录--distpath ./output
--workpath指定工作目录--workpath ./build
--specpath指定spec文件路径--specpath ./specs

5.2 资源文件打包

calculator.py - 带资源文件的计算器程序"""

简单计算器程序(带图标和配置文件)

"""

import tkinter as tk

from tkinter import ttk

import json

import os

import sys

def resource_path(relative_path):

    """

    获取资源文件的绝对路径

    PyInstaller打包后,资源文件会被解压到临时目录

    """

    try:

        # PyInstaller创建临时文件夹,路径存储在 _MEIPASS

        base_path = sys._MEIPASS

    except Exception:

        base_path = os.path.abspath(".")

    

    return os.path.join(base_path, relative_path)

class Calculator:

    def init(self, root):

        self.root = root

        self.root.title("简单计算器")

        

        # 加载配置文件

        self.load_config()

        

        # 设置图标

        try:

            icon_path = resource_path('icon.ico')

            self.root.iconbitmap(icon_path)

        except:

            pass

        

        self.create_widgets()

    

    def load_config(self):

        """加载配置文件"""

        try:

            config_path = resource_path('config.json')

            with open(config_path, 'r', encoding='utf-8') as f:

                self.config = json.load(f)

        except:

            self.config = {'theme': 'default', 'precision': 2}

    

    def create_widgets(self):

        """创建界面组件"""

        # 显示框

        self.display = tk.Entry(self.root, width=30, font=('Arial', 14),

                               justify='right')

        self.display.grid(row=0, column=0, columnspan=4, padx=10, pady=10)

        

        # 按钮布局

        buttons = [

            '7', '8', '9', '/',

            '4', '5', '6', '*',

            '1', '2', '3', '-',

            '0', '.', '=', '+'

        ]

        

        row = 1

        col = 0

        for button in buttons:

            cmd = lambda x=button: self.click(x)

            tk.Button(self.root, text=button, width=5, height=2,

                     command=cmd).grid(row=row, column=col, padx=5, pady=5)

            col += 1

            if col > 3:

                col = 0

                row += 1

        

        # 清除按钮

        tk.Button(self.root, text='C', width=5, height=2,

                 command=self.clear).grid(row=row, column=0, padx=5, pady=5)

    

    def click(self, key):

        """按钮点击事件"""

        if key == '=':

            try:

                result = eval(self.display.get())

                self.display.delete(0, tk.END)

                self.display.insert(0, str(result))

            except:

                self.display.delete(0, tk.END)

                self.display.insert(0, "错误")

        else:

            self.display.insert(tk.END, key)

    

    def clear(self):

        """清除显示"""

        self.display.delete(0, tk.END)

def main():

    root = tk.Tk()

    app = Calculator(root)

    root.mainloop()

if name == 'main':

    main()

配置文件 config.json:

{

    "theme": "light",

    "precision": 2,

    "window_size": "300x400"}

打包命令(Windows):

方式1:命令行指定资源文件

pyinstaller --onefile --noconsole ^

    --icon=icon.ico ^

    --add-data "config.json;." ^

    --add-data "icon.ico;." ^

    --name Calculator ^

    calculator.py

方式2:Linux/Mac(使用冒号分隔)

pyinstaller --onefile --noconsole \

    --icon=icon.ico \

    --add-data "config.json:." \

    --add-data "icon.ico:." \

    --name Calculator \

    calculator.py

5.3 使用 .spec 文件高级配置

Calculator.spec - 高级配置文件# -- mode: python ; coding: utf-8 --

block_cipher = None

分析阶段配置

a = Analysis(

    ['calculator.py'],                    # 主脚本

    pathex=[],                            # 额外的搜索路径

    binaries=[],                          # 二进制文件

    datas=[                               # 数据文件

        ('config.json', '.'),

        ('icon.ico', '.'),

        ('resources/*', 'resources'),     # 整个文件夹

    ],

    hiddenimports=[                       # 隐藏导入

        'tkinter',

        'json',

    ],

    hookspath=[],                         # 自定义钩子路径

    hooksconfig={},

    runtime_hooks=[],                     # 运行时钩子

    excludes=[                            # 排除的模块

        'matplotlib',

        'numpy',

        'pandas',

    ],

    win_no_prefer_redirects=False,

    win_private_assemblies=False,

    cipher=block_cipher,

    noarchive=False,

)

PYZ 打包

pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

EXE 配置

exe = EXE(

    pyz,

    a.scripts,

    a.binaries,

    a.zipfiles,

    a.datas,

    [],

    name='Calculator',                    # 程序名称

    debug=False,                          # 调试模式

    bootloader_ignore_signals=False,

    strip=False,

    upx=True,                             # 使用UPX压缩

    upx_exclude=[],

    runtime_tmpdir=None,

    console=False,                        # 不显示控制台

    disable_windowed_traceback=False,

    argv_emulation=False,

    target_arch=None,

    codesign_identity=None,

    entitlements_file=None,

    icon='icon.ico',                      # 图标

    version='version.txt',                # 版本信息文件

)

使用 .spec 文件打包:

生成 .spec 文件

pyinstaller --onefile --noconsole calculator.py

修改 .spec 文件后重新打包

pyinstaller Calculator.spec

清理后重新打包

pyinstaller --clean Calculator.spec

5.4 版本信息配置

version.txt - Windows版本信息# UTF-8## 用于 --version-file 参数

VSVersionInfo(

  ffi=FixedFileInfo(

    filevers=(1, 0, 0, 0),

    prodvers=(1, 0, 0, 0),

    mask=0x3f,

    flags=0x0,

    OS=0x40004,

    fileType=0x1,

    subtype=0x0,

    date=(0, 0)

  ),

  kids=[

    StringFileInfo(

      [

      StringTable(

        u'040904B0',

        [StringStruct(u'CompanyName', u'我的公司'),

        StringStruct(u'FileDescription', u'简单计算器'),

        StringStruct(u'FileVersion', u'1.0.0.0'),

        StringStruct(u'InternalName', u'Calculator'),

        StringStruct(u'LegalCopyright', u'Copyright © 2025'),

        StringStruct(u'OriginalFilename', u'Calculator.exe'),

        StringStruct(u'ProductName', u'计算器'),

        StringStruct(u'ProductVersion', u'1.0.0.0')])

      ]

    ),

    VarFileInfo([VarStruct(u'Translation', [1033, 1200])])

  ]

)

生成版本信息文件:

安装 pefile 库

pip install pefile

使用版本信息打包

pyinstaller --onefile --noconsole --icon=icon.ico --version-file=version.txt calculator.py

六、实战案例

6.1 案例1:命令行工具打包

file_manager.py - 文件管理工具"""

命令行文件管理工具

功能:批量重命名、文件搜索、文件统计

"""

import os

import sys

import argparse

from pathlib import Pat

himport json

class FileManager:

    def init(self):

        self.config_file = 'config.json'

        self.load_config()

    

    def load_config(self):

        """加载配置"""

        if os.path.exists(self.config_file):

            with open(self.config_file, 'r', encoding='utf-8') as f:

                self.config = json.load(f)

        else:

            self.config = {'last_path': '.'}

    

    def save_config(self):

        """保存配置"""

        with open(self.config_file, 'w', encoding='utf-8') as f:

            json.dump(self.config, f, indent=2, ensure_ascii=False)

    

    def batch_rename(self, directory, pattern, replacement):

        """

        批量重命名文件

        

        Args:

            directory: 目标目录

            pattern: 要替换的模式

            replacement: 替换后的文本

        """

        count = 0

        for file in Path(directory).iterdir():

            if file.is_file() and pattern in file.name:

                new_name = file.name.replace(pattern, replacement)

                new_path = file.parent / new_name

                file.rename(new_path)

                print(f"✅ {file.name} → {new_name}")

                count += 1

        

        print(f"\n共重命名 {count} 个文件")

        return count

    

    def search_files(self, directory, keyword, extension=None):

        """

        搜索文件

        

        Args:

            directory: 搜索目录

            keyword: 关键词

            extension: 文件扩展名(可选)

        """

        results = []

        for root, dirs, files in os.walk(directory):

            for file in files:

                if keyword.lower() in file.lower():

                    if extension is None or file.endswith(extension):

                        full_path = os.path.join(root, file)

                        size = os.path.getsize(full_path)

                        results.append({

                            'path': full_path,

                            'name': file,

                            'size': size

                        })

        

        return results

    

    def file_statistics(self, directory):

        """

        文件统计

        

        Args:

            directory: 统计目录

        """

        stats = {

            'total_files': 0,

            'total_size': 0,

            'extensions': {}

        }

        

        for root, dirs, files in os.walk(directory):

            for file in files:

                stats['total_files'] += 1

                

                full_path = os.path.join(root, file)

                size = os.path.getsize(full_path)

                stats['total_size'] += size

                

                ext = Path(file).suffix.lower()

                if ext:

                    if ext not in stats['extensions']:

                        stats['extensions'][ext] = {'count': 0, 'size': 0}

                    stats['extensions'][ext]['count'] += 1

                    stats['extensions'][ext]['size'] += size

        

        return stats

    

    def format_size(self, size):

        """格式化文件大小"""

        for unit in ['B', 'KB', 'MB', 'GB', 'TB']:

            if size < 1024.0:

                return f"{size:.2f} {unit}"

            size /= 1024.0

        return f"{size:.2f} PB"

def main():

    parser = argparse.ArgumentParser(description='文件管理工具')

    subparsers = parser.add_subparsers(dest='command', help='子命令')

    

    # 批量重命名命令

    rename_parser = subparsers.add_parser('rename', help='批量重命名')

    rename_parser.add_argument('directory', help='目标目录')

    rename_parser.add_argument('pattern', help='要替换的模式')

    rename_parser.add_argument('replacement', help='替换后的文本')

    

    # 搜索命令

    search_parser = subparsers.add_parser('search', help='搜索文件')

    search_parser.add_argument('directory', help='搜索目录')

    search_parser.add_argument('keyword', help='关键词')

    search_parser.add_argument('--ext', help='文件扩展名')

    

    # 统计命令

    stats_parser = subparsers.add_parser('stats', help='文件统计')

    stats_parser.add_argument('directory', help='统计目录')

    

    args = parser.parse_args()

    

    if not args.command:

        parser.print_help()

        return

    

    fm = FileManager()

    

    if args.command == 'rename':

        fm.batch_rename(args.directory, args.pattern, args.replacement)

    

    elif args.command == 'search':

        results = fm.search_files(args.directory, args.keyword, args.ext)

        print(f"\n找到 {len(results)} 个文件:\n")

        for r in results:

            print(f"�� {r['name']}")

            print(f"   路径: {r['path']}")

            print(f"   大小: {fm.format_size(r['size'])}\n")

    

    elif args.command == 'stats':

        stats = fm.file_statistics(args.directory)

        print("\n文件统计结果:")

        print("=" * 60)

        print(f"总文件数: {stats['total_files']}")

        print(f"总大小: {fm.format_size(stats['total_size'])}")

        print("\n按扩展名统计:")

        print("-" * 60)

        

        sorted_exts = sorted(stats['extensions'].items(),

                           key=lambda x: x[1]['size'], reverse=True)

        

        for ext, info in sorted_exts:

            print(f"{ext:15} {info['count']:6} 个文件  "

                  f"{fm.format_size(info['size']):>12}")

if name == 'main':

    main()

打包命令:

pyinstaller --onefile --name FileManager file_manager.py

使用示例:

批量重命名

FileManager.exe rename ./photos "IMG" "Photo"

搜索文件

FileManager.exe search ./documents "report" --ext .pdf

文件统计

FileManager.exe stats ./projects

6.2 案例2:PyQt5 GUI 应用打包

todo_app.py - 待办事项管理器(PyQt5)"""

待办事项管理器

功能:添加、删除、标记完成、保存到文件

"""import sysimport jsonfrom datetime import datetimefrom PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,

                            QHBoxLayout, QLineEdit, QPushButton, QListWidget,

                            QListWidgetItem, QMessageBox, QLabel)from PyQt5.QtCore import Qtfrom PyQt5.QtGui import QIconimport os

def resource_path(relative_path):

    """获取资源文件路径"""

    try:

        base_path = sys._MEIPASS

    except Exception:

        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)

class TodoApp(QMainWindow):

    def init(self):

        super().init()

        self.data_file = 'todos.json'

        self.todos = []

        self.init_ui()

        self.load_todos()

    

    def init_ui(self):

        """初始化界面"""

        self.setWindowTitle('待办事项管理器')

        self.setGeometry(100, 100, 500, 600)

        

        # 设置图标

        try:

            icon_path = resource_path('app.ico')

            self.setWindowIcon(QIcon(icon_path))

        except:

            pass

        

        # 中央部件

        central_widget = QWidget()

        self.setCentralWidget(central_widget)

        

        # 主布局

        layout = QVBoxLayout()

        

        # 标题

        title = QLabel('�� 我的待办事项')

        title.setStyleSheet('font-size: 24px; font-weight: bold; padding: 10px;')

        title.setAlignment(Qt.AlignCenter)

        layout.addWidget(title)

        

        # 输入区域

        input_layout = QHBoxLayout()

        self.input_field = QLineEdit()

        self.input_field.setPlaceholderText('输入新的待办事项...')

        self.input_field.returnPressed.connect(self.add_todo)

        

        add_btn = QPushButton('添加')

        add_btn.clicked.connect(self.add_todo)

        add_btn.setStyleSheet('background-color: #4CAF50; color: white; padding: 8px;')

        

        input_layout.addWidget(self.input_field)

        input_layout.addWidget(add_btn)

        layout.addLayout(input_layout)

        

        # 列表区域

        self.list_widget = QListWidget()

        self.list_widget.itemDoubleClicked.connect(self.toggle_todo)

        layout.addWidget(self.list_widget)

        

        # 按钮区域

        button_layout = QHBoxLayout()

        

        delete_btn = QPushButton('删除选中')

        delete_btn.clicked.connect(self.delete_todo)

        delete_btn.setStyleSheet('background-color: #f44336; color: white; padding: 8px;')

        

        clear_btn = QPushButton('清空已完成')

        clear_btn.clicked.connect(self.clear_completed)

        clear_btn.setStyleSheet('background-color: #FF9800; color: white; padding: 8px;')

        

        button_layout.addWidget(delete_btn)

        button_layout.addWidget(clear_btn)

        layout.addLayout(button_layout)

        

        # 统计信息

        self.stats_label = QLabel()

        self.stats_label.setAlignment(Qt.AlignCenter)

        self.stats_label.setStyleSheet('padding: 10px; color: #666;')

        layout.addWidget(self.stats_label)

        

        central_widget.setLayout(layout)

        self.update_stats()

    

    def add_todo(self):

        """添加待办事项"""

        text = self.input_field.text().strip()

        if text:

            todo = {

                'text': text,

                'completed': False,

                'created_at': datetime.now().isoformat()

            }

            self.todos.append(todo)

            self.refresh_list()

            self.input_field.clear()

            self.save_todos()

    

    def delete_todo(self):

        """删除选中的待办事项"""

        current_row = self.list_widget.currentRow()

        if current_row >= 0:

            del self.todos[current_row]

            self.refresh_list()

            self.save_todos()

    

    def toggle_todo(self, item):

        """切换完成状态"""

        row = self.list_widget.row(item)

        self.todos[row]['completed'] = not self.todos[row]['completed']

        self.refresh_list()

        self.save_todos()

    

    def clear_completed(self):

        """清空已完成的事项"""

        self.todos = [todo for todo in self.todos if not todo['completed']]

        self.refresh_list()

        self.save_todos()

    

    def refresh_list(self):

        """刷新列表显示"""

        self.list_widget.clear()

        for todo in self.todos:

            item = QListWidgetItem()

            if todo['completed']:

                item.setText(f"✅ {todo['text']}")

                item.setForeground(Qt.gray)

            else:

                item.setText(f"⭕ {todo['text']}")

            self.list_widget.addItem(item)

        self.update_stats()

    

    def update_stats(self):

        """更新统计信息"""

        total = len(self.todos)

        completed = sum(1 for todo in self.todos if todo['completed'])

        pending = total - completed

        self.stats_label.setText(

            f"总计: {total} | 已完成: {completed} | 待完成: {pending}"

        )

    

    def load_todos(self):

        """从文件加载待办事项"""

        try:

            if os.path.exists(self.data_file):

                with open(self.data_file, 'r', encoding='utf-8') as f:

                    self.todos = json.load(f)

                self.refresh_list()

        except Exception as e:

            QMessageBox.warning(self, '错误', f'加载数据失败: {str(e)}')

    

    def save_todos(self):

        """保存待办事项到文件"""

        try:

            with open(self.data_file, 'w', encoding='utf-8') as f:

                json.dump(self.todos, f, indent=2, ensure_ascii=False)

        except Exception as e:

            QMessageBox.warning(self, '错误', f'保存数据失败: {str(e)}')

    

    def closeEvent(self, event):

        """关闭事件"""

        self.save_todos()

        event.accept()

def main():

    app = QApplication(sys.argv)

    window = TodoApp()

    window.show()

    sys.exit(app.exec_())

if name == 'main':

    main()

打包命令:

安装依赖

pip install PyQt5

打包(单文件模式)

pyinstaller --onefile --noconsole --icon=app.ico --name TodoApp todo_app.py

或使用 .spec 文件

pyinstaller TodoApp.spec

TodoApp.spec 配置:

复制

-- mode: python ; coding: utf-8 --

block_cipher = None

a = Analysis(

    ['todo_app.py'],

    pathex=[],

    binaries=[],

    datas=[('app.ico', '.')],

    hiddenimports=['PyQt5'],

    hookspath=[],

    hooksconfig={},

    runtime_hooks=[],

    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,

    a.binaries,

    a.zipfiles,

    a.datas,

    [],

    name='TodoApp',

    debug=False,

    bootloader_ignore_signals=False,

    strip=False,

    upx=True,

    upx_exclude=[],

    runtime_tmpdir=None,

    console=False,

    disable_windowed_traceback=False,

    argv_emulation=False,

    target_arch=None,

    codesign_identity=None,

    entitlements_file=None,

    icon='app.ico',

)

6.3 案例3:Flask Web 应用打包

web_app.py - Flask Web 应用"""

简单的 Flask Web 应用

功能:本地 Web 服务器,提供文件浏览和上传

"""from flask import Flask, render_template_string, request, send_file, jsonifyimport osimport sysimport webbrowserimport threadingfrom pathlib import Path

def resource_path(relative_path):

    """获取资源文件路径"""

    try:

        base_path = sys._MEIPASS

    except Exception:

        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)

app = Flask(name)

app.config['UPLOAD_FOLDER'] = './uploads'

os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)

HTML 模板

HTML_TEMPLATE = """

    文件管理器

   

        body {

            font-family: Arial, sans-serif;

            max-width: 1200px;

            margin: 0 auto;

            padding: 20px;

            background-color: #f5f5f5;

        }

        .container {

            background: white;

            padding: 30px;

            border-radius: 10px;

            box-shadow: 0 2px 10px rgba(0,0,0,0.1);

        }

        h1 {

            color: #333;

            text-align: center;

        }

        .upload-section {

            margin: 20px 0;

            padding: 20px;

            background: #f9f9f9;

            border-radius: 5px;

        }

        .file-list {

            margin-top: 20px;

        }

        .file-item {

            padding: 10px;

            margin: 5px 0;

            background: #fff;

            border: 1px solid #ddd;

            border-radius: 5px;

            display: flex;

            justify-content: space-between;

            align-items: center;

        }

        .file-item:hover {

            background: #f0f0f0;

        }

        button {

            padding: 10px 20px;

            background: #4CAF50;

            color: white;

            border: none;

            border-radius: 5px;

            cursor: pointer;

        }

        button:hover {

            background: #45a049;

        }

        .delete-btn {

            background: #f44336;

        }

        .delete-btn:hover {

            background: #da190b;

        }

   

   

       

�� 文件管理器

        

       

           

上传文件

           

               

                上传

           

       

        

       

           

文件列表

            {% for file in files %}

           

                �� {{ file }}

               

                   

                        下载

                   

                    删除

               

           

            {% endfor %}

       

   

    

   

        function deleteFile(filename) {

            if (confirm('确定要删除 ' + filename + ' 吗?')) {

                fetch('/delete/' + filename, {method: 'DELETE'})

                    .then(() => location.reload());

            }

        }

   

"""

@app.route('/')def index():

    """首页"""

    files = os.listdir(app.config['UPLOAD_FOLDER'])

    return render_template_string(HTML_TEMPLATE, files=files)

@app.route('/upload', methods=['POST'])def upload():

    """上传文件"""

    if 'file' not in request.files:

        return '没有文件', 400

    

    file = request.files['file']

    if file.filename == '':

        return '文件名为空', 400

    

    file.save(os.path.join(app.config['UPLOAD_FOLDER'], file.filename))

    return '''

       

            alert('上传成功!');

            window.location.href = '/';

       

    '''

@app.route('/download/')def download(filename):

    """下载文件"""

    file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)

    return send_file(file_path, as_attachment=True)

@app.route('/delete/', methods=['DELETE'])def delete(filename):

    """删除文件"""

    file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)

    if os.path.exists(file_path):

        os.remove(file_path)

        return jsonify({'success': True})

    return jsonify({'success': False}), 404

def open_browser():

    """延迟打开浏览器"""

    import time

    time.sleep(1.5)

    webbrowser.open('http://127.0.0.1:5000')

def main():

    print("=" * 60)

    print("文件管理器 Web 服务已启动")

    print("访问地址: http://127.0.0.1:5000")

    print("按 Ctrl+C 停止服务")

    print("=" * 60)

    

    # 在新线程中打开浏览器

    threading.Thread(target=open_browser, daemon=True).start()

    

    # 启动 Flask 服务

    app.run(host='127.0.0.1', port=5000, debug=False)

if name == 'main':

    main()

打包命令:

安装依赖

pip install flask

打包

pyinstaller --onefile --noconsole --icon=app.ico --name FileManager web_app.py

或者显示控制台(方便调试)

pyinstaller --onefile --console --icon=app.ico --name FileManager web_app.py

七、常见问题及解决方法

7.1 导入错误问题

问题1:ModuleNotFoundError

复制

问题:打包后运行提示找不到模块# 原因:动态导入或隐式导入未被检测

解决方案1:使用 --hidden-import

pyinstaller --onefile --hidden-import=requests --hidden-import=PIL app.py

解决方案2:在 .spec 文件中添加

hiddenimports=['requests', 'PIL', 'numpy.core._methods']

解决方案3:在代码中显式导入import requests  # 即使不直接使用,也要导入

问题2:第三方库钩子缺失

某些库需要特殊处理,创建自定义钩子

hook-mylib.pyfrom PyInstaller.utils.hooks import collect_data_files, collect_submodules

datas = collect_data_files('mylib')

hiddenimports = collect_submodules('mylib')

使用钩子

pyinstaller --additional-hooks-dir=./hooks app.py

7.2 资源文件问题

问题3:找不到资源文件

错误的方式(打包后会失败)with open('config.json', 'r') as f:

    config = json.load(f)

正确的方式import sysimport os

def resource_path(relative_path):

    """获取资源文件的绝对路径"""

    try:

        # PyInstaller 创建临时文件夹,路径存储在 _MEIPASS

        base_path = sys._MEIPASS

    except Exception:

        base_path = os.path.abspath(".")

    

    return os.path.join(base_path, relative_path)

使用

config_path = resource_path('config.json')with open(config_path, 'r') as f:

    config = json.load(f)

问题4:图片/图标不显示

打包时添加资源文件

pyinstaller --onefile --add-data "images;images" --add-data "config.json;." app.py

在代码中使用

icon_path = resource_path('images/icon.png')

7.3 体积优化问题

问题5:打包文件过大

解决方案1:排除不需要的模块

pyinstaller --onefile --exclude-module=matplotlib --exclude-module=pandas app.py

解决方案2:使用虚拟环境(只安装必要的包)

python -m venv venv

venv\Scripts\activate

pip install 必要的包

pyinstaller app.py

解决方案3:使用 UPX 压缩# 下载 UPX: upx.github.io/# 将 upx.exe 放到 PATH 或 PyInstaller 目录

pyinstaller --onefile --upx-dir=./upx app.py

解决方案4:在 .spec 中精细控制

excludes=['tkinter', 'matplotlib', 'test', 'unittest']

体积对比示例:

optimization_test.py - 测试不同优化方案的效果"""

测试脚本:比较不同打包选项的文件大小

"""import subprocessimport os

def get_file_size(file_path):

    """获取文件大小(MB)"""

    if os.path.exists(file_path):

        size = os.path.getsize(file_path) / (1024 * 1024)

        return f"{size:.2f} MB"

    return "文件不存在"

测试不同的打包选项

test_cases = [

    {

        'name': '基础打包',

        'cmd': 'pyinstaller --onefile app.py',

        'output': 'dist/app.exe'

    },

    {

        'name': '排除模块',

        'cmd': 'pyinstaller --onefile --exclude-module=matplotlib app.py',

        'output': 'dist/app.exe'

    },

    {

        'name': 'UPX压缩',

        'cmd': 'pyinstaller --onefile --upx-dir=./upx app.py',

        'output': 'dist/app.exe'

    },

]

print("打包体积优化测试")print("=" * 60)

for test in test_cases:

    print(f"\n测试: {test['name']}")

    print(f"命令: {test['cmd']}")

    

    # 执行打包

    # subprocess.run(test['cmd'], shell=True)

    

    # 显示文件大小

    size = get_file_size(test['output'])

    print(f"文件大小: {size}")

7.4 运行时错误

问题6:打包后运行闪退

解决方案1:使用控制台模式查看错误

pyinstaller --onefile --console app.py

解决方案2:添加异常捕获import sysimport traceback

def main():

    try:

        # 你的主程序代码

        pass

    except Exception as e:

        # 将错误写入日志文件

        with open('error.log', 'w') as f:

            f.write(traceback.format_exc())

        print(f"发生错误: {e}")

        input("按回车键退出...")

        sys.exit(1)

if name == 'main':

    main()

问题7:DLL 缺失错误

Windows 常见 DLL 缺失

解决方案1:安装 Visual C++ Redistributable# 下载地址:aka.ms/vs/17/relea…

解决方案2:手动添加 DLL

pyinstaller --onefile --add-binary "msvcp140.dll;." app.py

解决方案3:使用 --collect-all

pyinstaller --onefile --collect-all numpy app.py

7.5 特定库问题

问题8:PyQt5/PySide2 打包问题

PyQt5 打包配置

pyinstaller --onefile --noconsole \

    --hidden-import=PyQt5.QtPrintSupport \

    --hidden-import=PyQt5.sip \

    app.py

或在 .spec 中

hiddenimports=[

    'PyQt5.QtPrintSupport',

    'PyQt5.sip',

    'PyQt5.QtCore',

    'PyQt5.QtGui',

    'PyQt5.QtWidgets',

]

问题9:NumPy/Pandas 打包问题

NumPy/Pandas 打包

pyinstaller --onefile \

    --hidden-import=numpy.core._methods \

    --hidden-import=numpy.lib.format \

    --collect-all pandas \

    app.py

问题10:Matplotlib 打包问题

Matplotlib 打包

pyinstaller --onefile \

    --hidden-import=matplotlib.backends.backend_tkagg \

    --collect-data matplotlib \

    app.py

7.6 调试技巧

debug_helper.py - 调试辅助工具"""

打包后的调试辅助工具

"""import sysimport osimport tracebackfrom datetime import datetime

class DebugHelper:

    def init(self, log_file='debug.log'):

        self.log_file = log_file

    

    def log(self, message):

        """写入日志"""

        timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')

        with open(self.log_file, 'a', encoding='utf-8') as f:

            f.write(f"[{timestamp}] {message}\n")

    

    def log_exception(self, exc):

        """记录异常"""

        self.log("=" * 60)

        self.log("发生异常:")

        self.log(traceback.format_exc())

        self.log("=" * 60)

    

    def log_system_info(self):

        """记录系统信息"""

        self.log("系统信息:")

        self.log(f"  Python版本: {sys.version}")

        self.log(f"  平台: {sys.platform}")

        self.log(f"  可执行文件: {sys.executable}")

        

        # 检查是否在打包环境中运行

        if getattr(sys, 'frozen', False):

            self.log(f"  运行模式: 打包后")

            self.log(f"  临时目录: {sys._MEIPASS}")

        else:

            self.log(f"  运行模式: 开发环境")

    

    def check_resources(self, resource_list):

        """检查资源文件是否存在"""

        self.log("\n资源文件检查:")

        for resource in resource_list:

            exists = os.path.exists(resource)

            status = "✅" if exists else "❌"

            self.log(f"  {status} {resource}")

使用示例def main():

    debug = DebugHelper()

    

    try:

        debug.log_system_info()

        debug.check_resources(['config.json', 'icon.ico', 'data/'])

        

        # 你的主程序代码

        print("程序运行中...")

        

    except Exception as e:

        debug.log_exception(e)

        print(f"发生错误,详情请查看 {debug.log_file}")

        input("按回车键退出...")

if name == 'main':

    main()

八、高级技巧与最佳实践

8.1 自动化打包脚本

build.py - 自动化打包脚本"""

自动化打包脚本

功能:清理、打包、测试、生成安装包

"""import subprocessimport shutilimport osimport sysfrom pathlib import Path

class Builder:

    def init(self, app_name, main_script):

        self.app_name = app_name

        self.main_script = main_script

        self.dist_dir = Path('dist')

        self.build_dir = Path('build')

        self.spec_file = Path(f'{app_name}.spec')

    

    def clean(self):

        """清理旧的构建文件"""

        print("�� 清理旧文件...")

        

        dirs_to_remove = [self.dist_dir, self.build_dir]

        for dir_path in dirs_to_remove:

            if dir_path.exists():

                shutil.rmtree(dir_path)

                print(f"  ✅ 删除 {dir_path}")

        

        if self.spec_file.exists():

            self.spec_file.unlink()

            print(f"  ✅ 删除 {self.spec_file}")

    

    def build(self, options=None):

        """执行打包"""

        print(f"\n�� 开始打包 {self.app_name}...")

        

        # 基础命令

        cmd = [

            'pyinstaller',

            '--onefile',

            '--noconsole',

            f'--name={self.app_name}',

        ]

        

        # 添加额外选项

        if options:

            cmd.extend(options)

        

        # 添加主脚本

        cmd.append(self.main_script)

        

        # 执行打包

        print(f"  命令: {' '.join(cmd)}")

        result = subprocess.run(cmd, capture_output=True, text=True)

        

        if result.returncode == 0:

            print("  ✅ 打包成功")

            return True

        else:

            print("  ❌ 打包失败")

            print(result.stderr)

            return False

    

    def test(self):

        """测试打包后的程序"""

        print(f"\n�� 测试 {self.app_name}...")

        

        exe_path = self.dist_dir / f'{self.app_name}.exe'

        if not exe_path.exists():

            print("  ❌ 可执行文件不存在")

            return False

        

        # 获取文件大小

        size_mb = exe_path.stat().st_size / (1024 * 1024)

        print(f"  文件大小: {size_mb:.2f} MB")

        

        # 可以添加更多测试...

        print("  ✅ 测试通过")

        return True

    

    def create_installer(self):

        """创建安装包(使用 Inno Setup)"""

        print(f"\n�� 创建安装包...")

        

        # 这里可以集成 Inno Setup 或 NSIS

        # 示例:生成 Inno Setup 脚本

        

        iss_content = f"""

[Setup]

AppName={self.app_name}

AppVersion=1.0

DefaultDirName={{pf}}\{self.app_name}

DefaultGroupName={self.app_name}

OutputDir=installer

OutputBaseFilename={self.app_name}_Setup

[Files]

Source: "dist\{self.app_name}.exe"; DestDir: "{{app}}"

[Icons]

Name: "{{group}}\{self.app_name}"; Filename: "{{app}}\{self.app_name}.exe"

Name: "{{commondesktop}}\{self.app_name}"; Filename: "{{app}}\{self.app_name}.exe"

"""

        

        iss_file = Path(f'{self.app_name}.iss')

        with open(iss_file, 'w', encoding='utf-8') as f:

            f.write(iss_content)

        

        print(f"  ✅ 生成 Inno Setup 脚本: {iss_file}")

        print("  提示: 使用 Inno Setup Compiler 编译此脚本")

    

    def package(self):

        """打包成压缩文件"""

        print(f"\n�� 创建发布包...")

        

        import zipfile

        

        zip_name = f'{self.app_name}_v1.0.zip'

        with zipfile.ZipFile(zip_name, 'w', zipfile.ZIP_DEFLATED) as zipf:

            exe_path = self.dist_dir / f'{self.app_name}.exe'

            if exe_path.exists():

                zipf.write(exe_path, f'{self.app_name}.exe')

                print(f"  ✅ 创建压缩包: {zip_name}")

            else:

                print("  ❌ 可执行文件不存在")

    

    def run_all(self, options=None):

        """执行完整的构建流程"""

        print("=" * 60)

        print(f"开始构建 {self.app_name}")

        print("=" * 60)

        

        # 1. 清理

        self.clean()

        

        # 2. 打包

        if not self.build(options):

            print("\n❌ 构建失败")

            return False

        

        # 3. 测试

        if not self.test():

            print("\n❌ 测试失败")

            return False

        

        # 4. 创建发布包

        self.package()

        

        # 5. 生成安装脚本

        self.create_installer()

        

        print("\n" + "=" * 60)

        print("✅ 构建完成!")

        print("=" * 60)

        return True

使用示例if name == 'main':

    # 配置

    APP_NAME = 'MyApp'

    MAIN_SCRIPT = 'app.py'

    

    # 打包选项

    OPTIONS = [

        '--icon=app.ico',

        '--add-data=config.json;.',

        '--hidden-import=requests',

    ]

    

    # 执行构建

    builder = Builder(APP_NAME, MAIN_SCRIPT)

    builder.run_all(OPTIONS)

8.2 多平台打包方案

multi_platform_build.py - 多平台打包"""

多平台打包脚本

支持 Windows、Linux、macOS

"""import platformimport subprocessimport sys

class MultiPlatformBuilder:

    def init(self, app_name, main_script):

        self.app_name = app_name

        self.main_script = main_script

        self.system = platform.system()

    

    def get_platform_options(self):

        """获取平台特定的打包选项"""

        options = {

            'Windows': [

                '--onefile',

                '--noconsole',

                '--icon=app.ico',

            ],

            'Linux': [

                '--onefile',

                '--noconsole',

            ],

            'Darwin': [  # macOS

                '--onefile',

                '--noconsole',

                '--icon=app.icns',  # macOS 使用 .icns 格式

            ]

        }

        

        return options.get(self.system, [])

    

    def build(self):

        """执行打包"""

        print(f"当前平台: {self.system}")

        

        cmd = ['pyinstaller']

        cmd.extend(self.get_platform_options())

        cmd.extend([f'--name={self.app_name}', self.main_script])

        

        print(f"执行命令: {' '.join(cmd)}")

        subprocess.run(cmd)

    

    def create_macos_app(self):

        """创建 macOS .app 包"""

        if self.system != 'Darwin':

            return

        

        print("创建 macOS .app 包...")

        

        # 使用 --windowed 选项创建 .app 包

        cmd = [

            'pyinstaller',

            '--windowed',

            '--onefile',

            f'--name={self.app_name}',

            '--icon=app.icns',

            self.main_script

        ]

        

        subprocess.run(cmd)

        print(f"✅ 创建完成: dist/{self.app_name}.app")

    

    def create_linux_appimage(self):

        """创建 Linux AppImage"""

        if self.system != 'Linux':

            return

        

        print("创建 Linux AppImage...")

        # 这里需要额外的工具如 appimagetool

        # 示例流程:

        # 1. 创建 AppDir 结构

        # 2. 复制可执行文件

        # 3. 创建 .desktop 文件

        # 4. 使用 appimagetool 打包

        

        print("提示: 需要安装 appimagetool")

使用示例if name == 'main':

    builder = MultiPlatformBuilder('MyApp', 'app.py')

    builder.build()

    

    if platform.system() == 'Darwin':

        builder.create_macos_app()

    elif platform.system() == 'Linux':

        builder.create_linux_appimage()

8.3 版本管理与自动更新

version_manager.py - 版本管理"""

版本管理和自动更新检查

"""

import json

import requests

from packaging import version

import sys

import os

class VersionManager:

    def init(self, current_version, update_url):

        self.current_version = current_version

        self.update_url = update_url

    

    def check_update(self):

        """检查更新"""

        try:

            response = requests.get(self.update_url, timeout=5)

            data = response.json()

            

            latest_version = data.get('version')

            download_url = data.get('download_url')

            changelog = data.get('changelog', '')

            

            if version.parse(latest_version) > version.parse(self.current_version):

                return {

                    'has_update': True,

                    'latest_version': latest_version,

                    'download_url': download_url,

                    'changelog': changelog

                }

            else:

                return {'has_update': False}

                

        except Exception as e:

            print(f"检查更新失败: {e}")

            return {'has_update': False, 'error': str(e)}

    

    def download_update(self, url, save_path):

        """下载更新"""

        try:

            print(f"正在下载更新...")

            response = requests.get(url, stream=True)

            total_size = int(response.headers.get('content-length', 0))

            

            with open(save_path, 'wb') as f:

                downloaded = 0

                for chunk in response.iter_content(chunk_size=8192):

                    if chunk:

                        f.write(chunk)

                        downloaded += len(chunk)

                        

                        # 显示进度

                        if total_size > 0:

                            progress = (downloaded / total_size) * 100

                            print(f'\r进度: {progress:.1f}%', end='')

            

            print(f'\n✅ 下载完成: {save_path}')

            return True

            

        except Exception as e:

            print(f'❌ 下载失败: {e}')

            return False

    

    def apply_update(self, new_exe_path):

        """应用更新(重启程序)"""

        import subprocess

        

        # 创建更新脚本

        if sys.platform == 'win32':

            update_script = 'update.bat'

            script_content = f"""

@echo off

echo 正在更新程序...

timeout /t 2 /nobreak > nul

move /y "{new_exe_path}" "{sys.executable}"

start "" "{sys.executable}"

del "%~f0"

"""

        else:

            update_script = 'update.sh'

            script_content = f"""#!/bin/bash

echo "正在更新程序..."

sleep 2

mv -f "{new_exe_path}" "{sys.executable}"

chmod +x "{sys.executable}"

"{sys.executable}" &

rm -f "$0"

"""

        

        with open(update_script, 'w') as f:

            f.write(script_content)

        

        # 执行更新脚本

        if sys.platform == 'win32':

            subprocess.Popen([update_script], shell=True)

        else:

            os.chmod(update_script, 0o755)

            subprocess.Popen([f'./{update_script}'], shell=True)

        

        sys.exit(0)

在主程序中使用def main():

    APP_VERSION = '1.0.0'

    UPDATE_URL = 'api.example.com/check_updat…'

    

    vm = VersionManager(APP_VERSION, UPDATE_URL)

    

    # 检查更新

    update_info = vm.check_update()

    

    if update_info.get('has_update'):

        print(f"发现新版本: {update_info['latest_version']}")

        print(f"更新内容:\n{update_info['changelog']}")

        

        choice = input("是否下载更新?(y/n): ")

        if choice.lower() == 'y':

            new_exe = 'new_version.exe'

            if vm.download_update(update_info['download_url'], new_exe):

                vm.apply_update(new_exe)

    else:

        print("当前已是最新版本")

8.4 加密与代码保护

code_protection.py - 代码保护方案"""

代码保护和加密

"""import base64import zlibfrom cryptography.fernet import Fernet

class CodeProtector:

    def init(self, key=None):

        """

        初始化加密器

        

        Args:

            key: 加密密钥(如果为None则生成新密钥)

        """

        if key is None:

            self.key = Fernet.generate_key()

        else:

            self.key = key

        

        self.cipher = Fernet(self.key)

    

    def encrypt_string(self, text):

        """加密字符串"""

        encrypted = self.cipher.encrypt(text.encode())

        return base64.b64encode(encrypted).decode()

    

    def decrypt_string(self, encrypted_text):

        """解密字符串"""

        encrypted = base64.b64decode(encrypted_text.encode())

        return self.cipher.decrypt(encrypted).decode()

    

    def obfuscate_code(self, code):

        """混淆代码(简单压缩+base64)"""

        compressed = zlib.compress(code.encode())

        obfuscated = base64.b64encode(compressed).decode()

        return obfuscated

    

    def deobfuscate_code(self, obfuscated):

        """反混淆代码"""

        compressed = base64.b64decode(obfuscated.encode())

        code = zlib.decompress(compressed).decode()

        return code

使用示例def protect_sensitive_data():

    """保护敏感数据"""

    protector = CodeProtector()

    

    # 保存密钥(注意:密钥需要安全存储)

    with open('secret.key', 'wb') as f:

        f.write(protector.key)

    

    # 加密敏感配置

    api_key = "your_secret_api_key"

    encrypted_key = protector.encrypt_string(api_key)

    

    print(f"加密后的API密钥: {encrypted_key}")

    

    # 在程序中使用

    # 读取密钥

    with open('secret.key', 'rb') as f:

        key = f.read()

    

    protector = CodeProtector(key)

    decrypted_key = protector.decrypt_string(encrypted_key)

    print(f"解密后的API密钥: {decrypted_key}")

打包时的注意事项"""

  1. 使用 --key 参数加密字节码(需要 tinyaes)

   pyinstaller --onefile --key=your_encryption_key app.py

  1. 使用 PyArmor 进行深度混淆

   pip install pyarmor

   pyarmor obfuscate app.py

   pyinstaller --onefile app.py

  1. 使用 Nuitka 编译为 C 代码

   pip install nuitka

   nuitka --standalone --onefile app.py

"""

8.5 性能优化技巧

performance_optimizer.py - 性能优化"""

打包性能优化技巧

"""import timeimport functoolsimport sys

class PerformanceOptimizer:

    """性能优化工具"""

        @staticmethod

    def lazy_import(module_name):

        """延迟导入(减少启动时间)"""

        def decorator(func):            @functools.wraps(func)

            def wrapper(*args, **kwargs):

                # 只在需要时才导入模块

                module = import(module_name)

                return func(module, *args, **kwargs)

            return wrapper

        return decorator

        @staticmethod

    def cache_result(func):

        """缓存函数结果"""

        cache = {}

                @functools.wraps(func)

        def wrapper(*args):

            if args in cache:

                return cache[args]

            result = func(*args)

            cache[args] = result

            return result

        

        return wrapper

        @staticmethod

    def measure_time(func):

        """测量函数执行时间"""        @functools.wraps(func)

        def wrapper(*args, **kwargs):

            start = time.time()

            result = func(*args, **kwargs)

            end = time.time()

            print(f"{func.name} 耗时: {end - start:.4f}秒")

            return result

        return wrapper

优化示例class OptimizedApp:

    def init(self):

        # 延迟导入重型库

        self._numpy = None

        self._pandas = None

        @property

    def numpy(self):

        """延迟导入 NumPy"""

        if self._numpy is None:

            import numpy as np

            self._numpy = np

        return self._numpy

        @property

    def pandas(self):

        """延迟导入 Pandas"""

        if self._pandas is None:

            import pandas as pd

            self._pandas = pd

        return self._pandas

        @PerformanceOptimizer.measure_time

    def process_data(self, data):

        """处理数据"""

        # 只在需要时才导入和使用 NumPy

        if isinstance(data, list):

            return self.numpy.array(data).mean()

        return data

启动优化def optimize_startup():

    """优化启动时间"""

    

    # 1. 禁用不必要的导入检查

    sys.dont_write_bytecode = True

    

    # 2. 预加载常用模块

    import importlib

    modules_to_preload = ['json', 'os', 'sys']

    for module in modules_to_preload:

        importlib.import_module(module)

    

    # 3. 设置递归限制(如果需要)

    sys.setrecursionlimit(1500)

.spec 文件优化配置"""

优化的 .spec 文件示例

a = Analysis(

    ['app.py'],

    pathex=[],

    binaries=[],

    datas=[],

    hiddenimports=[],

    hookspath=[],

    hooksconfig={},

    runtime_hooks=[],

    excludes=[

        # 排除不需要的标准库模块

        'tkinter',

        'unittest',

        'test',

        'distutils',

        'setuptools',

        'pip',

        # 排除不需要的第三方库

        'matplotlib',

        'scipy',

        'IPython',

    ],

    win_no_prefer_redirects=False,

    win_private_assemblies=False,

    cipher=None,

    noarchive=False,

)

移除重复的二进制文件

a.binaries = [x for x in a.binaries if not x[0].startswith('api-ms-win')]

移除不需要的数据文件

a.datas = [x for x in a.datas if not x[0].startswith('tcl')]

pyz = PYZ(a.pure, a.zipped_data, cipher=None)

exe = EXE(

    pyz,

    a.scripts,

    a.binaries,

    a.zipfiles,

    a.datas,

    [],

    name='OptimizedApp',

    debug=False,

    bootloader_ignore_signals=False,

    strip=True,  # 移除调试符号

    upx=True,    # 使用 UPX 压缩

    upx_exclude=[],

    runtime_tmpdir=None,

    console=False,

    disable_windowed_traceback=False,

    argv_emulation=False,

    target_arch=None,

    codesign_identity=None,

    entitlements_file=None,

)

"""

8.6 完整的生产级打包模板

production_build.py - 生产级打包模板"""

生产级打包脚本

包含:清理、构建、测试、签名、发布

"""import osimport sysimport shutilimport subprocessimport jsonimport hashlibfrom pathlib import Pathfrom datetime import datetime

class ProductionBuilder:

    def init(self, config_file='build_config.json'):

        """

        初始化构建器

        

        Args:

            config_file: 构建配置文件

        """

        self.load_config(config_file)

        self.build_time = datetime.now()

    

    def load_config(self, config_file):

        """加载构建配置"""

        with open(config_file, 'r', encoding='utf-8') as f:

            self.config = json.load(f)

        

        # 基本配置

        self.app_name = self.config['app_name']

        self.version = self.config['version']

        self.main_script = self.config['main_script']

        self.icon = self.config.get('icon', 'app.ico')

        

        # 目录配置

        self.dist_dir = Path(self.config.get('dist_dir', 'dist'))

        self.build_dir = Path(self.config.get('build_dir', 'build'))

        self.release_dir = Path(self.config.get('release_dir', 'release'))

    

    def clean(self):

        """清理构建目录"""

        print("\n" + "=" * 60)

        print("�� 清理构建目录")

        print("=" * 60)

        

        dirs_to_clean = [self.dist_dir, self.build_dir]

        

        for dir_path in dirs_to_clean:

            if dir_path.exists():

                shutil.rmtree(dir_path)

                print(f"✅ 清理: {dir_path}")

        

        # 清理 .spec 文件

        spec_files = list(Path('.').glob('*.spec'))

        for spec_file in spec_files:

            spec_file.unlink()

            print(f"✅ 清理: {spec_file}")

    

    def validate_environment(self):

        """验证构建环境"""

        print("\n" + "=" * 60)

        print("�� 验证构建环境")

        print("=" * 60)

        

        checks = []

        

        # 检查 Python 版本

        python_version = sys.version_info

        print(f"Python 版本: {python_version.major}.{python_version.minor}.{python_version.micro}")

        checks.append(python_version.major == 3 and python_version.minor >= 7)

        

        # 检查 PyInstaller

        try:

            result = subprocess.run(['pyinstaller', '--version'],

                                  capture_output=True, text=True)

            print(f"PyInstaller 版本: {result.stdout.strip()}")

            checks.append(True)

        except FileNotFoundError:

            print("❌ PyInstaller 未安装")

            checks.append(False)

        

        # 检查主脚本

        if Path(self.main_script).exists():

            print(f"✅ 主脚本存在: {self.main_script}")

            checks.append(True)

        else:

            print(f"❌ 主脚本不存在: {self.main_script}")

            checks.append(False)

        

        # 检查图标文件

        if Path(self.icon).exists():

            print(f"✅ 图标文件存在: {self.icon}")

            checks.append(True)

        else:

            print(f"⚠️ 图标文件不存在: {self.icon}")

            checks.append(True)  # 图标不是必需的

        

        return all(checks)

    

    def build(self):

        """执行构建"""

        print("\n" + "=" * 60)

        print(f"�� 构建 {self.app_name} v{self.version}")

        print("=" * 60)

        

        # 构建命令

        cmd = ['pyinstaller']

        

        # 基本选项

        cmd.extend([

            '--onefile',

            '--noconsole' if self.config.get('noconsole', True) else '--console',

            f'--name={self.app_name}',

        ])

        

        # 图标

        if Path(self.icon).exists():

            cmd.append(f'--icon={self.icon}')

        

        # 添加数据文件

        for data in self.config.get('add_data', []):

            cmd.append(f'--add-data={data}')

        

        # 添加二进制文件

        for binary in self.config.get('add_binary', []):

            cmd.append(f'--add-binary={binary}')

        

        # 隐藏导入

        for hidden in self.config.get('hidden_imports', []):

            cmd.append(f'--hidden-import={hidden}')

        

        # 排除模块

        for exclude in self.config.get('excludes', []):

            cmd.append(f'--exclude-module={exclude}')

        

        # 其他选项

        if self.config.get('upx', True):

            cmd.append('--upx-dir=./upx')

        

        if self.config.get('clean', True):

            cmd.append('--clean')

        

        # 主脚本

        cmd.append(self.main_script)

        

        # 执行构建

        print(f"\n执行命令:")

        print(' '.join(cmd))

        print()

        

        result = subprocess.run(cmd)

        

        if result.returncode == 0:

            print("\n✅ 构建成功")

            return True

        else:

            print("\n❌ 构建失败")

            return False

    

    def test(self):

        """测试构建结果"""

        print("\n" + "=" * 60)

        print("�� 测试构建结果")

        print("=" * 60)

        

        exe_name = f"{self.app_name}.exe" if sys.platform == 'win32' else self.app_name

        exe_path = self.dist_dir / exe_name

        

        if not exe_path.exists():

            print(f"❌ 可执行文件不存在: {exe_path}")

            return False

        

        # 文件大小

        size_mb = exe_path.stat().st_size / (1024 * 1024)

        print(f"�� 文件大小: {size_mb:.2f} MB")

        

        # 计算 MD5

        md5_hash = self.calculate_md5(exe_path)

        print(f"�� MD5: {md5_hash}")

        

        # 可以添加更多测试

        # 例如:运行程序并检查输出

        

        print("✅ 测试通过")

        return True

    

    def calculate_md5(self, file_path):

        """计算文件 MD5"""

        md5 = hashlib.md5()

        with open(file_path, 'rb') as f:

            for chunk in iter(lambda: f.read(4096), b''):

                md5.update(chunk)

        return md5.hexdigest()

    

    def sign(self):

        """代码签名(Windows)"""

        if sys.platform != 'win32':

            print("\n⚠️ 代码签名仅支持 Windows")

            return True

        

        print("\n" + "=" * 60)

        print("✍️ 代码签名")

        print("=" * 60)

        

        cert_file = self.config.get('certificate')

        if not cert_file or not Path(cert_file).exists():

            print("⚠️ 未配置证书文件,跳过签名")

            return True

        

        exe_path = self.dist_dir / f"{self.app_name}.exe"

        

        # 使用 signtool 签名

        cmd = [

            'signtool',

            'sign',

            '/f', cert_file,

            '/p', self.config.get('certificate_password', ''),

            '/t', 'timestamp.digicert.com',

            '/v',

            str(exe_path)

        ]

        

        result = subprocess.run(cmd, capture_output=True, text=True)

        

        if result.returncode == 0:

            print("✅ 签名成功")

            return True

        else:

            print(f"❌ 签名失败: {result.stderr}")

            return False

    

    def package(self):

        """打包发布文件"""

        print("\n" + "=" * 60)

        print("�� 打包发布文件")

        print("=" * 60)

        

        # 创建发布目录

        self.release_dir.mkdir(exist_ok=True)

        

        exe_name = f"{self.app_name}.exe" if sys.platform == 'win32' else self.app_name

        exe_path = self.dist_dir / exe_name

        

        # 创建版本标识

        version_tag = f"v{self.version}{self.build_time.strftime('%Y%m%d%H%M%S')}"

        

        # 1. 复制可执行文件

        release_exe = self.release_dir / f"{self.app_name}_{version_tag}.exe"

        shutil.copy2(exe_path, release_exe)

        print(f"✅ 复制可执行文件: {release_exe.name}")

        

        # 2. 创建 ZIP 压缩包

        import zipfile

        zip_path = self.release_dir / f"{self.app_name}_{version_tag}.zip"

        with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:

            zipf.write(exe_path, exe_name)

            

            # 添加其他文件

            for extra_file in self.config.get('extra_files', []):

                if Path(extra_file).exists():

                    zipf.write(extra_file, Path(extra_file).name)

        

        print(f"✅ 创建压缩包: {zip_path.name}")

        

        # 3. 生成版本信息文件

        version_info = {

            'app_name': self.app_name,

            'version': self.version,

            'build_time': self.build_time.isoformat(),

            'platform': sys.platform,

            'python_version': f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",

            'file_size_mb': round(exe_path.stat().st_size / (1024 * 1024), 2),

            'md5': self.calculate_md5(exe_path),

        }

        

        version_file = self.release_dir / f"{self.app_name}_{version_tag}_info.json"

        with open(version_file, 'w', encoding='utf-8') as f:

            json.dump(version_info, f, indent=2, ensure_ascii=False)

        

        print(f"✅ 生成版本信息: {version_file.name}")

        

        # 4. 生成 README

        readme_content = f"""

{self.app_name} v{self.version}

构建信息

  • 构建时间: {self.build_time.strftime('%Y-%m-%d %H:%M:%S')}

  • 平台: {sys.platform}

  • Python 版本: {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}

  • 文件大小: {version_info['file_size_mb']} MB

  • MD5: {version_info['md5']}

安装说明

  1. 解压 ZIP 文件

  2. 运行 {exe_name}

更新日志{self.config.get('changelog', '- 初始版本')}

联系方式

  • 官网: {self.config.get('website', 'N/A')}

  • 邮箱: {self.config.get('email', 'N/A')}

"""

        

        readme_file = self.release_dir / 'README.md'

        with open(readme_file, 'w', encoding='utf-8') as f:

            f.write(readme_content)

        

        print(f"✅ 生成 README: {readme_file.name}")

        

        return True

    

    def upload_to_server(self):

        """上传到服务器(可选)"""

        if not self.config.get('upload_enabled', False):

            print("\n⚠️ 未启用自动上传")

            return True

        

        print("\n" + "=" * 60)

        print("☁️ 上传到服务器")

        print("=" * 60)

        

        # 这里可以实现 FTP、SFTP 或云存储上传

        # 示例:使用 paramiko 上传到 SFTP

        

        print("⚠️ 上传功能需要额外实现")

        return True

    

    def generate_report(self):

        """生成构建报告"""

        print("\n" + "=" * 60)

        print("�� 构建报告")

        print("=" * 60)

        

        exe_name = f"{self.app_name}.exe" if sys.platform == 'win32' else self.app_name

        exe_path = self.dist_dir / exe_name

        

        report = f"""

╔══════════════════════════════════════════════════════════╗

║              构建报告 - {self.app_name}                  

╚══════════════════════════════════════════════════════════╝

应用名称: {self.app_name}

版本号:   {self.version}

构建时间: {self.build_time.strftime('%Y-%m-%d %H:%M:%S')}

文件信息:

  - 路径: {exe_path}

  - 大小: {exe_path.stat().st_size / (1024 * 1024):.2f} MB

  - MD5:  {self.calculate_md5(exe_path)}

构建配置:

  - Python: {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}

  - 平台:   {sys.platform}

  - 模式:   {'无控制台' if self.config.get('noconsole', True) else '控制台'}

发布目录: {self.release_dir}

╔══════════════════════════════════════════════════════════╗

║  构建完成!                                              

╚══════════════════════════════════════════════════════════╝

"""

        

        print(report)

        

        # 保存报告

        report_file = self.release_dir / f"build_report_{self.build_time.strftime('%Y%m%d_%H%M%S')}.txt"

        with open(report_file, 'w', encoding='utf-8') as f:

            f.write(report)

    

    def run_full_build(self):

        """执行完整构建流程"""

        start_time = datetime.now()

        

        print("\n")

        print("╔" + "═" * 58 + "╗")

        print(f"║  {self.app_name} v{self.version} - 生产构建流程".ljust(59) + "║")

        print("╚" + "═" * 58 + "╝")

        

        steps = [

            ("验证环境", self.validate_environment),

            ("清理目录", self.clean),

            ("执行构建", self.build),

            ("测试构建", self.test),

            ("代码签名", self.sign),

            ("打包发布", self.package),

            ("上传服务器", self.upload_to_server),

        ]

        

        for step_name, step_func in steps:

            if not step_func():

                print(f"\n❌ 构建失败于: {step_name}")

                return False

        

        # 生成报告

        self.generate_report()

        

        end_time = datetime.now()

        duration = (end_time - start_time).total_seconds()

        

        print(f"\n✅ 构建成功!总耗时: {duration:.2f} 秒")

        return True

构建配置文件示例

BUILD_CONFIG_EXAMPLE = """

{

    "app_name": "MyApp",

    "version": "1.0.0",

    "main_script": "app.py",

    "icon": "app.ico",

    

    "noconsole": true,

    "upx": true,

    "clean": true,

    

    "add_data": [

        "config.json;.",

        "resources;resources"

    ],

    

    "add_binary": [],

    

    "hidden_imports": [

        "requests",

        "PIL"

    ],

    

    "excludes": [

        "matplotlib",

        "pandas",

        "unittest",

        "test"

    ],

    

    "extra_files": [

        "README.md",

        "LICENSE"

    ],

    

    "dist_dir": "dist",

    "build_dir": "build",

    "release_dir": "release",

    

    "certificate": "cert.pfx",

    "certificate_password": "password",

    

    "upload_enabled": false,

    "upload_server": "ftp.example.com",

    "upload_path": "/releases",

    

    "changelog": "- 初始版本\n- 添加核心功能",

    "website": "example.com",

    "email": "support@example.com"

}

"""

使用示例if name == 'main':

    # 创建示例配置文件

    if not Path('build_config.json').exists():

        with open('build_config.json', 'w', encoding='utf-8') as f:

            f.write(BUILD_CONFIG_EXAMPLE)

        print("✅ 已创建示例配置文件: build_config.json")

        print("请根据实际情况修改配置文件,然后重新运行此脚本")

        sys.exit(0)

    

    # 执行构建

    builder = ProductionBuilder('build_config.json')

    success = builder.run_full_build()

    

    sys.exit(0 if success else 1)

九、总结与最佳实践清单

9.1 打包前检查清单

✅ 代码检查

  □ 所有导入语句正确

  □ 没有语法错误

  □ 路径使用 resource_path() 函数

  □ 异常处理完善

✅ 依赖管理

  □ requirements.txt 已更新

  □ 使用虚拟环境

  □ 第三方库版本兼容

✅ 资源文件

  □ 所有资源文件已列出

  □ 图标文件格式正确

  □ 配置文件路径正确

✅ 测试

  □ 开发环境测试通过

  □ 打包后测试通过

  □ 不同系统测试通过

9.2 最佳实践总结

使用虚拟环境:避免打包不必要的依赖

资源路径处理:始终使用 resource_path() 函数

异常处理:添加完善的错误处理和日志记录

版本管理:使用 .spec 文件管理复杂配置

体积优化:排除不需要的模块,使用 UPX 压缩

测试充分:在目标环境充分测试

文档完善:提供详细的安装和使用说明

自动化构建:使用脚本自动化构建流程

9.3 常用命令速查

基础打包

pyinstaller app.py

单文件打包

pyinstaller --onefile app.py

GUI应用(无控制台)

pyinstaller --onefile --noconsole app.py

完整配置

pyinstaller --onefile --noconsole --icon=app.ico --name=MyApp \

  --add-data="config.json;." --hidden-import=requests app.py

使用 .spec 文件

pyinstaller MyApp.spec

清理重新打包

pyinstaller --clean MyApp.spec