.so
文件的特点是看不到源码,但是依然能被其他.py
文件调用。将py
文件编译为so
文件可以在一定程度上减小源码泄露的可能性。这篇文章以.py
编译为.so
为例,记录完整的加密流程。
yum install gcc
pip install cython
创建一个名为setup.py
的脚本,并写入如下代码:
import sys
from distutils.core import setup
try:
from Cython.Build import cythonize
except:
print("你没有安装Cython,请安装 pip install Cython")
print("本项目需要 Visual Studio 2022 的C++开发支持,请确认安装了相应组件")
arg_list = sys.argv
f_name = arg_list[1]
sys.argv.pop(1)
setup(ext_modules=cythonize(f_name))
再编写一个setup_main.py
脚本。这个脚本主要进行额外的一些处理,比如:在加密的时候,排除某些文件不加密,排除某些文件不打包到加密目录中;在加密之后,将编译后的.c
文件删除等等。详细的配置信息在注释里面都有,根据自己项目的需求进行修改。
import os
# 项目根目录下不用(能)转译的py文件(夹)名,用于启动的入口脚本文件一定要加进来
ignore_files = ['build', 'package', 'venv', '__pycache__', '.git', 'setup.py', 'setup_main.py', '__init__.py', 'inference_app.py']
# 项目子目录下不用(能)转译的'py文件(夹)名
ignore_names = ['__init__.py']
# 不需要原样复制到编译文件夹的文件或者文件夹
ignore_move = ['venv', '__pycache__', 'server.log', 'setup.py', 'setup_main.py']
# 需要编译的文件夹绝对路径
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# 将以上不需要转译的文件(夹)加上绝对路径
ignore_files = [os.path.join(BASE_DIR, x) for x in ignore_files]
# 是否将编译打包到指定文件夹内 (True),还是和源文件在同一目录下(False),默认True
package = True
# 打包文件夹名 (package = True 时有效)
package_name = "package"
# 打包文件夹路径 (package = True 时有效)
package_path = os.path.join(BASE_DIR, package_name)
# 若没有打包文件夹,则生成一个
if not os.path.exists(package_path):
os.mkdir(package_path)
translate_pys = []
# 编译需要的py文件
def translate_dir(path):
pathes = os.listdir(path)
# if path != BASE_DIR and path != '__init__.py' in pathes:
# with open(os.path.join(path, '__init__.py'), 'w', encoding='utf8') as f:
# pass
for p in pathes:
if p in ignore_names:
continue
if p.startswith('__') or p.startswith('.') or p.startswith('build'):
continue
f_path = os.path.join(path, p)
if f_path in ignore_files:
continue
if os.path.isdir(f_path):
translate_dir(f_path)
else:
if not f_path.endswith('.py') and not f_path.endswith('.pyx'):
continue
if f_path.endswith('__init__.py') or f_path.endswith('__init__.pyx'):
continue
with open(f_path, 'r', encoding='utf8') as f:
content = f.read()
if not content.startswith('# cython: language_level=3'):
content = '# cython: language_level=3\n' + content
with open(f_path, 'w', encoding='utf8') as f1:
f1.write(content)
os.system('python setup.py ' + f_path + ' build_ext --inplace')
translate_pys.append(f_path)
f_name = '.'.join(f_path.split('.')[:-1])
py_file = '.'.join([f_name, 'py'])
c_file = '.'.join([f_name, 'c'])
print(f"f_path: {f_path}, c_file: {c_file}, py_file: {py_file}")
if os.path.exists(c_file):
os.remove(c_file)
# 移除编译临时文件
def remove_dir(path, rm_path=True):
if not os.path.exists(path):
return
pathes = os.listdir(path)
for p in pathes:
f_path = os.path.join(path, p)
if os.path.isdir(f_path):
remove_dir(f_path, False)
os.rmdir(f_path)
else:
os.remove(f_path)
if rm_path:
os.rmdir(path)
# 移动编译后的文件至指定目录
def mv_to_packages(path=BASE_DIR):
pathes = os.listdir(path)
for p in pathes:
if p.startswith('.'):
continue
if p in ignore_move:
continue
f_path = os.path.join(path, p)
if f_path == package_path:
continue
p_f_path = f_path.replace(BASE_DIR, package_path)
if os.path.isdir(f_path):
if not os.path.exists(p_f_path):
os.mkdir(p_f_path)
mv_to_packages(f_path)
else:
if not f_path.endswith('.py') or f_path not in translate_pys:
with open(f_path, 'rb') as f:
content = f.read()
with open(p_f_path, 'wb') as f:
f.write(content)
if f_path.endswith('.pyd') or f_path.endswith('.so'):
os.remove(f_path)
# 将编译后的文件重命名成:源文件名+.pyd,否则编译后的文件名会类似:myUtils.cp39-win_amd64.pyd
def batch_rename(src_path):
filenames = os.listdir(src_path)
same_name = []
count = 0
for filename in filenames:
old_name = os.path.join(src_path, filename)
if old_name == package_path:
continue
if os.path.isdir(old_name):
batch_rename(old_name)
if filename[-4:] == ".pyd" or filename[-3:] == ".so":
old_pyd = filename.split(".")
new_pyd = str(old_pyd[0]) + "." + str(old_pyd[2])
else:
continue
change_name = new_pyd
count += 1
new_name = os.path.join(src_path, change_name)
if change_name in filenames:
same_name.append(change_name)
continue
os.rename(old_name, new_name)
def run():
translate_dir(BASE_DIR)
remove_dir(os.path.join(BASE_DIR, 'build'))
if package:
mv_to_packages()
batch_rename(os.path.join(BASE_DIR, package_name))
if __name__ == '__main__':
run()
这个函数是从别人的博客那里复制粘贴过来的,但是博客链接我找不到了。。。非常感谢这位博主的分享,代码亲测有效!
执行命令:python setup_main.py
。如果执行成功,则会在当前目录下产生一个名为package_name
的目录。然后就可以按照同样的方式启动程序了。
这里有一个小坑需要注意一下。如果你有写函数参数类型声明的习惯,在编译之后,可能项目就无法运行了。
具体来说,当你写了类似如下的类型声明时:
# 错误写法
def fn(numbers: list[int]) -> int:
pass
在运行编译之后的项目时,会出现报错:
typeerror: type object is not subscriptable
解决方案:把所有类型声明中,带有中括号的部分删掉,不然编译成c语言后,程序会以为这个是索引。
# 正确写法
def fn(numbers: list) -> int:
pass