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打包的核心技能!快去把你的作品打包分享吧!

相关推荐
郭庆汝1 小时前
pytorch、torchvision与python版本对应关系
人工智能·pytorch·python
思则变4 小时前
[Pytest] [Part 2]增加 log功能
开发语言·python·pytest
漫谈网络5 小时前
WebSocket 在前后端的完整使用流程
javascript·python·websocket
try2find6 小时前
安装llama-cpp-python踩坑记
开发语言·python·llama
博观而约取7 小时前
Django ORM 1. 创建模型(Model)
数据库·python·django
精灵vector8 小时前
构建专家级SQL Agent交互
python·aigc·ai编程
Zonda要好好学习9 小时前
Python入门Day2
开发语言·python
Vertira9 小时前
pdf 合并 python实现(已解决)
前端·python·pdf
太凉9 小时前
Python之 sorted() 函数的基本语法
python
项目題供诗9 小时前
黑马python(二十四)
开发语言·python