Python中py文件批量加密(编译)成pyd文件(第8节)


在‌Python中,任何以“.py”为后缀名的源代码文件都是以明文形式存在,很难对代码进行加密,容易被复制和盗版,这对于需要保护知识产权的软件开发者来说是一个重大问题‌。在第10章Python模块的第11节“Python中的pyc文件”教程中,已经详细介绍了如何把Python文件加密成pyc文件,而pyc文件的主要缺点是很容易被反编译的。

如果想要把Python文件彻底编译成二进制文件,必须用到Python的第三方库Cython编译成pyd二进制文件。‌pyd文件‌是Python的动态链接库,专门为Windows平台设计的二进制模块文件,通常是用C或C++编写的二进制代码。‌pyd文件‌可以作为Python的扩展模块加载并使用,在Python脚本中,可以通过‌使用import语句加载‌pyd文件模块‌。例如,我们打开Pyhint编辑器,在“Pyhint\Learn-Python\test”文件夹内有一个mymodule.pyd文件,在同一文件夹内的new.py脚本文件中,就可以通过“import mymodule”语句加载mymodule.pyd文件模块。这里需要注意的是,必须确保Python版本与编译pyd文件的版本一致,否则可能导致模块无法加载。比如,通过Python3.8版本编译的pyd文件,不能在Python3.9版本的环境中加载使用。

由于pyd文件是编译后的二进制文件,直接反编译成py文件几乎是不可能的。但是,可以通过逆向工程工具和技术破解pyd文件功能,但这个过程非常复杂且不一定能完全恢复原始代码,通常只能得到大致的逻辑代码结构而非完整准确的源代码。

本节教程将介绍如何通过Tkinter库的GUI用户界面,批量把Python代码文件加密成pyd文件,可以自动对单个py文件或者目录中的所有py文件(包含子目录里的py文件)进行批量编译成pyd文件,操作简单,一键批量处理。

py文件批量编译成pyd文件效果图如下所示:

‌Python中py文件批量加密(编译)成pyd文件

1、批量添加水印过程分析

我们的主要目标是为某个文件夹下的所有py文件批量编译成pyd文件。首先,准备好一批需要加密的py文件,将它们放在一个文件夹中,方便批量处理。程序通过Python的Tkinter图形用户界面(GUI)创建一个应用,用户可以通过简单的界面选择待批量处理的文件夹,“编译结果处理”如果选择“保留原文件”,编译后的文件夹内将同时保留py原文件和编译后的pyd文件;“编译结果处理”如果选择“删除原文件”,编译完成后会自动删除原py文件,慎用!删除后不可恢复,回收站内也将无法找回删除的文件,编译前务必做好备份工作。

具体实现步骤如下:

输入文件夹:程序验证用户输入的文件夹是否包含横杠“-”,如果你的文件名包含横杠(例如my-module.py),在编译成Python模块时,横杠会被转换为下划线(例如my_module.pyd),因为在Python中横杠“-”不是有效的模块名或包名的一部分。

编译结果处理:“编译结果处理”可以选择“保留原文件”或者“删除原文件”。

总的GUI界面设计:使用Tkinter库创建一个操作简单的图形用户界面,方便用户操作。

进度条显示:通过使用tkinter中的ttk.Progressbar控件来显示进度,编译过程可以反映执行进度。

2、代码实现

(1) 安装cython库

首先,确保你已经安装了cython库。如果还没有安装,可以通过pip安装:

pip install cython

(2) 导入必要的库

import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import os, shutil, time, sys, glob

(3)检测cython模块是否已安装

try:
    # 检测是否安装了cython模块
    import Cython
except ImportError: 
    # 如果没有安装,则提示先安装cython模块
    messagebox.showwarning("警告", "请先安装cython模块:pip install cython!")
    # 退出程序
    sys.exit()

(4)py文件编译成pyd文件的主函数

# py编译函数
def pyd_compile(path):
    # 获取文件夹路径
    batch_folder = os.path.dirname(path)
    # 获取不带路径的文件名
    file_path = os.path.split(path)[1]
    # 获取“编译结果处理”选项
    del_py = position_var.get()
    # 将batch_folder设置为当前的工作目录,以便于运行Python代码
    os.chdir(batch_folder)
    # 在batch_folder工作目录中自动生成单独的pyd_setup.py文件
    with open('pyd_setup.py', 'w') as f:
        f.write('from setuptools import setup\n')
        f.write('from Cython.Build import cythonize\n')
        f.write('setup(\n')
        f.write("name='test',\n")
        f.write("ext_modules=cythonize('%s')\n" % file_path)
        f.write(")\n")
    # 开始编译py文件,将输出结果使用NUL丢弃,否则编译过程会输出大量信息到控制台
    os.system('python pyd_setup.py build_ext --inplace > NUL 2>&1')
    # 获取.py的文件名
    filename = file_path.split('.py')[0]
    # 程序暂停执行‌2秒
    time.sleep(2)
    # 获取pyd文件名
    pyd_name = '%s\\%s.pyd' % (batch_folder, filename)
    # 删除旧的pyd文件
    if os.path.exists(pyd_name):
        os.remove(pyd_name)
    # 获取编译后pyd文件的全名,类似***.cp38-win_amd64.pyd
    amd64_pyd = glob.glob(filename + "*.pyd")
    # 重命名,删除编译后多余的“cp38-win_amd64”文件名
    os.rename(amd64_pyd[0], pyd_name)
    # 删除编译后产生“.c”为后缀名的临时文件
    os.remove('%s.c' % filename)
    # 获取编译后生成的build文件夹
    build_batch_folder = os.path.join(batch_folder, 'build')
    # 删除编译后生成的build文件夹
    shutil.rmtree(build_batch_folder)
    # 最后删除编译过程自动生成的pyd_setup.py
    os.remove('pyd_setup.py')
    # 如果处理结果选项是“删除原文件",将删除py源文件,无法恢复,慎用!
    if del_py == '删除原文件':
        os.remove(file_path)

(5)批量编译指定目录下的全部py文件

# 遍历指定目录下的全部py文件,包含子目录里的py文件
def compile_all_file(path):
    # 统计文件夹内的.py文件总数
    total_files = sum(1 for root, dirs, files in os.walk(path) for file in files if file.endswith('.py'))
    # 如果.py文件不存在则警告提示
    if total_files == 0:
        messagebox.showwarning("警告", "你选择的文件夹内不存在.py文件!")
        return
    # 获取当前处理进度,初始化为0
    current_file = 0
    # 设置进度条最大进度为100%
    max_progress = 100.0
    # 遍历指定目录及其子目录中的所有文件
    for root, dirs, files in os.walk(path):
        for name in files:
            # 筛选出.py文件
            if name.endswith(".py"):
                current_file += 1
                # 计算当前进度
                progress = (current_file / total_files) * max_progress
                # 更新进度条变量
                progress_var.set(progress)
                # 更新进度条显示
                progress_bar.update()
                # 更新Tkinter的GUI用户界面
                app.update()
                # 获取.py文件开始编译
                file_path = os.path.join(root, name)
                pyd_compile(file_path)

(6)选择指定需要批量处理的文件夹

# 打开文件选择对话框,用于选择需要批量编译成pyd的文件夹,文件夹内的文件名不能包含横杠“-”,否则无法选择文件夹
def select_batch_folder():
    batch_folder = filedialog.askdirectory(title="选择文件夹")
    if batch_folder:
        open_folder.delete(0, tk.END)
        open_folder.insert(0, batch_folder)
    folder_path = open_folder.get()
    # 遍历目录
    for root, dirs, files in os.walk(folder_path):
        for filename in files:
            # 检查文件名是否包含横杠“-”
            if '-' in filename:
                messagebox.showwarning("警告", f"警告:你选择的文件夹内文件名不能包含横杠'-':\n{os.path.join(root, filename)}")
                # 清除选择
                open_folder.delete(0, tk.END)

(7)点击按钮触发批量处理进程函数

# 添加处理进程
def compile_process():
    paths = open_folder.get()
    # 判断是否为空
    if not paths:
        messagebox.showwarning("警告", "“待编译的文件夹”不能为空!")
        return
    try:
        # 开始批量编译成pyd
        compile_all_file(paths)
    except Exception as e:
        messagebox.showerror("出现错误", str(e))
        return None
    messagebox.showinfo("完成", "Python文件编译完成!")

(8)主程序窗口设计

# 创建主窗口
app = tk.Tk()
app.title("py文件批量编译成pyd工具")
app.geometry("600x320")
app.resizable(False, False)

# 设置主窗口居中
app.update_idletasks()
window_width = app.winfo_width()
window_height = app.winfo_height()
screen_width = app.winfo_screenwidth()
screen_height = app.winfo_screenheight()
x = (screen_width - window_width) // 2
y = (screen_height - window_height) // 2
app.geometry(f"{window_width}x{window_height}+{x}+{y}")

# 初始化del_py变量
del_py = ""

# 待编译的文件夹选择
compile_frame = tk.Frame(app)
compile_frame.pack(pady=10, padx=20)

open_folder_label = tk.Label(compile_frame, text="选择待编译的文件夹:", width=18)
open_folder_label.grid(column=0, row=0, pady=5, padx=2, sticky="e")

open_folder = tk.Entry(compile_frame, width=40)
open_folder.grid(column=1, row=0, pady=5, padx=2, sticky="w")

open_folder_browse_button = tk.Button(compile_frame, text="浏览", command=select_batch_folder)
open_folder_browse_button.grid(column=2, row=0, pady=5, padx=2, sticky="w")

# 编译结果处理选择
position_label = tk.Label(compile_frame, pady=5, padx=2, text="编译结果处理:", width=12)
position_label.grid(column=0, row=1, sticky="e")

position_var = tk.StringVar()
position_option = ttk.Combobox(
    compile_frame,
    textvariable=position_var,
    values=["保留原文件", "删除原文件"],
    width=10,
    state="readonly",
)
position_option.current(0)
position_option.grid(column=1, row=1, pady=5, padx=2, sticky="w")

# 处理进度标签
progress_label = tk.Label(compile_frame, pady=5, padx=2, text="处理进度:", width=12)
progress_label.grid(column=0, row=2, sticky="e")

# 创建进度条控件
progress_var = tk.DoubleVar()
progress_bar = ttk.Progressbar(compile_frame, orient="horizontal", length=300, mode='determinate', variable=progress_var)
progress_bar.grid(column=1, row=2, pady=20, sticky="w")
# 初始化进度条显示为0
progress_var.set(0)
progress_bar.update()

# 开始批量编译按钮
start_button = tk.Button(compile_frame, text="开始批量编译", command=compile_process)
start_button.grid(column=0, row=3, pady=10, columnspan=2)

# 创建一个Text小部件,设置其大小
text_widget = tk.Text(compile_frame, font = ('黑体', 14), borderwidth=2, height=3, width=50)
text_widget.grid(column=0, row=4, pady=15, columnspan=3)

# 插入多行文本
text_widget.insert(tk.END, "注意:“编译结果处理”如果选择“删除原文件”,编译完成后会自动删除原py文件,此操作无法恢复、慎用!")

# 当前模块直接被执行
if __name__ == "__main__":
    # 运行主循环
    app.mainloop()

(9)py文件批量编译成pyd文件的完整代码如下所示:

import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import os, shutil, time, sys, glob

try:
    # 检测是否安装了cython模块
    import Cython
except ImportError: 
    # 如果没有安装,则提示先安装cython模块
    messagebox.showwarning("警告", "请先安装cython模块:pip install cython!")
    # 退出程序
    sys.exit()

# py编译函数
def pyd_compile(path):
    # 获取文件夹路径
    batch_folder = os.path.dirname(path)
    # 获取不带路径的文件名
    file_path = os.path.split(path)[1]
    # 获取“编译结果处理”选项
    del_py = position_var.get()
    # 将batch_folder设置为当前的工作目录,以便于运行Python代码
    os.chdir(batch_folder)
    # 在batch_folder工作目录中自动生成单独的pyd_setup.py文件
    with open('pyd_setup.py', 'w') as f:
        f.write('from setuptools import setup\n')
        f.write('from Cython.Build import cythonize\n')
        f.write('setup(\n')
        f.write("name='test',\n")
        f.write("ext_modules=cythonize('%s')\n" % file_path)
        f.write(")\n")
    # 开始编译py文件,将输出结果使用NUL丢弃,否则编译过程会输出大量信息到控制台
    os.system('python pyd_setup.py build_ext --inplace > NUL 2>&1')
    # 获取.py的文件名
    filename = file_path.split('.py')[0]
    # 程序暂停执行‌2秒
    time.sleep(2)
    # 获取pyd文件名
    pyd_name = '%s\\%s.pyd' % (batch_folder, filename)
    # 删除旧的pyd文件
    if os.path.exists(pyd_name):
        os.remove(pyd_name)
    # 获取编译后pyd文件的全名,类似***.cp38-win_amd64.pyd
    amd64_pyd = glob.glob(filename + "*.pyd")
    # 重命名,删除编译后多余的“cp38-win_amd64”文件名
    os.rename(amd64_pyd[0], pyd_name)
    # 删除编译后产生“.c”为后缀名的临时文件
    os.remove('%s.c' % filename)
    # 获取编译后生成的build文件夹
    build_batch_folder = os.path.join(batch_folder, 'build')
    # 删除编译后生成的build文件夹
    shutil.rmtree(build_batch_folder)
    # 最后删除编译过程自动生成的pyd_setup.py
    os.remove('pyd_setup.py')
    # 如果处理结果选项是“删除原文件",将删除py源文件,无法恢复,慎用!
    if del_py == '删除原文件':
        os.remove(file_path)

# 遍历指定目录下的全部py文件,包含子目录里的py文件
def compile_all_file(path):
    # 统计文件夹内的.py文件总数
    total_files = sum(1 for root, dirs, files in os.walk(path) for file in files if file.endswith('.py'))
    # 如果.py文件不存在则警告提示
    if total_files == 0:
        messagebox.showwarning("警告", "你选择的文件夹内不存在.py文件!")
        return
    # 获取当前处理进度,初始化为0
    current_file = 0
    # 设置进度条最大进度为100%
    max_progress = 100.0
    # 遍历指定目录及其子目录中的所有文件
    for root, dirs, files in os.walk(path):
        for name in files:
            # 筛选出.py文件
            if name.endswith(".py"):
                current_file += 1
                # 计算当前进度
                progress = (current_file / total_files) * max_progress
                # 更新进度条变量
                progress_var.set(progress)
                # 更新进度条显示
                progress_bar.update()
                # 更新Tkinter的GUI用户界面
                app.update()
                # 获取.py文件开始编译
                file_path = os.path.join(root, name)
                pyd_compile(file_path)

# 打开文件选择对话框,用于选择需要批量编译成pyd的文件夹,文件夹内的文件名不能包含横杠“-”,否则无法选择文件夹
def select_batch_folder():
    batch_folder = filedialog.askdirectory(title="选择文件夹")
    if batch_folder:
        open_folder.delete(0, tk.END)
        open_folder.insert(0, batch_folder)
    folder_path = open_folder.get()
    # 遍历目录
    for root, dirs, files in os.walk(folder_path):
        for filename in files:
            # 检查文件名是否包含横杠“-”
            if '-' in filename:
                messagebox.showwarning("警告", f"警告:你选择的文件夹内文件名不能包含横杠'-':\n{os.path.join(root, filename)}")
                # 清除选择
                open_folder.delete(0, tk.END)

# 添加处理进程
def compile_process():
    paths = open_folder.get()
    # 判断是否为空
    if not paths:
        messagebox.showwarning("警告", "“待编译的文件夹”不能为空!")
        return
    try:
        # 开始批量编译成pyd
        compile_all_file(paths)
    except Exception as e:
        messagebox.showerror("出现错误", str(e))
        return None
    messagebox.showinfo("完成", "Python文件编译完成!")

# 创建主窗口
app = tk.Tk()
app.title("py文件批量编译成pyd工具")
app.geometry("600x320")
app.resizable(False, False)

# 设置主窗口居中
app.update_idletasks()
window_width = app.winfo_width()
window_height = app.winfo_height()
screen_width = app.winfo_screenwidth()
screen_height = app.winfo_screenheight()
x = (screen_width - window_width) // 2
y = (screen_height - window_height) // 2
app.geometry(f"{window_width}x{window_height}+{x}+{y}")

# 初始化del_py变量
del_py = ""

# 待编译的文件夹选择
compile_frame = tk.Frame(app)
compile_frame.pack(pady=10, padx=20)

open_folder_label = tk.Label(compile_frame, text="选择待编译的文件夹:", width=18)
open_folder_label.grid(column=0, row=0, pady=5, padx=2, sticky="e")

open_folder = tk.Entry(compile_frame, width=40)
open_folder.grid(column=1, row=0, pady=5, padx=2, sticky="w")

open_folder_browse_button = tk.Button(compile_frame, text="浏览", command=select_batch_folder)
open_folder_browse_button.grid(column=2, row=0, pady=5, padx=2, sticky="w")

# 编译结果处理选择
position_label = tk.Label(compile_frame, pady=5, padx=2, text="编译结果处理:", width=12)
position_label.grid(column=0, row=1, sticky="e")

position_var = tk.StringVar()
position_option = ttk.Combobox(
    compile_frame,
    textvariable=position_var,
    values=["保留原文件", "删除原文件"],
    width=10,
    state="readonly",
)
position_option.current(0)
position_option.grid(column=1, row=1, pady=5, padx=2, sticky="w")

# 处理进度标签
progress_label = tk.Label(compile_frame, pady=5, padx=2, text="处理进度:", width=12)
progress_label.grid(column=0, row=2, sticky="e")

# 创建进度条控件
progress_var = tk.DoubleVar()
progress_bar = ttk.Progressbar(compile_frame, orient="horizontal", length=300, mode='determinate', variable=progress_var)
progress_bar.grid(column=1, row=2, pady=20, sticky="w")
# 初始化进度条显示为0
progress_var.set(0)
progress_bar.update()

# 开始批量编译按钮
start_button = tk.Button(compile_frame, text="开始批量编译", command=compile_process)
start_button.grid(column=0, row=3, pady=10, columnspan=2)

# 创建一个Text小部件,设置其大小
text_widget = tk.Text(compile_frame, font = ('黑体', 14), borderwidth=2, height=3, width=50)
text_widget.grid(column=0, row=4, pady=15, columnspan=3)

# 插入多行文本
text_widget.insert(tk.END, "注意:“编译结果处理”如果选择“删除原文件”,编译完成后会自动删除原py文件,此操作无法恢复、慎用!")

# 当前模块直接被执行
if __name__ == "__main__":
    # 运行主循环
    app.mainloop()