1. 程式人生 > >Python 專案轉.so動態庫

Python 專案轉.so動態庫

最近, 作者遇到一個需求, 需要把Python的工程部署到別的叢集, 但是又要保證Python程式碼的安全性. 於是上網搜尋, 搜到幾個解決方案, 但是都不是符合需求. 綜合搜到的幾個解決方案, 最終作者採用了編譯成so動態庫的方式釋出.

首先說一下搜到到幾個解決方案, 以及它們的優缺點

  1. 編譯成pyc釋出
    優點: 操作簡單
    缺點: 可以被反編譯
  2. cx_freeze
    優點: 可以通過freeze命令直接把一個專案所有的依賴生成一個二進位制, 所以部署到新的環境時, 十分方便
    缺點: freeze命令如果工程專案很大的話, 速度非常慢, 而且其生成的Python程式碼其實也是pyc, 可以被反編譯
  3. pyminifier
    優點: 通過程式碼混淆的方式保護程式碼的安全
    缺點: 貌似, 只對單個檔案的混淆其作用, 如果是一個工程專案就不好使了
  4. cython編譯成動態庫
    優點: 可以將程式碼編譯成.so動態庫, 起到程式碼保護的作用
    缺點: 編譯速度太慢了

綜合以上幾個優缺點, 作者最終選擇了通過cython編譯成動態庫的方式, 來達到保護Python程式碼的目的, cython官方文件

說下具體的做法和原理:
cython首先會把python程式碼翻譯成C語言程式碼, 然後cython在將其編譯成.so動態庫, 最後, 在編譯好的build/lib.linux-x86_64-2.7(不同的平臺和python版本這個目錄是不一樣, 作者的是linux平臺, Python2.7版本)資料夾中, 直接引用即可.
但是這裡有一個坑, 如果你編譯的是一個Python的庫, 那麼你的build/lib.linux-x86_64-2.7中的庫檔案中, 每個庫裡必須有一個__init__.py

檔案, 所以, 下面的程式碼會首先進行一個把一個空的__init__.py檔案拷貝到對應的庫中的操作, 然後搜尋所有的.py檔案, 將其編譯成動態庫, 然後把所有的非.py檔案, 移動到原目錄對應的位置. 下面是對應的轉換的setup.py檔案和例子

setup.py檔案原始碼

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# @File    : setup.py
# @Time    : 2018/12/04
# @Author  : spxcds ([email protected])

import os
import sys
import shutil
import numpy
import tempfile

from setuptools import setup
from setuptools.extension import Extension

from Cython.Build import cythonize
from Cython.Distutils import build_ext

import platform

build_root_dir = 'build/lib.' + platform.system().lower() + '-' + platform.machine() + '-' + str(
    sys.version_info.major) + '.' + str(sys.version_info.minor)

print(build_root_dir)

extensions = []
ignore_folders = ['build', 'test', 'tests']
conf_folders = ['conf']


def get_root_path(root):
    if os.path.dirname(root) in ['', '.']:
        return os.path.basename(root)
    else:
        return get_root_path(os.path.dirname(root))


def copy_file(src, dest):
    if os.path.exists(dest):
        return

    if not os.path.exists(os.path.dirname(dest)):
        os.makedirs(os.path.dirname(dest))
    if os.path.isdir(src):
        shutil.copytree(src, dest)
    else:
        shutil.copyfile(src, dest)


def touch_init_file():
    init_file_name = os.path.join(tempfile.mkdtemp(), '__init__.py')
    with open(init_file_name, 'w'):
        pass
    return init_file_name


init_file = touch_init_file()
print(init_file)


def compose_extensions(root='.'):
    for file_ in os.listdir(root):
        abs_file = os.path.join(root, file_)

        if os.path.isfile(abs_file):
            if abs_file.endswith('.py'):
                extensions.append(Extension(get_root_path(abs_file) + '.*', [abs_file]))
            elif abs_file.endswith('.c') or abs_file.endswith('.pyc'):
                continue
            else:
                copy_file(abs_file, os.path.join(build_root_dir, abs_file))
            if abs_file.endswith('__init__.py'):
                copy_file(init_file, os.path.join(build_root_dir, abs_file))

        else:
            if os.path.basename(abs_file) in ignore_folders:
                continue
            if os.path.basename(abs_file) in conf_folders:
                copy_file(abs_file, os.path.join(build_root_dir, abs_file))
            compose_extensions(abs_file)


compose_extensions()
os.remove(init_file)

setup(
    name='my_project',
    version='1.0',
    ext_modules=cythonize(
        extensions,
        nthreads=16,
        compiler_directives=dict(always_allow_keywords=True),
        include_path=[numpy.get_include()]),
    cmdclass=dict(build_ext=build_ext))

# python setup.py build_ext

下面是一個例子

目錄結構是這樣子的

.
├── main.py
├── mypkg
│   ├── foo.py
│   ├── __init__.py
│   └── t
│       ├── __init__.py
│       └── t.py
└── setup.py

然後執行命令python setup.py build_ext 即可看到新的目錄結構

├── build
│   ├── lib.linux-x86_64-2.7
│   │   ├── main.so
│   │   ├── mypkg
│   │   │   ├── foo.so
│   │   │   ├── __init__.py
│   │   │   ├── __init__.so
│   │   │   └── t
│   │   │       ├── __init__.py
│   │   │       ├── __init__.so
│   │   │       └── t.so
│   │   └── setup.so
│   └── temp.linux-x86_64-2.7
│       ├── main.o
│       ├── mypkg
│       │   ├── foo.o
│       │   ├── __init__.o
│       │   └── t
│       │       ├── __init__.o
│       │       └── t.o
│       └── setup.o
├── main.c
├── main.py
├── mypkg
│   ├── foo.c
│   ├── foo.py
│   ├── __init__.c
│   ├── __init__.py
│   └── t
│       ├── __init__.c
│       ├── __init__.py
│       ├── t.c
│       └── t.py
├── setup.c
└── setup.py

然後, 將main.py拷貝到build/lib.linux-x86_64-2.7 直接就可以運行了

.
├── main.py
├── main.so
├── mypkg
│   ├── foo.so
│   ├── __init__.py
│   ├── __init__.so
│   └── t
│       ├── __init__.py
│       ├── __init__.so
│       └── t.so
└── setup.so

$ cat main.py

from mypkg.foo import hello
from mypkg import fun1
from mypkg.t.t import t

if __name__ == '__main__':
    hello()
    fun1()
    t()

$ python main.py
this is in hello
this is in fun1
this is in t

未經允許禁止轉載 https://spxcds.com/2018/12/05/python_to_so