Python3快速入門(十八)——PyInstaller打包發(fā)布

一、PyInstaller簡介

1、PyInstaller簡介

PyInstaller是一個跨平臺的Python應(yīng)用打包工具,支持 Windows/Linux/MacOS三大主流平臺,能夠把 Python 腳本及其所在的 Python 解釋器打包成可執(zhí)行文件,從而允許最終用戶在無需安裝 Python 的情況下執(zhí)行應(yīng)用程序。
PyInstaller 制作出來的執(zhí)行文件并不是跨平臺的,如果需要為不同平臺打包,就要在相應(yīng)平臺上運行PyInstaller進行打包。

2、PyInstaller安裝

pip install PyInstaller

二、PyInstaller基礎(chǔ)用法

1、PyInstaller使用

pyinstaller main.py
PyInstaller 最簡單使用只需要指定作為程序入口的腳本文件。PyInstaller 執(zhí)行打包程序后會在當前目錄下創(chuàng)建下列文件和目錄:
main.spec 文件,其前綴和腳本名相同,指定了打包時所需的各種參數(shù);
build 子目錄,其中存放打包過程中生成的臨時文件。warnxxxx.txt文件記錄了生成過程中的警告/錯誤信息。如果 PyInstaller 運行有問題,需要檢查warnxxxx.txt文件來獲取錯誤的詳細內(nèi)容。xref-xxxx.html文件輸出 PyInstaller 分析腳本得到的模塊依賴關(guān)系圖。
dist子目錄,存放生成的最終文件。如果使用單文件模式將只有單個執(zhí)行文件;如果使用目錄模式的話,會有一個和腳本同名的子目錄,其內(nèi)才是真正的可執(zhí)行文件以及附屬文件。

2、PyInstaller命令行選項

PyInstaller命令行選項可以通過幫助信息查看:
pyinstaller --help
-y | --noconfirm:直接覆蓋輸出文件,而無需提示,在多次重復運行命令時可避免反復確認。
-D | --onedir:生成包含執(zhí)行文件的目錄(默認行為)。
-F | --onefile:生成單一的可執(zhí)行文件,不推薦使用。
-i | --icon [.ico | .exe | .icns]:為 Windows/Mac 平臺的執(zhí)行文件指定圖標。
--version-file [filename]:添加文件版本信息。
-c | --console | --nowindowed:通過控制臺窗口運行程序 并且分配標準輸入/輸出,(默認行為)。
-w | --windowed | --noconsole:不創(chuàng)建控制臺窗口,也不分配標準輸入/輸出,主要用來運行 GUI 程序。沒有輸入輸出會給調(diào)試帶來一定困難,因此即便是 GUI 程序,建議在調(diào)試時禁用本選項,在最終發(fā)布時再打開。
--add-data [file:dir] :添加數(shù)據(jù)文件。如果有多個文件需要添加,本選項可以出現(xiàn)多次。參數(shù)的格式為文件名+輸出目錄名,用路徑分隔符分割,在 Windows 下使用?;,其它系統(tǒng)下則使用?:。 如果輸出到和腳本相同的目錄,則使用?.?作為輸出目錄。
--add-binary [file:dir] :添加二進制文件,即運行程序所需的.exe/.dll/.so 等。

3、單目錄模式

單目錄模式是 PyInstaller 將 Python 程序編譯為同一個目錄下的多個文件,其中 xxxx.exe 是程序入口點(xxxx 是腳本文件名稱,可以通過命令行修改)。單目錄模式是 PyInstaller 的默認模式,可以自己加上?-D?或者?--onedir?開關(guān)顯式開啟。
單目錄模式打包生成的目錄除可執(zhí)行文件外,還包括 Python 解釋器(PythonXX.dll)、系統(tǒng)運行庫(ucrtbase.dll 以及其它 apixx.dll),以及一些編譯后的 Python 模塊(.pyd 文件)。

4、單文件模式

單文件模式是將整個程序編譯為單一的可執(zhí)行文件。需要在命令行添加?-F?或者?--onefile?開關(guān)開啟。
Python腳本是解釋型程序,而不是 原生的編譯程序,并不能產(chǎn)生出真正單一的可執(zhí)行文件。如果使用單文件模式,PyInstaller打包生成的是自動解壓程序,需要先把所有文件解壓到一個臨時目錄(通常名為 _MEIxxxx ,xxxx是隨機數(shù)字),再從臨時目錄加載解釋器和附屬文件。程序運行完畢后,如果一切正常,會將臨時目錄再刪除。
PyInstaller會對運行時的Python解釋器修改。如果直接運行 Python 腳本,那么sys.frozen?變量不存在,如果通過 PyInstaller 生成的可執(zhí)行文件運行,PyInstaller 會設(shè)置sys.frozen?變量為 True;如果使用單文件模式, sys._MEIPASS? 變量包含了PyInstaller 自動創(chuàng)建的臨時目錄名。
單文件模式因為有臨時目錄和解壓文件過程,所以程序啟動速度會比較慢。如果程序運行到一半崩潰,則臨時目錄將沒有機會被刪除。

三、PyInstaller規(guī)格文件

PyInstaller 在生成文件的同時會創(chuàng)建一個相應(yīng)的.spec 文件,.spec 文件本質(zhì)上是一個特殊的 Python 腳本,記錄了生成所需的指令。

1、Spec文件生成

使用pyinstaller [options] xxx.py進行打包時,PyInstaller 會首先根據(jù)選項生成對應(yīng)的 .spec 文件,然后執(zhí)行 .spec 文件所指定的過程生成最終文件。因此,可以直接指定spec文件執(zhí)行打包過程。
pyinstaller [options] xxx.spec

2、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 腳本模塊編譯為對應(yīng)的 .pyd 并打包;
EXE:將打包后的 Python 模塊及其它文件一起生成可執(zhí)行的文件結(jié)構(gòu);
COLLECT:將引用到的附屬文件拷貝到生成目錄的對應(yīng)位置。
如果數(shù)據(jù)文件很多導致 Analysis 太長,則可以提取為單獨的變量。

            
              data_files = [(...)]
a = Analysis(...,
             datas=data_files,
             ...)
            
          

可以為數(shù)據(jù)/二進制文件指定通配符,從而匹配同一類型的多個文件。

            
              a = Analysis(...,
             datas=[('media/*.mp3', 'media')],
             ...)
            
          

可以將指定文件和指定目錄打包進行打包,如下:

            
              a = Analysis(...,
             datas=[('config.ini', '.'), ('data', 'data')],
             ...)
            
          

將config.ini文件打包當可執(zhí)行文件當前目錄下,將data目錄打包到可執(zhí)行文件當前目錄下。

四、PyInstaller Hook機制

1、PyInstaller Hook簡介

PyInstaller 使用遞歸方法,從入口的腳本文件逐個分析,獲取依賴模塊。
PyInstaller 能識別 ctypes、SWIG、Cython 等形式的模塊調(diào)用,但文件名必須為字面值。但PyInstaller 無法識別動態(tài)和調(diào)用,例如?import、exec、eval,以及以變量為參數(shù)的調(diào)用。
當 PyInstaller 識別完所有模塊后,會在內(nèi)部構(gòu)成一個樹形結(jié)構(gòu)表示調(diào)用關(guān)系圖,調(diào)用關(guān)系在生成目標時也會一并輸出(xref-xxxx.html 文件)。PYZ 步驟會將所有識別到的模塊匯集起來,如果有必要會編譯成.pyd,然后將文件打包。但仍然存在以下問題:
(1)由于動態(tài)模塊調(diào)用未必可以自動識別到,因此不會打包到文件中,執(zhí)行時肯定會出現(xiàn)問。
(2)有些模塊并非是以模塊的形式,而是通過文件系統(tǒng)去訪問 .py 文件,代碼在運行時同樣會出現(xiàn)問題。
為了解決上述問題,PyInstaller引入了Hooks機制,對于兩種問題引入了兩種類型的 Hook。兩種 Hook 主要是按照加載時間區(qū)分,第一種Hook在 PyInstaller 文檔中沒有明確的命名,是在生成過程中,導入特定模塊時調(diào)用的,稱為 Import Hook;第二種是Runtime Hook,是在執(zhí)行文件啟動期間、加載特定模塊時調(diào)用的。

2、Import Hooks

PyInstaller 定義的所有 Hook 在 PyInstaller 安裝目錄的 hooks 子目錄下,文件的命名均為 hook-[模塊名].py 的形式,即為 Import Hook。
當 PyInstaller 生成過程中找到特定的導入模塊,就會到hooks目錄下查找是否存在對應(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 用來描述并非通過 import 明確導入,而是通過其它動態(tài)機制加載的模塊。

3、Runtime Hooks

Runtime Hooks均位于 PyInstaller 安裝目錄下的loader\rthooks 子目錄下,并且命名方式是 pyi_rth_[模塊名稱].py (rth 代表 run time hook)。
loader\rthooks.dat內(nèi)容是一個字典,記錄了系統(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í)行文件運行期間執(zhí)行的。PyInstaller 修改了模塊加載機制,當運行期間加載任何模塊時,PyInstaller 會檢查是否有對應(yīng)的 Runtime Hook,如果有,則運行相應(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')
            
          

五、錯誤調(diào)試

使用PyInstaller進行打包時,最常見的錯誤是Failed to execute script xxx,通常做法是先使用pyinstaller -c xxx.py將應(yīng)用打包為控制臺應(yīng)用,在命令行執(zhí)行相應(yīng)可執(zhí)行程序查看錯誤輸出,進而逐個排除錯誤。