基于Cython编译整个Python项目并保留原项目结构
Cython实就是把py 代码编译成 C 或 C++ 代码来执行(类似于windows中的 .dll 动态链接库),在Linux 上会生成.so二进制文件,Windows下为.pyd,所以还有一个作用是加速代码的执行效率。
目录
安装 Microsoft Visual Studio 2022(windows下)
前言
很多人需要将 python 代码部署到其他端上,而 .pyc 文件
很容易被反编译并直接运行的,因为它是解释型语言。不像 C 或者 java 可以编译后生成机器码直接部署。还有就是把项目打包成 .exe
文件在 windows 上运行,但是若在 Linux 平台显然不可取,最后选择了使用Cython这个库来对项目进行加密。
Cython
实质就是把py 代码编译成 C 或 C++ 代码来执行(类似于windows中的 .dll 动态链接库),在Linux 上会生成 . so
二进制文件,Windows下为 .pyd
,所以还有一个作用是加速代码的执行效率。
【注意】 .pyd 文件不能在 Linux 下使用,反之亦然!但还有一些限制如项目中不能删除 __init__.py
否者包导入会失败。详细可参考官方文档。
准备工作
安装 Cython
pip install Cython
安装 Microsoft Visual Studio 2022(windows下)
Windows 下需要安装 VS 才能编译,Linux 下只要安装 gcc 等即可,不像windows那样需要安装这么臃肿的软件/组件。
附 Linux 下的各种依赖包(CentOS)
yum -y install gcc gcc-c++ zlib-devel bzip2-devel ncurses-devel sqlite-devel readline-devel tk-devel libffi-devel expat-devel gdbm-devel make
安装所需组件(Python)
安装所需组件(C++)
编写项目编译文件(两个)
这两个文件放在项目根目录,配置完成后直接用终端(如CMD)运行 setup_main.py 即可,注意备份原项目文件。
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
import os
# 项目根目录下不用(能)转译的py文件(夹)名,用于启动的入口脚本文件一定要加进来
ignore_files = ['build', 'package', 'venv', '__pycache__', '.git', 'setup.py', 'setup_main.py', 'server.py', '__init__.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()
后记
1. 入口文件必须要留一个,什么意思呢?比如你用命令行:python server.py 启动的那个文件必须要是 .py 文件,要是你觉得 server.py 也要编译隐藏,那就打不了搞一个专门启动 server.py 文件的 .py 文件,再套一层。
2. __init__.py 文件要排除编译,否则会出现引入失败问题,项目子文件夹下必须要有 __init__.py空的都行,否则也可能会出现引入失败问题。
3. 开源发文不易,如果对你有帮助,请 点赞+收藏+关注 三连让我知道👍🏻
更多推荐
所有评论(0)