在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文件效果图如下所示:
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()