Python 语法简单,使用方便,我们可以使用它快速地编写程序和构建应用。在编写好程序之后,我们必然要进行程序的分发。
而这,就涉及到了源码保护的问题。我们不需要程序的使用者能够看到程序的源码。但是,Python 作为一门动态语言和脚本语言,运行通过它编写的程序,并不需要进行静态编译和打包的过程,对其代码进行加密是一件很麻烦、复杂和困难的事情。
虽说 Cython
的主要目的是带来性能的提升,但是基于它的原理:将 .py
/.pyx
编译为 .c
文件,再将 .c
文件编译为 .so
(Unix) 或 .pyd
(Windows),其带来的另一个好处就是难以破解。而且文件可直接import
由于项目交付需要,尝试使用Cython对项目进行编译,额外的好处是可以继续使用现有的docker环境、镜像、以及docker-compose进行部署
一、环境
需要使用docker上使用的python编译,python版本和gcc库都会影响文件的使用
gcc,这里使用的是gcc-7
Cython==0.29.35, 这个版本不是必须的
1、gcc安装
这里介绍ubuntu18.04版本下gcc的安装过程
-
编辑apt源
vim /etc/apt/sources.list
用下面内容替换
# 默认注释了源码镜像以提高 apt update 速度,如有需要可自行取消注释 deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic main restricted universe multiverse # deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic main restricted universe multiverse deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-updates main restricted universe multiverse # deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-updates main restricted universe multiverse deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-backports main restricted universe multiverse # deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-backports main restricted universe multiverse # deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-security main restricted universe multiverse # # deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-security main restricted universe multiverse deb http://security.ubuntu.com/ubuntu/ bionic-security main restricted universe multiverse # deb-src http://security.ubuntu.com/ubuntu/ bionic-security main restricted universe multiverse
-
更新apt
apt-get update
-
由于这里用的python3.7,还需安装python3.7-dev,否则gcc编译时会找不到"Python.h"等头文件默认的 Ubuntu 软件源包含了一个软件包组,名称为 “build-essential”,它包含了 GNU 编辑器集合,GNU 调试器,和其他编译软件所必需的开发库和工具。
apt-get install gcc-7 build-essential python3.7-dev -y
2、Cython安装
这个简单,直接pip安装即可
pip3.7 install Cython==0.29.35
二、编译
由于Cython会出现目录结构错误问题,需要编写代码控制编译过程,并清理编译中间产物。需注意留下启动入口文件
编写setup.py,内容如下。并将文件放到项目根目录
import os
import platform
import shutil
from distutils.core import setup
from Cython.Build import cythonize
package_name = "package"
ignore_dirs = ['__pycache__', 'build', 'dist', package_name, 'static', 'bk', 'img_data', 'model']
ignore_file_in_root = ['setup.py', 'setup_main.py', 'requirements.txt', 'proxy.py', 'success.flag']
# 项目子目录下不用(能)转译的'py文件(夹)名
ignore_names = {}
# 需要直接拷贝到编译文件夹的文件
need_move = ['proxy.py']
# 需要编译的文件夹绝对路径
base_dir = os.path.dirname(os.path.abspath(__file__))
# 将以上不需要转译的文件(夹)加上绝对路径
ignore_files = {os.path.join(base_dir, x) for x in ignore_dirs}
ignore_files.update({os.path.join(base_dir, x) for x in ignore_file_in_root})
ignore_files.update({os.path.join(base_dir, x) for x in need_move})
need_move_files = [os.path.join(base_dir, x) for x in need_move]
files = []
build_error_files = []
if os.path.exists(os.path.join(base_dir, package_name)):
shutil.rmtree(os.path.join(base_dir, package_name))
if os.path.exists(os.path.join(base_dir, 'build')):
shutil.rmtree(os.path.join(base_dir, 'build'))
package_path = os.path.join(base_dir, package_name)
# 若没有打包文件夹,则生成一个
if not os.path.exists(package_path):
os.mkdir(package_path)
sys = platform.system()
build_dir = ""
# 由于生成文件的路径有错误,只能看目标文件夹是否出现了新的文件,而且没办法使用多线程处理
def get_new_file(original_file_name):
global build_dir
if not build_dir:
for file_dir in os.listdir(os.path.join(base_dir, "build")):
if sys == "Windows":
if file_dir.startswith("lib.win"):
build_dir = os.path.join(base_dir, "build", file_dir)
break
elif sys == "Linux":
if file_dir.startswith("lib.linux"):
build_dir = os.path.join(base_dir, "build", file_dir)
break
print("build_dir: ", build_dir)
if not build_dir:
raise Exception("build_dir is None")
for root, dirs, files in os.walk(build_dir):
for file in files:
file_path = os.path.join(root, file)
if file.startswith(original_file_name):
return file_path
raise Exception("No new file generated")
# 获取全部待处理文件
def get_files(dir_path):
for file_dir in os.listdir(dir_path):
file_dir_path = os.path.join(dir_path, file_dir)
if file_dir.endswith(".c"):
os.remove(file_dir_path)
continue
elif file_dir in ignore_names:
continue
elif file_dir.endswith(".pyc"):
continue
if dir_path == base_dir and file_dir_path in ignore_files:
continue
if os.path.isdir(file_dir_path):
get_files(file_dir_path)
else:
files.append(file_dir_path)
# 如果是py文件,需要在文件头加上cython的语句,然后编译,编译完成后再去掉cython的语句
# 如果是其他文件,直接拷贝到目标文件夹
def process_file(file_path):
source = file_path
build_error = False
if file_path.endswith(".py"):
_, file_name = os.path.split(file_path)
with open(file_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(file_path, 'w', encoding='utf8') as f1:
f1.write(content)
try:
setup(name="asr_attack", ext_modules=cythonize([file_path]), script_args=["build_ext"])
except Exception as e:
print("cythonize error: ", e)
build_error_files.append(file_path)
build_error = True
with open(file_path, 'r', encoding='utf8') as f:
content = f.read()
if content.startswith('# cython: language_level=3'):
content = content.split("\n", 1)[1]
with open(file_path, 'w', encoding='utf8') as f1:
f1.write(content)
if not build_error:
source = get_new_file(file_name.split(".")[0])
print("source: ", source)
dir_name, file_name = os.path.split(file_path.replace(base_dir + os.path.sep, ""))
if dir_name:
dir_name = os.path.join(package_path, dir_name)
if not os.path.exists(dir_name):
os.makedirs(dir_name, exist_ok=True)
else:
dir_name = package_path
suffix = source.split(".")[-1]
file_name = f'{file_name.split(".")[0]}.{suffix}'
shutil.copy(source, os.path.join(dir_name, file_name))
if not build_error and (source.endswith(".so") or source.endswith(".pyd")):
os.remove(source)
if __name__ == '__main__':
get_files(base_dir)
print("*" * 50, "get_files finish", "*" * 50)
print("*" * 50, "need process files: ", len(files), "*" * 50)
i = 1
for file in files:
print("*" * 50, f"processing: {i} / {len(files)}", "*" * 50)
process_file(file)
i += 1
print("*" * 50, "process_file finish" + "*" * 50)
for file in need_move_files:
shutil.copy(file, os.path.join(package_path, os.path.basename(file)))
print("*" * 50, "copy_files finish" + "*" * 50)
for root, dirs, files in os.walk(base_dir):
for file in files:
if file.endswith(".c"):
os.remove(os.path.join(root, file))
if os.path.exists(os.path.join(base_dir, 'build')):
shutil.rmtree(os.path.join(base_dir, 'build'))
print("*" * 50, "clean_files finish" + "*" * 50)
# 将编译失败的文件打印出来
if build_error_files:
print("*" * 50, "build_error_files", "*" * 50)
for file in build_error_files:
print(file)
执行
python3.7 setup.py
结束后,package目录下就是项目文件了
三、其他问题
py文件不能出现直接引用同级目录文件的情况
如果某个文件执行错误,可以将有py文件替换掉so文件(个别错误可以这么办)