Python入门教程丨3.6 代码打包

想象一下,当你开发了一个超酷的Python程序,但当你想分享给朋友时,对方却要经历「安装Python→配置环境→安装依赖库」的繁琐过程,即便是使用 docker,也会有一定的门槛,如果朋友是纯小白,他们可能会直接放弃!

而将代码打包后就可以

✅ 让任何人在没有Python环境的情况下双击运行程序✅ 保护你的源代码(虽然不能完全防止反编译)✅ 打造专业级的软件分发体验


1. 打包工具选择

代码打包是指将开发人员编写的源代码、依赖库、资源文件等整合在一起,生成一个可执行的文件或程序包的过程。常见的可执行文件有 Windows 操作系统中的.exe 或者 Mac 系统中的 .dmg 或者.app,用户双击文件即可运行程序,无需安装额外的开发环境。

我打包了一个app,无需环境,直接点击即可运行!

1.1 主流工具对比

工具 平台支持 单文件输出 加密能力 学习难度 适用场景
PyInstaller Win/macOS/Linux ✔️ ✔️ ⭐⭐ 通用首选
cx_Freeze 跨平台 ⭐⭐⭐ 生成安装包
py2exe 仅Windows ✔️ ⭐⭐ 简单Windows程序
Nuitka 跨平台 ✔️ ✔️ ⭐⭐⭐⭐ 高性能/代码保护

1.2 工具选择决策

是否需要跨平台?
├─ 是 → PyInstaller/Nuitka
└─ 否 → 
   ├─ Windows → py2exe
   └─ macOS → py2app

是否需要极致性能?
├─ 是 → Nuitka
└─ 否 → PyInstaller

综合来看,我们选择PyInstaller就可以胜任大部分任务了!


2.PyInstaller深度实战

2.1 环境准备

安装PyInstaller:

pip install pyinstaller

2.2 基础打包

我们使用 pyqt6 写了一个简单的窗口,下面我们要把它打包成 APP

scss 复制代码
import sys
from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QVBoxLayout

def on_button_click():
    print("按钮被点击了!")

# 创建应用程序实例
app = QApplication(sys.argv)
# 创建主窗口
window = QWidget()
window.setWindowTitle('窗口')
layout = QVBoxLayout()
label = QLabel('欢迎来到PyQt6的世界!')
layout.addWidget(label)
button = QPushButton('点击我')
button.clicked.connect(on_button_click)
layout.addWidget(button)
# 将布局设置到窗口
window.setLayout(layout)

window.show()
sys.exit(app.exec())

我们在命令行窗口中使用打包命令:

css 复制代码
# 生成单文件+隐藏控制台+自定义图标
pyinstaller --onefile --noconsole --icon=app.ico hello.py

参数含义:

  • --onefile:生成单个.exe文件
  • --noconsole:隐藏命令行窗口(GUI程序必选)
  • --icon:指定软件图标

运行成功后我们会在同级目录下找到一个dist文件,里面就是我们打包后的 APP

2.3 pyinstaller 设置环境变量(运行失败后设置)

注意:如果运行后无反应,可能需要添加环境变量。

首先我们需要找到 pyinstaller 的路径,通常情况下,PyInstaller 会被安装到 Python 的 Scripts 文件夹中,可以通过以下命令查看 PyInstaller 的安装路径:

sql 复制代码
pip show pyinstaller

该命令会显示 PyInstaller 的安装位置,例如:

Location: C:\Users\用户名\AppData\Roaming\Python\PythonXX\Scripts

其中XX 是 Python 的版本号。

➡️ 修改方式:

1️⃣ Windows 系统:

  1. 打开"系统属性"->"高级"->"环境变量"。
  2. 在"系统变量"部分找到并选择"Path",点击"编辑"。
  3. 在弹出的窗口中,点击"新建",然后输入 PyInstaller 的安装路径(刚刚找到的)。
  4. 点击"确定"保存设置。

2️⃣ macOS 系统:

  1. 打开终端。
  2. 输入以下命令。
ruby 复制代码
export PATH=$PATH:/path/to/pyinstaller

/path/to/pyinstaller 替换为 PyInstaller 的实际安装路径。

  1. 保存文件后,运行以下命令使更改生效:
bash 复制代码
source ~/.bash_profile

😯 配置完成后,打开新的命令提示符或终端窗口,输入以下命令验证 PyInstaller 是否可用:

css 复制代码
pyinstaller --version

如果返回 PyInstaller 的版本号,则说明配置成功。

2.4 复杂项目

如果你的项目是包含很多图片、配置文件和第三方库的复杂项目,目录结构像下面这样,那么打包会稍微复制些。

arduino 复制代码
myapp/
├── main.py
├── config.ini
├── data/
│   ├── images/
│   └── translations/
└── requirements.txt

这时我们打包的时候就需要指定依赖文件,即添加你需要的数据。

csharp 复制代码
pyinstaller --add-data "data/*:data" --add-data "config.ini:." main.py
  • --add-data "源路径:目标路径"

  • 源路径 :项目中资源文件的路径(支持通配符 *)。

  • 打包后程序运行时,资源文件会被解压到临时目录的 data/ 下(通过 sys._MEIPASS 访问)。

  • 分隔符

  • Windows 使用 ;,例如:"data/*;data"

  • Linux/macOS 使用 :,例如:"data/*:data"

在打包后的程序中,资源文件会被解压到一个临时目录(sys._MEIPASS),因此写代码的时候不能直接使用相对路径 (如 data/images/logo.png)。

需要通过以下方法动态获取资源路径:

python 复制代码
import sys
import os

def resource_path(relative_path):
    """ 获取资源的绝对路径(兼容开发环境和打包环境) """
    if hasattr(sys, '_MEIPASS'):
        # 打包后:资源在临时目录 sys._MEIPASS 下
        base_path = sys._MEIPASS
    else:
        # 开发环境:资源在项目根目录下
        base_path = os.path.abspath(".")
    return os.path.join(base_path, relative_path)

# 使用示例
config_path = resource_path('config.ini')
image_path = resource_path('data/images/logo.png')

# 读取文件
with open(config_path, 'r') as f:
    print(f.read())
  • 开发环境 :直接使用相对路径(如 data/images/logo.png)。
  • 打包环境 :通过 sys._MEIPASS 获取临时目录,再拼接相对路径。
  • resource_path() 函数会自动处理两种环境的路径差异。

3. 高级开发技巧,让你的程序更专业

3.1 使用UPX压缩体积

我们会发现打包的应用通常都很大,一个简单的应用就几十上百 MB,多少有点奇怪了。这时候我们可以使用 UPX 这个开源项目来压缩体积,一般可减少30%-50%的文件大小

操作步骤

  1. 下载UPX:upx.github.io
  2. 解压到任意目录(例如 C:\upx
  3. 打包时指定UPX路径:
css 复制代码
pyinstaller --onefile --upx-dir = C:\upx main.py
下载UPX时请选对操作系统

3.2 版本信息与数字签名

在 Windows 平台上,为应用程序添加版本信息是一个非常有用的功能,版本信息可以帮助用户和开发者识别应用程序的版本、公司名称、版权信息等,同时也能提升应用程序的专业性和可信度。

版本信息:

版本信息是嵌入在可执行文件(EXE)中的元数据,用户和系统可以通过以下方式查看:

  • 右键点击 EXE 文件 -> 属性 -> 详细信息:可以看到版本号、公司名称、版权信息等。
  • 程序内部:某些应用程序可能会在"关于"页面显示这些信息。

具体步骤:

1️⃣ 创建 version_info.txt 文件

创建一个文本文件,命名为 version_info.txt,内容如下:

ini 复制代码
# UTF-8
Version=1.0.0
CompanyName=公司名称
FileDescription=文件描述
InternalName=myapp
LegalCopyright=© 2023 公司名称. All rights reserved.
OriginalFilename=myapp.exe
ProductName=APP名称

2️⃣ 使用 PyInstaller 打包时附加版本信息

在打包命令中使用 --version-file 参数指定 version_info.txt 文件:

css 复制代码
pyinstaller --onefile --noconsole --version-file=version_info.txt main.py

参数说明

  • --onefile:生成单个可执行文件。
  • --noconsole:不显示控制台窗口(适用于 GUI 程序)。
  • --version-file:指定版本信息文件。

3️⃣ 验证版本信息

打包完成后,右键点击生成的 EXE 文件,选择"属性" -> "详细信息",即可看到添加的版本信息。


3.3 代码签名(防止杀毒软件误报)

代码签名是为可执行文件添加数字签名,以验证文件的来源和完整性。未签名的应用程序可能会被某些杀毒软件误报为恶意软件。

代码签名的步骤

1️⃣ 获取代码签名证书

  • 从受信任的证书颁发机构(如 DigiCert、GlobalSign)购买代码签名证书。
  • 或者使用自签名证书(仅适用于测试环境)。

2️⃣ 使用签名工具签名

  • 使用 signtool(Windows SDK 自带)或其他签名工具为 EXE 文件签名。
bash 复制代码
signtool sign /fd SHA256 /a /tr http://timestamp.digicert.com /td SHA256 /v myapp.exe

参数说明:

  • /fd SHA256:指定签名算法为 SHA256。
  • /a:自动选择证书。
  • /tr:指定时间戳服务器。
  • /v:显示详细信息。

3️⃣ 验证签名

右键点击 EXE 文件,选择"属性" -> "数字签名",查看签名信息。


3.4 反编译措施

在企业级软件开发中,保护代码安全至关重要。Python 作为解释型语言,源码易被反编译(如通过 pyinstxtractor 解包或 uncompyle6 反编译字节码),因此需采取额外措施增加逆向难度。我们介绍以下两种方法虽无法完全杜绝破解,但能显著提高攻击者成本。

3.4.1 使用 Nuitka 将代码编译为 C 二进制

Nuitka库可以将 Python 代码转换为 C 代码并编译为二进制文件,从根本上消除字节码泄露风险。

使用步骤

  1. 安装 Nuitka

    pip install nuitka

  2. 编译为独立二进制文件(以 PyQt6 项目为例):

css 复制代码
nuitka --standalone --onefile --enable-plugin=pyqt6 --output-dir=dist main.py

参数说明

  • --standalone:生成独立可执行文件(包含依赖)。
  • --onefile:打包为单个文件。
  • --enable-plugin=pyqt6:启用 PyQt6 插件支持(自动处理资源文件)。
  • --output-dir=dist:输出到 dist 目录。

3.4.2 使用 PyArmor 进行代码混淆

原理 :对 Python 字节码加密,运行时动态解密,阻止直接反编译为可读代码。
适用场景:快速增加逆向成本,适合中小型项目或与 Nuitka 结合使用。

步骤

  1. 安装 PyArmor

    pip install pyarmor

  2. 混淆代码

css 复制代码
pyarmor obfuscate --output dist/main_obfuscated --exact main.py

参数说明

  • --output:指定输出目录。
  • --exact:严格模式(禁用部分易破解特性)。

3.4.3 验证反编译效果

  • 工具测试
    使用 pyinstxtractoruncompyle6 尝试反编译生成的可执行文件,观察是否输出有效代码。
  • 日志监控
    在代码中嵌入反调试逻辑,记录异常调用行为(如检测调试器附加)。
方法 安全性 性能影响 部署复杂度
Nuitka 编译 ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐
PyArmor 混淆 ⭐⭐⭐ ⭐⭐⭐ ⭐⭐

企业开发中,建议优先使用 Nuitka 编译 + 代码签名 ,对核心模块辅以 PyArmor 混淆,形成多重防护,最大限度保障软件安全。


4. 实战项目------打包一个真实应用

我们之前写了一个多线程词频分析工具,现在我们再加入简单的 GUI 界面,将其打包成一个软件!

目标:打包一个有PyQt6 界面、多线程词频分析工具,自动分析某个文件夹内所有文本的高频词

4.1 项目结构

bash 复制代码
word_analyzer/
├── main.py              # 主程序入口
├── version_info.txt     # 版本信息文件
├── icon.ico             # 应用程序图标
├── requirements.txt     # 依赖列表
└── data/
    └── stopwords.txt    # 停用词文件(可选)

4.2 完整代码(main.py

python 复制代码
import os
import sys
import jieba
from collections import defaultdict
from docx import Document
from concurrent.futures import ThreadPoolExecutor, as_completed
from PyQt6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QPushButton,
    QLabel, QProgressBar, QTextEdit, QFileDialog, QMessageBox
)
from PyQt6.QtCore import Qt, pyqtSignal, QObject

#参数配置
FILE_TYPES = ('.txt', '.doc', '.docx', '.md')  # 文件类型
NUM_THREADS = 4                                # 线程数
TOP_N = 10                                     # 高频词数量

class AnalysisSignals(QObject):
    progress_updated = pyqtSignal(int, int)    # 进度更新
    result_ready = pyqtSignal(list)            # 最终结果
    error_occurred = pyqtSignal(str)           # 错误信息

# GUI界面
class WordAnalyzerApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("词频分析工具")
        self.setGeometry(100, 100, 600, 400)
        # self.setWindowIcon(self.load_icon("icon.ico"))
        
        # 初始化组件
        self.init_ui()
        self.signals = AnalysisSignals()
        self.signals.progress_updated.connect(self.update_progress)
        self.signals.result_ready.connect(self.show_result)
        self.signals.error_occurred.connect(self.show_error)
        
        # 变量初始化
        self.file_count = 0
        self.stopwords = self.load_stopwords()

    def init_ui(self):
        """ 初始化界面布局 """
        central_widget = QWidget()
        layout = QVBoxLayout()

        # 选择文件夹按钮
        self.btn_choose = QPushButton("选择文件夹", self)
        self.btn_choose.clicked.connect(self.choose_folder)
        layout.addWidget(self.btn_choose)

        # 文件夹路径标签
        self.lbl_folder = QLabel("未选择文件夹")
        layout.addWidget(self.lbl_folder)

        # 进度条
        self.progress = QProgressBar(self)
        self.progress.setAlignment(Qt.AlignmentFlag.AlignCenter)
        layout.addWidget(self.progress)

        # 结果显示区域
        self.txt_result = QTextEdit(self)
        self.txt_result.setReadOnly(True)
        layout.addWidget(self.txt_result)

        central_widget.setLayout(layout)
        self.setCentralWidget(central_widget)

    def load_icon(self, path):
        """ 加载图标文件(兼容打包环境) """
        if hasattr(sys, '_MEIPASS'):
            base_path = sys._MEIPASS
        else:
            base_path = os.path.abspath(".")
        return os.path.join(base_path, path)

    def load_stopwords(self):
        """ 加载停用词文件(可选) """
        try:
            stopwords_path = self.resource_path("data/stopwords.txt")
            with open(stopwords_path, 'r', encoding='utf-8') as f:
                return set(line.strip() for line in f)
        except:
            return {"的", "在", "了", "是", "和", "我", "你"}

    def resource_path(self, relative_path):
        """ 获取资源文件的绝对路径 """
        if hasattr(sys, '_MEIPASS'):
            base_path = sys._MEIPASS
        else:
            base_path = os.path.abspath(".")
        return os.path.join(base_path, relative_path)

    def choose_folder(self):
        """ 选择文件夹并启动分析 """
        folder = QFileDialog.getExistingDirectory(self, "选择文件夹")
        if folder:
            self.lbl_folder.setText(f"分析文件夹: {folder}")
            self.start_analysis(folder)

    def start_analysis(self, folder_path):
        """ 启动多线程分析 """
        self.btn_choose.setEnabled(False)
        self.progress.setValue(0)
        self.txt_result.clear()

        # 获取文件列表
        file_paths = []
        for root, _, files in os.walk(folder_path):
            for file in files:
                if file.lower().endswith(FILE_TYPES):
                    file_paths.append(os.path.join(root, file))
        self.file_count = len(file_paths)
        if self.file_count == 0:
            self.show_error("未找到支持的文件!")
            return

        # 使用线程池处理
        self.thread_pool = ThreadPoolExecutor(max_workers=NUM_THREADS)
        self.thread_pool.submit(self.analyze_folder, file_paths)

    def analyze_folder(self, file_paths):
        """ 多线程分析词频 """
        try:
            jieba.initialize()
            global_word_count = defaultdict(int)
            futures = []

            # 提交任务到线程池
            with ThreadPoolExecutor(max_workers=NUM_THREADS) as executor:
                for file_path in file_paths:
                    futures.append(executor.submit(self.process_file, file_path))

                # 监控进度
                for i, future in enumerate(as_completed(futures)):
                    local_count = future.result()
                    for word, count in local_count.items():
                        global_word_count[word] += count
                    self.signals.progress_updated.emit(i + 1, self.file_count)

            # 输出结果
            sorted_words = sorted(global_word_count.items(), key=lambda x: -x[1])
            self.signals.result_ready.emit(sorted_words[:TOP_N])

        except Exception as e:
            self.signals.error_occurred.emit(str(e))
        finally:
            self.btn_choose.setEnabled(True)

    def process_file(self, file_path):
        """ 处理单个文件 """
        content = self.read_file(file_path)
        words = jieba.cut(content)
        local_count = defaultdict(int)
        for word in words:
            if word not in self.stopwords and len(word) > 1:
                local_count[word] += 1
        return local_count

    def read_file(self, file_path):
        """ 读取文件内容 """
        ext = os.path.splitext(file_path)[1].lower()
        try:
            if ext in ('.txt', '.md'):
                for encoding in ['utf-8', 'gbk']:
                    try:
                        with open(file_path, 'r', encoding=encoding) as f:
                            return f.read()
                    except UnicodeDecodeError:
                        continue
                return ""
            elif ext in ('.doc', '.docx'):
                doc = Document(file_path)
                return '\n'.join(para.text for para in doc.paragraphs)
            else:
                return ""
        except Exception as e:
            raise Exception(f"读取文件失败: {file_path} ({str(e)})")

    def update_progress(self, current, total):
        """ 更新进度条 """
        self.progress.setMaximum(total)
        self.progress.setValue(current)

    def show_result(self, result):
        """ 显示结果 """
        self.txt_result.clear()
        self.txt_result.append("=== 高频关键词Top-{} ===".format(TOP_N))
        for word, count in result:
            self.txt_result.append(f"{word}: {count}次")

    def show_error(self, message):
        """ 显示错误弹窗 """
        QMessageBox.critical(self, "错误", message)
        self.btn_choose.setEnabled(True)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = WordAnalyzerApp()
    window.show()
    sys.exit(app.exec())

4.3 打包配置

创建 version_info.txt

ini 复制代码
# UTF-8
Version=1.0.0
CompanyName=MyCompany
FileDescription=词频分析工具
InternalName=WordAnalyzer
LegalCopyright=© 2023 MyCompany. All rights reserved.
OriginalFilename=WordAnalyzer.exe
ProductName=词频分析工具

打包命令:

python 复制代码
pyinstaller --onefile --noconsole \
--add-data "data/*:data" \
--add-data "icon.ico:." \
--icon=icon.ico \
--version-file=version_info.txt \
--name=WordAnalyzer \
main.py

代码签名(可选)

python 复制代码
signtool sign /fd SHA256 /a /tr http://timestamp.digicert.com /td SHA256 /v dist/WordAnalyzer.exe

4.4 运行效果


现在,你已经掌握了Python打包的核心技能!快去把你的作品打包分享吧!

相关推荐
Tomorrow'sThinker9 分钟前
Python零基础学习第三天:函数与数据结构
开发语言·windows·python
元媛媛12 分钟前
Python - 轻量级后端框架 Flask
开发语言·python·flask
疏狂难除23 分钟前
基于Rye的Django项目通过Pyinstaller用Github工作流简单打包
后端·python·django
囚~徒~35 分钟前
flask 接口文档自动化
python·flask·自动化
行码棋1 小时前
【Python】omegaconf 用法详解
开发语言·python
SomeB1oody1 小时前
【Python机器学习】1.6. 逻辑回归理论(基础):逻辑函数、逻辑回归的原理、分类任务基本框架、通过线性回归求解分类问题
人工智能·python·机器学习·分类·逻辑回归·线性回归
朝丽雨月1 小时前
Manus智能体多代理协同系统:架构创新与实践应用
人工智能·python
梦幻编织者1 小时前
python使用django搭建图书管理系统
数据库·python·django
航重名了779481 小时前
Python主流环境管理工具深度对比指南
python
love_c++2 小时前
TensorFlow 的基本概念和使用场景
人工智能·python·tensorflow