PyInstaller是一個(gè)跨平臺(tái)的Python應(yīng)用打包工具,支持 Windows/Linux/MacOS三大主流平臺(tái),能夠把 Python 腳本及其所在的 Python 解釋器打包成可執(zhí)行文件,從而允許最終用戶在無(wú)需安裝 Python 的情況下執(zhí)行應(yīng)用程序。
PyInstaller 制作出來(lái)的執(zhí)行文件并不是跨平臺(tái)的,如果需要為不同平臺(tái)打包,就要在相應(yīng)平臺(tái)上運(yùn)行PyInstaller進(jìn)行打包。
成都創(chuàng)新互聯(lián)公司一直在為企業(yè)提供服務(wù),多年的磨煉,使我們?cè)趧?chuàng)意設(shè)計(jì),全網(wǎng)整合營(yíng)銷推廣到技術(shù)研發(fā)擁有了開發(fā)經(jīng)驗(yàn)。我們擅長(zhǎng)傾聽企業(yè)需求,挖掘用戶對(duì)產(chǎn)品需求服務(wù)價(jià)值,為企業(yè)制作有用的創(chuàng)意設(shè)計(jì)體驗(yàn)。核心團(tuán)隊(duì)擁有超過(guò)10年以上行業(yè)經(jīng)驗(yàn),涵蓋創(chuàng)意,策化,開發(fā)等專業(yè)領(lǐng)域,公司涉及領(lǐng)域有基礎(chǔ)互聯(lián)網(wǎng)服務(wù)成都移動(dòng)服務(wù)器托管、成都app軟件開發(fā)公司、手機(jī)移動(dòng)建站、網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)絡(luò)整合營(yíng)銷。
pip install PyInstaller
pyinstaller main.py
PyInstaller 最簡(jiǎn)單使用只需要指定作為程序入口的腳本文件。PyInstaller 執(zhí)行打包程序后會(huì)在當(dāng)前目錄下創(chuàng)建下列文件和目錄:
main.spec 文件,其前綴和腳本名相同,指定了打包時(shí)所需的各種參數(shù);
build 子目錄,其中存放打包過(guò)程中生成的臨時(shí)文件。warnxxxx.txt文件記錄了生成過(guò)程中的警告/錯(cuò)誤信息。如果 PyInstaller 運(yùn)行有問(wèn)題,需要檢查warnxxxx.txt文件來(lái)獲取錯(cuò)誤的詳細(xì)內(nèi)容。xref-xxxx.html文件輸出 PyInstaller 分析腳本得到的模塊依賴關(guān)系圖。
dist子目錄,存放生成的最終文件。如果使用單文件模式將只有單個(gè)執(zhí)行文件;如果使用目錄模式的話,會(huì)有一個(gè)和腳本同名的子目錄,其內(nèi)才是真正的可執(zhí)行文件以及附屬文件。
PyInstaller命令行選項(xiàng)可以通過(guò)幫助信息查看:pyinstaller --help
-y | --noconfirm:直接覆蓋輸出文件,而無(wú)需提示,在多次重復(fù)運(yùn)行命令時(shí)可避免反復(fù)確認(rèn)。
-D | --onedir:生成包含執(zhí)行文件的目錄(默認(rèn)行為)。
-F | --onefile:生成單一的可執(zhí)行文件,不推薦使用。
-i | --icon [.ico | .exe | .icns]:為 Windows/Mac 平臺(tái)的執(zhí)行文件指定圖標(biāo)。
--version-file [filename]:添加文件版本信息。
-c | --console | --nowindowed:通過(guò)控制臺(tái)窗口運(yùn)行程序 并且分配標(biāo)準(zhǔn)輸入/輸出,(默認(rèn)行為)。
-w | --windowed | --noconsole:不創(chuàng)建控制臺(tái)窗口,也不分配標(biāo)準(zhǔn)輸入/輸出,主要用來(lái)運(yùn)行 GUI 程序。沒有輸入輸出會(huì)給調(diào)試帶來(lái)一定困難,因此即便是 GUI 程序,建議在調(diào)試時(shí)禁用本選項(xiàng),在最終發(fā)布時(shí)再打開。--add-data [file:dir]
:添加數(shù)據(jù)文件。如果有多個(gè)文件需要添加,本選項(xiàng)可以出現(xiàn)多次。參數(shù)的格式為文件名+輸出目錄名,用路徑分隔符分割,在 Windows 下使用?;,其它系統(tǒng)下則使用?:。 如果輸出到和腳本相同的目錄,則使用?.?作為輸出目錄。--add-binary [file:dir]
:添加二進(jìn)制文件,即運(yùn)行程序所需的.exe/.dll/.so 等。
單目錄模式是 PyInstaller 將 Python 程序編譯為同一個(gè)目錄下的多個(gè)文件,其中 xxxx.exe 是程序入口點(diǎn)(xxxx 是腳本文件名稱,可以通過(guò)命令行修改)。單目錄模式是 PyInstaller 的默認(rèn)模式,可以自己加上?-D?或者?--onedir?開關(guān)顯式開啟。
單目錄模式打包生成的目錄除可執(zhí)行文件外,還包括 Python 解釋器(PythonXX.dll)、系統(tǒng)運(yùn)行庫(kù)(ucrtbase.dll 以及其它 apixx.dll),以及一些編譯后的 Python 模塊(.pyd 文件)。
單文件模式是將整個(gè)程序編譯為單一的可執(zhí)行文件。需要在命令行添加?-F?或者?--onefile?開關(guān)開啟。
Python腳本是解釋型程序,而不是 原生的編譯程序,并不能產(chǎn)生出真正單一的可執(zhí)行文件。如果使用單文件模式,PyInstaller打包生成的是自動(dòng)解壓程序,需要先把所有文件解壓到一個(gè)臨時(shí)目錄(通常名為_MEIxxxx
,xxxx是隨機(jī)數(shù)字),再?gòu)呐R時(shí)目錄加載解釋器和附屬文件。程序運(yùn)行完畢后,如果一切正常,會(huì)將臨時(shí)目錄再刪除。
PyInstaller會(huì)對(duì)運(yùn)行時(shí)的Python解釋器修改。如果直接運(yùn)行 Python 腳本,那么sys.frozen?變量不存在,如果通過(guò) PyInstaller 生成的可執(zhí)行文件運(yùn)行,PyInstaller 會(huì)設(shè)置sys.frozen?變量為 True;如果使用單文件模式,sys._MEIPASS?
變量包含了PyInstaller 自動(dòng)創(chuàng)建的臨時(shí)目錄名。
單文件模式因?yàn)橛信R時(shí)目錄和解壓文件過(guò)程,所以程序啟動(dòng)速度會(huì)比較慢。如果程序運(yùn)行到一半崩潰,則臨時(shí)目錄將沒有機(jī)會(huì)被刪除。
PyInstaller 在生成文件的同時(shí)會(huì)創(chuàng)建一個(gè)相應(yīng)的.spec 文件,.spec 文件本質(zhì)上是一個(gè)特殊的 Python 腳本,記錄了生成所需的指令。
使用pyinstaller [options] xxx.py進(jìn)行打包時(shí),PyInstaller 會(huì)首先根據(jù)選項(xiàng)生成對(duì)應(yīng)的 .spec 文件,然后執(zhí)行 .spec 文件所指定的過(guò)程生成最終文件。因此,可以直接指定spec文件執(zhí)行打包過(guò)程。pyinstaller [options] xxx.spec
單目錄模式生成的spec 文件格式如下:
a = Analysis(...)
pyz = PYZ(...)
exe = EXE(...)
coll = COLLECT(...)
單文件模式生成的spec 文件格式如下:
a = Analysis(...)
pyz = PYZ(...)
exe = EXE(...)
單文件模式是將所有內(nèi)容統(tǒng)一打包到 .exe,而單目錄模式除了生成 .exe 外,還需要拷貝其它附屬文件。
Analysis用于分析腳本的引用關(guān)系,并將所有查找到的相關(guān)內(nèi)容記錄在內(nèi)部結(jié)構(gòu)中,供后續(xù)步驟使用;
PYZ將所有 Python 腳本模塊編譯為對(duì)應(yīng)的 .pyd 并打包;
EXE:將打包后的 Python 模塊及其它文件一起生成可執(zhí)行的文件結(jié)構(gòu);
COLLECT:將引用到的附屬文件拷貝到生成目錄的對(duì)應(yīng)位置。
如果數(shù)據(jù)文件很多導(dǎo)致 Analysis 太長(zhǎng),則可以提取為單獨(dú)的變量。
data_files = [(...)]
a = Analysis(...,
datas=data_files,
...)
可以為數(shù)據(jù)/二進(jìn)制文件指定通配符,從而匹配同一類型的多個(gè)文件。
a = Analysis(...,
datas=[('media/*.mp3', 'media')],
...)
可以將指定文件和指定目錄打包進(jìn)行打包,如下:
a = Analysis(...,
datas=[('config.ini', '.'), ('data', 'data')],
...)
將config.ini文件打包當(dāng)可執(zhí)行文件當(dāng)前目錄下,將data目錄打包到可執(zhí)行文件當(dāng)前目錄下。
PyInstaller 使用遞歸方法,從入口的腳本文件逐個(gè)分析,獲取依賴模塊。
PyInstaller 能識(shí)別 ctypes、SWIG、Cython 等形式的模塊調(diào)用,但文件名必須為字面值。但PyInstaller 無(wú)法識(shí)別動(dòng)態(tài)和調(diào)用,例如?import、exec、eval,以及以變量為參數(shù)的調(diào)用。
當(dāng) PyInstaller 識(shí)別完所有模塊后,會(huì)在內(nèi)部構(gòu)成一個(gè)樹形結(jié)構(gòu)表示調(diào)用關(guān)系圖,調(diào)用關(guān)系在生成目標(biāo)時(shí)也會(huì)一并輸出(xref-xxxx.html 文件)。PYZ 步驟會(huì)將所有識(shí)別到的模塊匯集起來(lái),如果有必要會(huì)編譯成.pyd,然后將文件打包。但仍然存在以下問(wèn)題:
(1)由于動(dòng)態(tài)模塊調(diào)用未必可以自動(dòng)識(shí)別到,因此不會(huì)打包到文件中,執(zhí)行時(shí)肯定會(huì)出現(xiàn)問(wèn)。
(2)有些模塊并非是以模塊的形式,而是通過(guò)文件系統(tǒng)去訪問(wèn) .py 文件,代碼在運(yùn)行時(shí)同樣會(huì)出現(xiàn)問(wèn)題。
為了解決上述問(wèn)題,PyInstaller引入了Hooks機(jī)制,對(duì)于兩種問(wèn)題引入了兩種類型的 Hook。兩種 Hook 主要是按照加載時(shí)間區(qū)分,第一種Hook在 PyInstaller 文檔中沒有明確的命名,是在生成過(guò)程中,導(dǎo)入特定模塊時(shí)調(diào)用的,稱為 Import Hook;第二種是Runtime Hook,是在執(zhí)行文件啟動(dòng)期間、加載特定模塊時(shí)調(diào)用的。
PyInstaller 定義的所有 Hook 在 PyInstaller 安裝目錄的 hooks 子目錄下,文件的命名均為 hook-[模塊名].py 的形式,即為 Import Hook。
當(dāng) PyInstaller 生成過(guò)程中找到特定的導(dǎo)入模塊,就會(huì)到hooks目錄下查找是否存在對(duì)應(yīng)的Hook,如果存在,則執(zhí)行之。
hook-PyQt5.py文件如下:
import os
from PyInstaller.utils.hooks import collect_system_data_files
from PyInstaller.utils.hooks.qt import pyqt5_library_info, get_qt_binaries
# Ensure PyQt5 is importable before adding info depending on it.
if pyqt5_library_info.version:
hiddenimports = [
# PyQt5.10 and earlier uses sip in an separate package;
'sip',
# PyQt5.11 and later provides SIP in a private package. Support both.
'PyQt5.sip'
]
# Collect the ``qt.conf`` file.
datas = [x for x in
collect_system_data_files(pyqt5_library_info.location['PrefixPath'],
'PyQt5')
if os.path.basename(x[0]) == 'qt.conf']
# Collect required Qt binaries.
binaries = get_qt_binaries(pyqt5_library_info)
hiddenimports是PyInstaller 用來(lái)描述并非通過(guò) import 明確導(dǎo)入,而是通過(guò)其它動(dòng)態(tài)機(jī)制加載的模塊。
Runtime Hooks均位于 PyInstaller 安裝目錄下的loader\rthooks 子目錄下,并且命名方式是 pyi_rth_[模塊名稱].py
(rth 代表 run time hook)。
loader\rthooks.dat內(nèi)容是一個(gè)字典,記錄了系統(tǒng)中所有支持的 Runtime Hooks。rthooks.dat文件如下:
{
'certifi': ['pyi_rth_certifi.py'],
'django': ['pyi_rth_django.py'],
'enchant': ['pyi_rth_enchant.py'],
'gi': ['pyi_rth_gi.py'],
'gi.repository.Gio': ['pyi_rth_gio.py'],
'gi.repository.GLib': ['pyi_rth_glib.py'],
'gi.repository.GdkPixbuf': ['pyi_rth_gdkpixbuf.py'],
'gi.repository.Gtk': ['pyi_rth_gtk.py'],
'gi.repository.Gst': ['pyi_rth_gstreamer.py'],
'gst': ['pyi_rth_gstreamer.py'],
'kivy': ['pyi_rth_kivy.py'],
'kivy.lib.gstplayer': ['pyi_rth_gstreamer.py'],
'matplotlib': ['pyi_rth_mplconfig.py', 'pyi_rth_mpldata.py'],
'osgeo': ['pyi_rth_osgeo.py'],
'pkg_resources': ['pyi_rth_pkgres.py'],
'PyQt4': ['pyi_rth_qt4plugins.py'],
'PyQt5': ['pyi_rth_pyqt5.py'],
'PyQt5.QtWebEngineWidgets': ['pyi_rth_pyqt5webengine.py'],
'PySide': ['pyi_rth_qt4plugins.py'],
'PySide2': ['pyi_rth_pyside2.py'],
'PySide2.QtWebEngineWidgets': ['pyi_rth_pyside2webengine.py'],
'_tkinter': ['pyi_rth__tkinter.py'],
'traitlets': ['pyi_rth_traitlets.py'],
'twisted.internet.reactor': ['pyi_rth_twisted.py'],
'usb': ['pyi_rth_usb.py'],
'win32com': ['pyi_rth_win32comgenpy.py'],
'multiprocessing': ['pyi_rth_multiprocessing.py'],
'nltk': ['pyi_rth_nltk.py'],
}
Runtime Hooks 是在執(zhí)行文件運(yùn)行期間執(zhí)行的。PyInstaller 修改了模塊加載機(jī)制,當(dāng)運(yùn)行期間加載任何模塊時(shí),PyInstaller 會(huì)檢查是否有對(duì)應(yīng)的 Runtime Hook,如果有,則運(yùn)行相應(yīng)Hook。因此,Runtime Hooks 是和腳本一起編譯到可執(zhí)行文件中的。
pyi_rth_pyqt5.py文件如下:
import os
import sys
# The path to Qt's components may not default to the wheel layout for
# self-compiled PyQt5 installations. Mandate the wheel layout. See
# ``utils/hooks/qt.py`` for more details.
pyqt_path = os.path.join(sys._MEIPASS, 'PyQt5', 'Qt')
os.environ['QT_PLUGIN_PATH'] = os.path.join(pyqt_path, 'plugins')
os.environ['QML2_IMPORT_PATH'] = os.path.join(pyqt_path, 'qml')
使用PyInstaller進(jìn)行打包時(shí),最常見的錯(cuò)誤是Failed to execute script xxx,通常做法是先使用pyinstaller -c xxx.py將應(yīng)用打包為控制臺(tái)應(yīng)用,在命令行執(zhí)行相應(yīng)可執(zhí)行程序查看錯(cuò)誤輸出,進(jìn)而逐個(gè)排除錯(cuò)誤。