基于Cython编译整个Python项目并保留原项目结构

  • Post author:
  • Post category:python



目录


前言


准备工作


安装 Cython


安装 Microsoft Visual Studio 2022(windows下)


安装所需组件(Python)


安装所需组件(C++)


编写项目编译文件(两个)


setup.py


setup_main.py


后记


前言

很多人需要将 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. 开源发文不易,如果对你有帮助,请

点赞+收藏+关注

三连让我知道

👍🏻



版权声明:本文为qq_41313821原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。