pyc文件是一种二进制文件,由原生Python文件经过编译后所生成的,py文件编译成pyc文件后加载速度更快而且提高了代码的安全性。pyc的内容与python的版本相关,不同版本编译的pyc文件不一样。
# 安装
pip install compileall2
import compileall
# compile_file编译单个文件
compileall.compile_file('main.py')
# compile_dir 函数编译文件夹下的py文件
compileall.compile_dir('Lib/', force=True)
# 使用多处理器编译
compileall.compile_dir('Lib/', workers=2)
代码混淆是指在不改变代码逻辑的情况下,对代码结构进行变换,通过一些带有混淆性质的命名、注释等,使代码变得晦涩难懂,从而达到保护代码的作用。
可以这样下手:
移除注释和文档。没有这些说明,在一些关键逻辑上就没那么容易明白了。
改变缩进。完美的缩进看着才舒服,如果缩进忽长忽短,看着也一定闹心。
在 tokens 中间加入一定空格。这就和改变缩进的效果差不多。
重命名函数、类、变量。命名直接影响了可读性,乱七八糟的名字可是阅读理解的一大障碍。
在空白行插入无效代码。这就是障眼法,用无关代码来打乱阅读节奏。
实现方法:
网站混淆:Oxyry Python Obfuscator - The most reliable python obfuscator in the world, 破解: https://www.cnblogs.com/Eeyhan/p/13154217.html
使用 pyobfuscate 库进行混淆 (2条消息) Python 源码混淆与加密_pyobfuscate_早起的python的博客-CSDN博客
优点:
简单方便,提高了一点源码破解门槛
兼容性好,只要源码逻辑能够做到兼容,混淆代码亦能
不足:
只能对单个文件混淆,无法做到多个互相有联系的源码文件的联动混淆
代码结构未发生变化,也能获取字节码,破解难度不大
类似的还有pyinstaller、Nuitka。
使用py2exe进行打包的步骤:
1、编写入口文件。本示例中取名为hello.py:
print('Hello World')
2、编写setup.py
from distutils.core import setup
import py2exe
setup(console=['hello.py'])
3、生成可执行文件
python setup.py py2exe
优点:
能够直接打包成 exe,方便分发和执行
破解门槛比 .pyc 更高一些
不足:
兼容性差,只能运行在 Windows 系统上
生成的可执行文件内的布局是明确、公开的,可以找到源码对应的.pyc文件,进而反编译出源码
1、安装pyinstaller
2、在Terminal下输入:“pyinstaller -F -w *.py” 就可以制作出exe。生成的文件放在同目录dist下。
常用选项
-F(注意大写)是所有库文件打包成一个exe。不加-F参数生成一堆文件,但运行快,压缩后比单个exe文件还小一点点。 加-F参数生成一个exe文件,运行起来慢。
-D:默认选项,创建一个目录,包含exe文件以及大量依赖文件;
-a,–ascii 不包含 Unicode 字符集支持;
-d,–debug 产生 debug 版本的可执行文件;
-i: 后接图标文件名,后缀是.ico,表示用自定义图标生成exe程序
-c:默认选项,使用控制台(就是类似cmd的黑框);(仅对 Windows 有效);
-w: 生成的exe程序不带窗口执行(仅对 Windows 有效);
-o DIR,–out=DIR 指定 spec 文件的生成目录。如果没有指定,则默认使用当前目录来生成 spec 文件;
-p 表示你自己自定义需要加载的类路径,一般情况下用不到;
-n NAME,–name=NAME 指定项目(产生的 spec)名字。如果省略该选项,那么第一个脚本的主文件名将作为 spec 的名字;
-i 选择图标
示例:
book.py:
import webbrowser
url="https://blog.csdn.net/leiwuhen92?type=blog"
print("hello world")
webbrowser.open_new(url)
打包指令:
pyinstaller -F -i test.ico.ico -w book.py
优点:
缺点:
打包超级慢,启动超级慢
生成的exe比较大
可以反编译
Python可执行文件反编译教程(exe转py)_python_脚本之家 (jb51.net)
(6条消息) Python 反编译:pycdc工具的使用_小嗷犬的博客-CSDN博客
在线Python pyc文件编译与反编译 (lddgo.net)
使用python打包工具nuitka进行编译打包 - 知乎 (zhihu.com)
nuitka的作用是将python程序转换成C语言的可执行elf文件。这样在运行时就可以享受到C语言处理过程中的优化,提高速度。经测试,Nuitka打包后的exe比Pyinstaller打包后的exe运行速度提升30%。
对于第三方依赖包较多的项目(比如需要import torch,tensorflow,cv2,numpy,pandas,geopy等等)而言,这里最好打包的方式是只将属于自己的代码转成C++,不管这些大型的第三方包!
指令:
python -m nuitka --standalone --show-memory --show-progress --nofollow-imports --plugin-enable=qt-plugins --follow-import-to=utils,src --output-dir=out --windows-icon-from-ico=./logo.ico demo.py
参数:
--standalone:方便移植到其他机器,不用再安装python
--show-memory --show-progress:展示整个安装的进度过程
--nofollow-imports:不编译代码中所有的import,比如keras,numpy之类的。
--plugin-enable=qt-plugins:我这里用到pyqt5来做界面的,这里nuitka有其对应的插件。
--follow-import-to=utils,src:需要编译成C++代码的指定的2个包含源码的文件夹,这里用,来进行分隔。
--output-dir=out:指定输出的结果路径为out。
--windows-icon-from-ico=./logo.ico:指定生成的exe的图标为logo.ico这个图标,这里推荐一个将图片转成ico格式文件的网站(比特虫)。
--windows-disable-console:运行exe取消弹框。这里没有放上去是因为我们还需要调试,可能哪里还有问题之类的。
原理:先用cython将python语言代码转换为c语言代码,然后用c编译器(gcc)生成可执行文件或动态链接库。
Cython是一个编程语言,它通过类似Python的语法来编写C扩展并可以被Python调用,既具备了Python快速开发的特点,又可以让代码运行起来像C一样快,同时还可以方便地调用C library。
Cython是属于python的超集,用于编写python的c扩展语言。
pyx文件由 Cython 编译为.c文件,包含 python 扩展模块的代码。.c文件由 C 编译器编译为.so文件(或 Windows 上的.pyd)。
生成的.so文件或pyd文件是D语言(C/C++综合进化版本)生成的二进制文件,理论上很难反编译。
Note: 纯python源码被cython编译后,因为没有使用类型标注等cython语法,故编译后的动态链接库和可执行文件,主要依赖python的运行时,并不依赖C/C++运行时,主要由python解释器执行,多线程GIL的问题同样存在,性能提升有限,但对for循环、大的列表对象遍历性能有明显优化效果。
# 安装cython
pip3 install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple cython
# 安装c编译器(linux需安装python-devel, gcc)
centos: yum install python-devel gcc
ubuntu: apt-get install build-essential
先用cython将python语言代码转换为c语言代码,然后用c编译器(gcc)生成可执行文件或动态链接库。
通过shell 或 python脚本的方式,将项目启动的入口py编译成可执行文件,将项目的其他.py文件编译成.so(__init__.py除外)
Note: __init__.py文件定义了python的包结构,为了使cython编译后的.so能按照正常路径import,__init__.py不能被编译,故为了保护代码,整个项目的所有__init__.py文件不建议放业务相关代码。
目录结构如下:
test/
├── test.py
├── main.py
test.py:
def hello():
print('hello!')
main.py:文件头的#!/usr/bin/python3.8标记是否程序启动文件
#!/usr/bin/python3.8
from test import hello
if __name__ == "__main__":
hello()
4.3.1.1、将启动main.py编译成二进制可执行文件main
main.py ---> main.c ---> main.o ---> main :
# step1: 将python代码翻译成c代码(main.py -> main.c)
cython -D -3 --directive always_allow_keywords=true --embed main.py
# step2: 将c代码编译为目标文件(main.c -> main.o)
gcc -c main.c -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing -I /usr/include/python3.8 -L /usr/bin -lpython3.8 -o main.o
# step3: 将目标文件编译为二进制可执行文件(main.o -> main)
gcc main.o -I /usr/include/python3.8 -L /usr/bin -lpython3.8 -o main
main.py ---> main.c ---> main:
# step1: 将python代码翻译成c代码(main.py -> main.c)
cython -D -3 --directive always_allow_keywords=true --embed main.py
# step2: 将c代码编译为二进制可执行文件(main.c -> main)
gcc main.c -I /usr/include/python3.8 -L /usr/bin -lpython3.8 -o main
4.3.1.2、将test.py编译为动态链接库test.so
test.py ---> test.c ---> test.so:
# step1: 将python代码翻译成c代码(test.py -> test.c)
cython -D -3 --directive always_allow_keywords=true test.py
# step2: 将c代码编译为linux动态链接库文件(test.c -> test.so)
gcc test.c -shared -pthread -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing -I /usr/include/python3.8 -L /usr/bin -lpython3.8 -o test.so
cython参数说明:
-D, --no-docstrings, Strip docstrings from the compiled module.
-o, --output-file Specify name of generated C file
-2 Compile based on Python-2 syntax and code semantics.
-3 Compile based on Python-3 syntax and code semantics.
gcc参数说明:
-shared:
编译动态库时要用到
-pthread:
在Linux中要用到多线程时,需要链接pthread库
-fPIC:
作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code),
则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意
位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。
-fwrapv:
它定义了溢出时候编译器的行为——采用二补码的方式进行操作
-O参数
这是一个程序优化参数,一般用-O2就是,用来优化程序用的
-O2:
会尝试更多的寄存器级的优化以及指令级的优化,它会在编译期间占用更多的内存和编译时间。
-O3: 在O2的基础上进行更多的优化
-Wall:
编译时 显示Warning警告,但只会显示编译器认为会出现错误的警告
-fno-strict-aliasing:
“-fstrict-aliasing”表示启用严格别名规则,“-fno-strict-aliasing”表示禁用严格别名规则,当gcc的编译优化参数为“-O2”、“-O3”和“-Os”时,默认会打开“-fstrict-aliasing”。
-I (大写的i):
是用来指定头文件目录
-I /home/hello/include表示将/home/hello/include目录作为第一个寻找头文件的目录,寻找的顺序是:/home/hello/include-->/usr/include-->/usr/local/include
-l:
-l(小写的 L)参数就是用来指定程序要链接的库,-l参数紧接着就是库名,把库文件名的头lib和尾.so去掉就是库名了,例如我们要用libtest.so库库,编译时加上-ltest参数就能用上了
windows系统使用cython需要确保已安装C/C++编译器且环境变量正确配置,cython能找到编译器。windows系统可使用MSVC(Microsoft Visual C/C++)或者clang编译器。
将test.py编译为动态链接库test.pyd:
test.py ---> test.c ---> test.pyd:
# step1: 将python代码翻译成c代码(test.py -> test.c)
cython -D -3 --directive always_allow_keywords=true test.py
# step2: 将c代码编译为windows动态链接库文件(test.c -> test.pyd)
cythonize -i test.c
最后得到windows下的动态链接库文件test.cp39-win_amd64.pyd,还需要将文件名重命名为test.pyd。
cythonize参数说明:
-b, --build build extension modules using distutils
-i, --inplace build extension modules in place using distutils(implies -b),即将编译后的扩展模块直接放在与test.py同级的目录中。
https://github.com/leiwuhen92/cython_test
main.py:主函数入口
#!/usr/bin/python3.8
from test import hello
from compute import compute
if __name__ == "__main__":
hello()
compute.is_leap_year(1992)
test.py:
def hello():
print('hello!')
compute/compute.py:
def is_leap_year(year):
if year%4==0 and year%100!=0 or year%400==0:
print(year,"是闰年")
else:
print(year,"不是闰年")
if __name__ == "__main__":
is_leap_year(1992)
setup.py:
import pathlib
import shutil
from subprocess import Popen, PIPE, STDOUT
# python编译so
def py_to_so(py_to_so_list):
"""
:param py_to_so_list: py文件列表,例如[test.py,]
:return:
"""
for py_to_so_item in py_to_so_list:
# 将python代码翻译成c代码
basename = py_to_so_item[:-3]
py_to_c = "cython -D -3 --directive always_allow_keywords=true --embed {}".format(py_to_so_item)
pl = Popen(py_to_c, shell=True, stdout=PIPE, stderr=STDOUT)
pl.communicate()[0].decode('utf-8', errors='ignore')
# 将c代码编译为linux动态链接库文件(
c_to_so = "gcc {} -shared -pthread -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing -I /usr/include/python3.8 -L /usr/bin -lpython3.8 -o {}".format(basename + ".c", basename + ".so")
p2 = Popen(c_to_so, shell=True, stdout=PIPE, stderr=STDOUT)
p2.communicate()[0].decode('utf-8', errors='ignore')
def py_to_bin(py_to_bin_list):
"""
:param py_to_bin_list: py文件列表,例如[main.py,]
:return:
"""
for py_to_bin_item in py_to_bin_list:
basename = py_to_bin_item[:-3]
# 将python代码翻译成c代码
py_to_c = "cython -D -3 --directive always_allow_keywords=true --embed {}".format(py_to_bin_item)
p1 = Popen(py_to_c, shell=True, stdout=PIPE, stderr=STDOUT)
p1.communicate()[0].decode('utf-8', errors='ignore')
# 将c代码编译为二进制可执行文件
c_to_bin = "gcc {} -I /usr/include/python3.8 -L /usr/bin -lpython3.8 -o {}".format(basename + ".c", basename)
p2 = Popen(c_to_bin, shell=True, stdout=PIPE, stderr=STDOUT)
p2.communicate()[0].decode('utf-8', errors='ignore')
def clean_build(project):
"""
清理编译文件夹中的文件
:param project:
:return:
"""
# 清理py文件
for i in project.glob("**/*.py"):
if i.name != "__init__.py" or i.name != "setup.py":
i.unlink()
# 清理c文件
for j in project.glob("**/*.c"):
j.unlink()
# 清理__pycache__文件夹
for k in project.glob("**/__pycache__"):
shutil.rmtree(k)
if __name__ == "__main__":
# 源文件夹
project_pathlib = pathlib.Path.cwd()
py_to_bin_list = ["main.py"]
py_to_bin(py_to_bin_list)
py_to_so_list = ["test.py"]
for py in project_pathlib.glob("**/*.py"):
if py.name != "__init__.py" and py.name != "setup.py" and py.name != "main.py":
py_to_so_list.append(str(py))
py_to_so(py_to_so_list)
clean_build(project_pathlib)
优点:
生成的二进制so(linux)或.pyd(windows)文件难以破解
性能会提升
不足:
兼容性稍差,对于不同版本的操作系统,可能需要重新编译
虽然支持大多数Python代码,但如果一旦发现部分代码不支持,完善成本较高