用 Cython + Pyinstaller 安全地打包程式

前言

Pyinstaller 是很多人打包 Python 程式執行檔的第一選擇,但單純使用 Pyinstaller 打包的執行檔很容易被反編譯,如果要保護辛苦寫的程式碼不被輕易看光,可以搭配 Cython 將程式預先編譯成二進制檔案 (pyd),如此一來反編譯的難度會提升不少。

程式調整

  1. 由於 Pyinstaller 必須要指定一個 py 檔開始打包,這表示主程式會是明碼的,這時候可以再多建一個入口程式,由入口程式去 import 並執行主程式
  2. Pyinstaller 會自動解析 py 程式 import 的套並打包,但無法解析 pyd 檔,所以可以直接在入口程式 import 所有被使用到的套件,包括各程式彼此間的 import

下面舉個簡單的例子:

原始程式

  • app.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    from a import a
    from b import b

    def app():
    a()
    b()

    if __name__ == "__main__":
    app()
  • a.py
    1
    2
    3
    4
    5
    6
    from datetime import datetime
    import time

    def a():
    print(f"It's a. {datetime.now()}")
    time.sleep(0.5)
  • b.py
    1
    2
    3
    4
    from datetime import datetime

    def b():
    print(f"It's b. {datetime.now()}")

入口程式

  • main.py
    1
    2
    3
    4
    5
    6
    7
    from a import a
    from b import b
    from app import app
    from datetime import datetime
    import time

    app()

Cython 編譯

  1. 安裝 Cython

    1
    pip install cython
  2. 建立 setup.py
    這裡可以一個一個列出來,也可以用萬用字元 (*)

    1
    2
    3
    4
    5
    6
    from setuptools import setup
    from Cython.Build import cythonize

    setup(
    ext_modules=cythonize(["app.py", "a.py", "b.py"])
    )
  3. 編譯

    1
    python setup.py build_ext --inplace

    編譯完後目錄會多出 c 檔跟 pyd 檔,類似於下面這樣:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ├── build
    ├── a.c
    ├── a.cp38-win_amd64.pyd
    ├── a.py
    ├── app.c
    ├── app.cp38-win_amd64.pyd
    ├── app.py
    ├── b.c
    ├── b.cp38-win_amd64.pyd
    ├── b.py
    ├── main.py
    └── setup.py

    我們可以把編譯後的 py 檔移掉只留下 pyd 檔,會發現 main.py 一樣可以正常執行。

Pyinstaller 打包

  1. 安裝 Pyinstaller
    1
    pip install pyinstaller
  2. 打包
    1
    pyinstaller --onefile main.py
    打包完的執行檔會放在 dist 目錄下,大功告成。