實(shí)用技巧!Pyinstaller打包技巧!

當(dāng)我們使用Python開(kāi)發(fā)好程序需要打包成exe時(shí),主流的做法便是使用pyinstaller,這玩意,看似簡(jiǎn)單,其實(shí)挺麻煩的,坑比較多,特別是涉及到比較復(fù)雜的庫(kù)時(shí),另外一個(gè)麻煩的事情是,打包失敗后,搜索到的很多解決方案是沒(méi)有效果的。
前一段時(shí)間,我用Python開(kāi)發(fā)了視頻同步助手,也是用pyinstaller打包的,其中涉及到opencv-python、ffmpeg、moviepy等包,嗯,這個(gè)過(guò)程比較磨人,在我配合pyinstaller源碼與其文檔后,掌握了一些技巧,本文簡(jiǎn)單總結(jié)記錄一下,希望對(duì)你有所幫助。
動(dòng)態(tài)導(dǎo)入問(wèn)題
如果你項(xiàng)目中使用了opencv-python庫(kù),簡(jiǎn)單利用pyinstaller打包,很容易出現(xiàn)打包成功了,卻無(wú)法運(yùn)行exe的情況,如下圖:

從報(bào)錯(cuò)細(xì)節(jié)來(lái)看,它讓你檢查OpenCV是否安裝(Check OpenCV installation),但這其實(shí)不是報(bào)錯(cuò)原因,核心在這句:
native_module = importlib.import_module("cv2")
importlib庫(kù)在業(yè)務(wù)型項(xiàng)目中是比較少使用的,其作用就是動(dòng)態(tài)載入相應(yīng)的庫(kù),而我們?cè)谌粘5臉I(yè)務(wù)開(kāi)發(fā)中,使用import關(guān)鍵字來(lái)實(shí)現(xiàn)庫(kù)的載入。
很多Python開(kāi)源項(xiàng)目會(huì)使用importlib來(lái)實(shí)現(xiàn)插件系統(tǒng),值得學(xué)習(xí),但這里卻因?yàn)閕mportlib的原因,讓pyinstaller打包失敗。
閱讀pyinstaller文檔中的【W(wǎng)hat PyInstaller Does and How It Does It】小節(jié),可知,pyinstaller在打包時(shí),會(huì)將項(xiàng)目的依賴也打包進(jìn)來(lái),但不包含下面幾種情況:
實(shí)現(xiàn)了__import__()方法的類實(shí)例,在項(xiàng)目中使用時(shí),無(wú)法被pyinstaller檢測(cè) 通過(guò)importlib.import_module()方法導(dǎo)入的庫(kù),無(wú)法被pyinstaller檢測(cè) 通過(guò)sys.path執(zhí)行的邏輯,無(wú)法被pyinstaller檢測(cè)
嗯,pyinstaller存在這些局限,而很多知名的庫(kù)卻大量出現(xiàn)上面的三種情況,比如Django、opencv-python。
怎么辦?文檔給出了4種解決方案:
通過(guò)pyinstaller命令行打包時(shí),通過(guò)相應(yīng)的配置參數(shù),給出額外的信息 將項(xiàng)目修改成使用import關(guān)鍵字導(dǎo)入的形式 編寫(xiě)spec文件,給出額外信息,這與第1種方法相同,命令行上指定的參數(shù),等價(jià)于spec配置文件中的配置 使用hook,實(shí)現(xiàn)動(dòng)態(tài)替換
首先排除方法2,因?yàn)檫@種方式只適用于你自己的項(xiàng)目,而Django、opencv-python這類第三方庫(kù),改不動(dòng),改動(dòng)了也不好維護(hù)。
然后排除方法1與方法3,對(duì)于簡(jiǎn)單情況,這兩種方法是可以的,文本后面點(diǎn)也會(huì)介紹,但一些第三方庫(kù),動(dòng)態(tài)導(dǎo)入的地方比較多,你通過(guò)寫(xiě)死配置的形式不太靠譜。
嗯,剩下方法4了。
什么是pyinstaller的hook?其實(shí)就是動(dòng)態(tài)替換一些信息的一種方法。以opencv-python為例,開(kāi)發(fā)者自己知道不同版本的opencv-python動(dòng)態(tài)導(dǎo)入時(shí),會(huì)導(dǎo)入什么地方的數(shù)據(jù),通過(guò)hook的形式,在不改動(dòng)opencv-python的基礎(chǔ)上,動(dòng)態(tài)映射成我們自己的導(dǎo)入方式。
pyinstaller文檔中給出了hook的開(kāi)發(fā)細(xì)節(jié),但不用急著動(dòng)手,pyinstaller的社區(qū)已經(jīng)將一些知名庫(kù)的hook都開(kāi)發(fā)好了,當(dāng)你安裝好pyinstaller時(shí),相應(yīng)的hook庫(kù)其實(shí)也安裝好了,叫pyinstaller-hooks-contrib。

pyinstaller-hooks-contrib 是社區(qū)維護(hù)的pyinstaller hooks機(jī)制

我們以opencv-python為例,找到opencv-python代碼動(dòng)態(tài)導(dǎo)入的位置,如下圖:

當(dāng)我們打包opencv-python時(shí),需要注意opencv-python的版本,因?yàn)椴煌姹镜膐pencv-python,需要hook的位置可能會(huì)改變,我們看到pyinstaller opencv-python相關(guān)的hook代碼中的注釋也可以看出其版本要求:

經(jīng)過(guò)多次實(shí)驗(yàn),下面的版本關(guān)系可以讓opencv-python成功打包。
pip uninstall pyinstaller-hooks-contrib
pip install pyinstaller-hooks-contrib==2021.3
pip uninstall pyinstaller
pip install pyinstaller==4.5.1
pip uninstall opencv-python
pip install opencv-python==4.5.4.58
但,單純的解決版本問(wèn)題,還是無(wú)法很好的使用opencv-python,我們還需要將opencv-python的完整路徑告訴pyinstaller,這需要使用方法1或方法3,我個(gè)人習(xí)慣使用方法3,即利用spec配置文件的形式來(lái)給pyinstaller更多額外信息。
spec文件
閱讀pyinstaller文檔中的【Using Spec Files】小節(jié)可知,spec文件會(huì)告訴pyinstaller打包時(shí),如何處理被打包腳本,且spec文件實(shí)際上是可執(zhí)行的python代碼。
從文檔可知,spec文件主要有4個(gè)用途:
當(dāng)你希望將數(shù)據(jù)文件與打包程序捆綁在一起時(shí) 當(dāng)你希望包含運(yùn)行時(shí)庫(kù)時(shí)(DLL、SO等文件) 當(dāng)你希望將Python run-time options添加到可執(zhí)行文件時(shí) 當(dāng)您想創(chuàng)建一個(gè)包含合并的公共模塊的多程序包時(shí)
用途3與用途4沒(méi)有在實(shí)際項(xiàng)目中使用過(guò),所以不討論,我們主要來(lái)看看用途1與用途2。
我們可以使用下面命令創(chuàng)建spec文件:
pyi-makespec main.py下面是【無(wú)感視頻同步助手】的spec文件,相比于創(chuàng)建出的默認(rèn)spec文件,內(nèi)容多會(huì)多一些,建議你直接從我這里復(fù)制出去用。
# -*- mode: python ; coding: utf-8 -*-
import json
import os
import sys
import PyInstaller.config
# 存放最終打包成app的相對(duì)路徑
buildPath = 'build'
PyInstaller.config.CONF['distpath'] = buildPath
# 存放打包成app的中間文件的相對(duì)路徑
cachePath = os.path.join(buildPath, 'cache')
if not os.path.exists(cachePath):
os.makedirs(cachePath)
PyInstaller.config.CONF['workpath'] = cachePath
# icon相對(duì)路徑
icoPath = os.path.join('logo.ico')
# 項(xiàng)目名稱
appName = '無(wú)感視頻同步助手'
# 版本號(hào)
version = '1.0.0'
# 對(duì)Python字節(jié)碼加密
block_cipher = pyi_crypto.PyiBlockCipher(key='875650321356')
a = Analysis(['gui_main.py'],
pathex=["venv\\Lib\\site-packages\\cv2"],
binaries=[("venv\\Lib\\site-packages\\cv2\\opencv_videoio_ffmpeg453_64.dll", ".")],
datas=[('gui\\frontend', 'gui\\frontend')],
hiddenimports=[],
hookspath=[],
hooksconfig={},
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,
a.binaries,
a.zipfiles,
a.datas,
[],
name=appName,
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=icoPath)
其中:
pathex=["venv\\Lib\\site-packages\\cv2"],
便是將opencv-python完整項(xiàng)目的路徑告訴pyinstaller,這樣打包pyinstaller-python時(shí),再配合上正確的pyinstaller與opencv-python版本,便可以打包出可正常打開(kāi)的exe。
知識(shí)點(diǎn):第三方庫(kù)代碼相關(guān)的放在pathex字段中
打包后的opencv-python無(wú)法處理視頻
一切似乎很ok,但真正運(yùn)行業(yè)務(wù)邏輯時(shí),會(huì)報(bào)錯(cuò):

經(jīng)過(guò)加日志重打包后分析可知,它在下面位置報(bào)錯(cuò):

opencv-python處理視頻其實(shí)利用了ffmpeg.dll,而我們打包時(shí),如果沒(méi)有告訴pyinstaller ffmpeg.dll的位置,pyinstaller就不會(huì)將其打包進(jìn)來(lái),則會(huì)導(dǎo)致運(yùn)行報(bào)錯(cuò)。
所以,spec文件中需要下面的內(nèi)容:
binaries=[("venv\\Lib\\site-packages\\cv2\\opencv_videoio_ffmpeg453_64.dll", ".")],
知識(shí)點(diǎn):dll、so這類動(dòng)態(tài)庫(kù),要寫(xiě)在binaries字段中。
靜態(tài)資源打包
【無(wú)感視頻同步助手】使用了html、css來(lái)做布局,這些不是python代碼,對(duì)python而言,類似于image、video之類的靜態(tài)資源,這類靜態(tài)資源,我們需要寫(xiě)到spec文件的datas字段中:
datas=[('gui\\frontend', 'gui\\frontend')],
打包moviepy
搞定opencv-python后,你可以用類似的方法來(lái)搞moviepy這個(gè)庫(kù),畢竟moviepy也是基于ffmpeg來(lái)弄的,這不簡(jiǎn)單。
嗯,不會(huì)靈活變通的話,可能會(huì)懵逼,因?yàn)閙oviepy有如下導(dǎo)入方式,且社區(qū)沒(méi)有提供moviepy的hook:

moviepy的作者偷懶,直接通過(guò)exec來(lái)批量導(dǎo)入需要的庫(kù),不可為不騷。
怎么解決?
使用方法2,沒(méi)錯(cuò),將其改成使用import關(guān)鍵字導(dǎo)入的形式,但不是改moviepy的代碼。我們創(chuàng)建moviepy_import.py文件,將需要導(dǎo)入的庫(kù)都寫(xiě)進(jìn)去。

然后再項(xiàng)目入口py文件中,import moviepy_import,解決moviepy批量導(dǎo)入的騷寫(xiě)法。
此外,moviepy打包還有另外一個(gè)問(wèn)題,因?yàn)閙oviepy使用了imageio_ffmpeg這個(gè)庫(kù),而imageio_ffmpeg會(huì)使用ffmpeg,但我們打包時(shí),沒(méi)有將ffmpeg文件打包進(jìn)去,moviepy在運(yùn)行時(shí)便會(huì)報(bào)錯(cuò)。
瀏覽imageio_ffmpeg目錄,發(fā)現(xiàn)它自己會(huì)安裝對(duì)應(yīng)版本的ffmpeg。

找到moviepy報(bào)錯(cuò)位置,其實(shí)是imageio_ffmpeg庫(kù)的_utils.py文件中的get_ffmpeg_exe()方法,如下圖:

其實(shí)就是找不到ffmpeg而報(bào)錯(cuò),我的解決方法是手動(dòng)設(shè)置一下:

結(jié)尾
嗯,目前我筆記里有記錄的坑就上文中這些了,一個(gè)體會(huì)是,閱讀源碼和閱讀文檔的能力很重要,特別是資料比較少的情況。
推薦閱讀:
入門: 最全的零基礎(chǔ)學(xué)Python的問(wèn)題 | 零基礎(chǔ)學(xué)了8個(gè)月的Python | 實(shí)戰(zhàn)項(xiàng)目 |學(xué)Python就是這條捷徑
干貨:爬取豆瓣短評(píng),電影《后來(lái)的我們》 | 38年NBA最佳球員分析 | 從萬(wàn)眾期待到口碑撲街!唐探3令人失望 | 笑看新倚天屠龍記 | 燈謎答題王 |用Python做個(gè)海量小姐姐素描圖 |碟中諜這么火,我用機(jī)器學(xué)習(xí)做個(gè)迷你推薦系統(tǒng)電影
趣味:彈球游戲 | 九宮格 | 漂亮的花 | 兩百行Python《天天酷跑》游戲!
AI: 會(huì)做詩(shī)的機(jī)器人 | 給圖片上色 | 預(yù)測(cè)收入 | 碟中諜這么火,我用機(jī)器學(xué)習(xí)做個(gè)迷你推薦系統(tǒng)電影
小工具: Pdf轉(zhuǎn)Word,輕松搞定表格和水?。?/a> | 一鍵把html網(wǎng)頁(yè)保存為pdf!| 再見(jiàn)PDF提取收費(fèi)! | 用90行代碼打造最強(qiáng)PDF轉(zhuǎn)換器,word、PPT、excel、markdown、html一鍵轉(zhuǎn)換 | 制作一款釘釘?shù)蛢r(jià)機(jī)票提示器! |60行代碼做了一個(gè)語(yǔ)音壁紙切換器天天看小姐姐!|
年度爆款文案
點(diǎn)閱讀原文,看B站我的視頻!

