1、前言
封裝打包Python的好處,節省了安裝各種各樣包依賴的問題,同時可以加強我們代碼隱私的安全性,這里我的演示環境是Python3.6 ,CentOS7的系統,同時打包工具采用pyinstaller。
2、環境准備
2.1 Python共享so模塊
默認Python模塊是私有的,我們想打包就需要將我們的so模塊變為共享的,那么我們需要執行兩個操作即可。
- 重新編譯Python,加入編譯參數
--enable-shared
- 將so共享庫加入到系統之中
[root@c7-work-1 ~]# cat /etc/ld.so.conf.d/python3.6.conf
/usr/local/python3.6.5-shared/lib
[root@c7-work-1 ~]# ldconfig
完成上面操作即可了。
2.2 安裝pyinstaller
pip install pyinstaller
2.3 准備工程
這里我們只需要准備工程和相關依賴包,並且安裝成功即可,程序能正常跑則沒問題。
這里根據自己經驗來即可了,沒啥技巧。
3、代碼調整
默認情況下,我們打包后有些代碼原有的獲取方式會有些轉變。
3.1 運行時的環境
當應用程序運行時,可能需要訪問以下三個常規位置中的任何位置的數據文件:
- 捆綁在一起的數據文件;
- 用戶的配置文件;
- 用戶工作目錄中的文件
在Python中我們需要注意提防如下:
P1: 使用__file__
和sys._MEIPASS
當程序未凍結時,標准Python變量__file__是現在正在執行的腳本的完整路徑。當捆綁的應用程序啟動時,引導加載程序會設置sys.frozen屬性並將絕對路徑存儲到bundle文件夾中sys._MEIPASS。對於單文件夾捆綁包,這是該文件夾的路徑,無論用戶在哪里放置它。對於單文件包,這是 引導加載程序創建的臨時文件夾的路徑(請參閱單文件程序的工作原理)。_MEIxxxxxx
P2: 使用sys.executable
和sys.argv[0]
當正常的Python腳本運行時,sys.executable
是執行程序的路徑,即Python解釋器。在一個凍結的應用sys.executable
程序中,也是執行程序的路徑,但這不是Python; 它是單文件應用程序中的引導加載程序或單文件夾應用程序中的可執行文件。這為您提供了一種可靠的方法來查找用戶實際啟動的凍結可執行文件。
值sys.argv[0]
是用戶命令中使用的名稱或相對路徑。它可能是相對路徑或絕對路徑,具體取決於平台以及應用程序的啟動方式。
下面是一個演示代碼,有興趣可以執行下看看效果:
#!/usr/bin/python3
import sys, os
frozen = 'not'
if getattr(sys, 'frozen', False):
# we are running in a bundle
frozen = 'ever so'
bundle_dir = sys._MEIPASS
else:
# we are running in a normal Python environment
bundle_dir = os.path.dirname(os.path.abspath(__file__))
print( 'we are',frozen,'frozen')
print( 'bundle dir is', bundle_dir )
print( 'sys.argv[0] is', sys.argv[0] )
print( 'sys.executable is', sys.executable )
print( 'os.getcwd is', os.getcwd() )
3.2 調整項目代碼
根據上面的規則,着重調整一下代碼。
原代碼:
BASE_PATH = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
New代碼:
if getattr(sys, 'frozen', False):
BASE_PATH = sys._MEIPASS
else:
BASE_PATH = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
只需要啟動入口腳本調整即可,其他腳本無需調整
4、打包封裝
打包:
pyinstaller thunder_predict.spec
如何生成 *.spec文件呢,可以這里直接pyinstaller pyscript即可,然后在修改調整;下面提供我的實例文件:
# -*- mode: python -*-
block_cipher = None
a = Analysis(['predict/thunder_predict.py'],
pathex=['.'],
binaries=[],
datas=[('model', 'model'), ('dataset', 'dataset')],
hiddenimports=['cython','sklearn','sklearn.ensemble','sklearn.neighbors.typedefs',
'sklearn.neighbors.quad_tree','sklearn.tree._utils','scipy._lib.messagestream'],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='thunder_predict',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True )
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
name='thunder_predict')
5、打包So文件庫
我們為什么要打包成為so文件呢?
Python有py、pyc、pyw、pyo、pyd等文件格式。
其中,pyc是二進制文件。但很容易被反編譯。
pyw也不行,只是隱藏命令行界面而已,可以作為入口腳本。
pyo和pyc差不多,也容易被反編譯。
最后剩下pyd格式。pyd格式是D語言(C/C++綜合進化版本)生成的二進制文件,實際也會是dll文件。該文件目前位置沒找到可以被反編譯的消息,只能被反匯編。Sublime text編輯器也是使用該格式。
5.1 打包So模塊
打包生成so文件:
python3 ../utils/build_so.py build_ext --inplace
清理無用的文件:
rm -fr build main.c main.py
測試(成功測試成功):
[root@c7-work-1 thunder]# python3 predict/thunder_predict.py 01:dXBsb2FkRGF0YTE1MzI2Nzk4MzQzNDI= 199
success
Cython腳本內容:
from distutils.core import setup
from Cython.Build import cythonize
"""
python build_so.py build_ext --inplace
"""
setup(
name = 'main',
ext_modules = cythonize("main.py"),
)
5.2 重新打包整個
命令如下:
[root@c7-work-1 thunder]# pyinstaller thunder_predict.spec
36 INFO: PyInstaller: 3.4
37 INFO: Python: 3.6.5
................ 省略一下信息
我們檢查一下是否已經將我們的So庫文件打包。
[root@c7-work-1 thunder]# ls dist/thunder_predict/main.cpython-36m-x86_64-linux-gnu.so
dist/thunder_predict/main.cpython-36m-x86_64-linux-gnu.so
完美。
5.3 完美之余有些略顯通俗的坑
Q: pyinstaller利用獲取腳本中import來來打包對應的庫,如果你打包成so文件了還可以?
A: 這里我通過Python PKG的方式,將so會差別的庫放入到__init__.py中,就可以解決這個問題。