国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

PyInstaller 完美打包 Python 腳本,輸出結(jié)構(gòu)清晰、便于二次編輯的打包程序

這篇具有很好參考價(jià)值的文章主要介紹了PyInstaller 完美打包 Python 腳本,輸出結(jié)構(gòu)清晰、便于二次編輯的打包程序。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問(wèn)。

引入問(wèn)題

如果我要寫(xiě)一個(gè) Python 項(xiàng)目,打包成 exe 運(yùn)行(方便在沒(méi)有 Python 的電腦上使用),我需要打包出的根目錄結(jié)構(gòu)美觀,沒(méi)有多余的、雜亂的依賴(lài)文件在那里礙眼,而且需要在發(fā)現(xiàn) bug 時(shí),我還需要能夠修改里面的代碼后,無(wú)需再次打包,就能正常運(yùn)行,該怎么做呢?

就以一個(gè) Hello 項(xiàng)目為例,記一下我找到的完美方法。最新代碼已經(jīng)放到,歡迎參閱:https://github.com/HaujetZhao/PyInstaller-Perfect-Build-Method

首先,新建項(xiàng)目文件夾,寫(xiě)一個(gè) hello.py

PyInstaller 完美打包 Python 腳本,輸出結(jié)構(gòu)清晰、便于二次編輯的打包程序

PyInstaller 完美打包 Python 腳本,輸出結(jié)構(gòu)清晰、便于二次編輯的打包程序

用 PyInstaller 把 hello.py 打包,pyinstaller ./hello.py 命令會(huì)得到 builddist 文件夾,以及 hello.spec 文件:

PyInstaller 完美打包 Python 腳本,輸出結(jié)構(gòu)清晰、便于二次編輯的打包程序

為了方便文件名排序,在下面我把 hello.spec 重命名成了 build-hello.py

其中:

  • build 文件夾是存放打包時(shí)臨時(shí)文件用的
  • dist 文件夾存放了打包好的應(yīng)用
  • build-hello.spec 內(nèi)容是 PyInstaller 根據(jù)我們的命令行生成的打包參數(shù)

打開(kāi) dist/hello 文件夾,可以看到我們打包好的 hello.exe 躺在一堆依賴(lài)文件之間,非常丑陋

PyInstaller 完美打包 Python 腳本,輸出結(jié)構(gòu)清晰、便于二次編輯的打包程序

我們的目標(biāo),就是要把這些依賴(lài)包都移到一個(gè)子文件夾中,讓打包文件夾變得整潔,同時(shí)讓程序正常運(yùn)行。

最后我們可以打包成這個(gè)樣子:

PyInstaller 完美打包 Python 腳本,輸出結(jié)構(gòu)清晰、便于二次編輯的打包程序

首先,所有的依賴(lài)模塊都被移動(dòng)到了 libs 文件夾,整個(gè)打包根目錄清清爽爽,只留下了必要的 python310.dllbase_library.zip。

其次,如你所見(jiàn),這個(gè)程序的脾氣不是太好,出口成臟,我們希望用戶在拿到這個(gè)開(kāi)源程序時(shí),可以修改腳本的內(nèi)容,不需要重新打包就能直接從 hello.exe 運(yùn)行。因此我們要把 hello.exe 做成程序入口,實(shí)際的邏輯寫(xiě)在 hello_main.py ,同時(shí)要確保 hello_main.py 中的依賴(lài)都被正確打包到 libs 文件夾。

我們一步步解決。

第一步:自定義依賴(lài)包位置

生成 spec 文件

達(dá)到目的的關(guān)鍵在于用命令行打包時(shí)自動(dòng)生成的 build-hello.spec (為方便文件名排序,已重命名為 build-hello.spec),它的本質(zhì)是一個(gè) python 文件,pyinstaller 有兩種運(yùn)行模式:

  • pyinstaller build-hello.spec 會(huì)使用 spec 文件中的配置進(jìn)行打包
  • pyinstaller hello.py <other args> 根據(jù)命令行參數(shù)自動(dòng)生成 spec 文件,再依據(jù)使用 spec 文件中的配置進(jìn)行打包

pyinstaller 在打包時(shí),實(shí)際上是在做了一些準(zhǔn)備工作后,直接運(yùn)行了 spec 文件里的 Python 代碼。

相比于給命令行添加參數(shù),直接編輯 spec 文件,在里面保存參數(shù),更優(yōu)雅,更方便操作。

除了直接打包腳,本文件自動(dòng)生成 spec 配置,還可以通過(guò)執(zhí)行 pyi-makespec hello.py 不打包,只生成 spec 配置。

解釋 spec 文件

打開(kāi) build-hello.spec 文件,有如下內(nèi)容(已作注釋?zhuān)?/p>

# -*- mode: python ; coding: utf-8 -*-


block_cipher = None

# 這一部分負(fù)責(zé)收集你的腳本需要的所有模塊和文件。的;hiddenimports 參數(shù)可以指定一些 PyInstaller 無(wú)法自動(dòng)檢測(cè)到的模塊。
a = Analysis(
    ['hello.py'],       # 指定要打包的 Python 腳本的路徑(可以是相對(duì)路徑)
    pathex=[],          # 用來(lái)指定模塊搜索路徑
    binaries=[],        # 包含了動(dòng)態(tài)鏈接庫(kù)或共享對(duì)象文件,會(huì)在運(yùn)行之后自動(dòng)更新,加入依賴(lài)的二進(jìn)制文件
    datas=[],           # 列表,用于指定需要包含的額外文件。每個(gè)元素都是一個(gè)元組:(文件的源路徑, 在打包文件中的路徑)
    hiddenimports=[],   # 用于指定一些 PyInstaller 無(wú)法自動(dòng)檢測(cè)到的模塊
    hookspath=[],       # 指定查找 PyInstaller 鉤子的路徑
    hooksconfig={},     # 自定義 hook 配置,這是一個(gè)字典,一行注釋寫(xiě)不下,此處先不講
    runtime_hooks=[],   # 指定運(yùn)行時(shí) hook,本質(zhì)是一個(gè) Python 腳本,hook 會(huì)在你的腳本運(yùn)行前運(yùn)行,可用于準(zhǔn)備環(huán)境
    excludes=[],        # 用于指定需要排除的模塊
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)
# 除此之外,a 還有一些沒(méi)有列出的屬性:
#   pure 是一個(gè)列表,包含了所有純 Python 模塊的信息,每個(gè)元素是一個(gè)元組,包含了:模塊名, pyc路徑, py 路徑,這些模塊會(huì)被打包到一個(gè) .pyz 文件中。
#   scripts 是一個(gè)列表,包含了你的 Python 腳本的信息。每個(gè)元素是一個(gè)元組,其中包含了腳本的內(nèi)部名,腳本的源路徑,以及一些元數(shù)據(jù)。這些腳本會(huì)被打包到一個(gè)可執(zhí)行文件中。


# pyz 是指生成的可執(zhí)行文件的名稱(chēng)。它是由 PyInstaller 用來(lái)打包 Python 程序和依賴(lài)項(xiàng)的主要文件。


# 創(chuàng)建 pyz 文件,它在運(yùn)行時(shí)會(huì)被解壓縮到臨時(shí)目錄中,然后被加載和執(zhí)行。它會(huì)被打包進(jìn) exe 文件
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)


# 創(chuàng)建 exe 文件
exe = EXE(
    pyz,            # 包含了所有純 Python 模塊
    a.scripts,      # 包含了主腳本及其依賴(lài)
    [],             # 所有需要打包到 exe 文件內(nèi)的二進(jìn)制文件
    exclude_binaries=True,  # 若為 True,所有的二進(jìn)制文件將被排除在 exe 之外,轉(zhuǎn)而被 COLLECT 函數(shù)收集
    name='hello',   # 生成的 exe 文件的名字。
    debug=False,    # 打包過(guò)程中是否打印調(diào)試信息?
    bootloader_ignore_signals=False,
    strip=False,    # 是否移除所有的符號(hào)信息,使打包出的 exe 文件更小
    upx=True,       # 是否用 upx 壓縮 exe 文件
    console=True,   # 若為 True 則在控制臺(tái)窗口中運(yùn)行,否則作為后臺(tái)進(jìn)程運(yùn)行
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)


# 這個(gè)對(duì)象包含了所有需要分發(fā)的文件
# 包括 EXE 函數(shù)創(chuàng)建的 exe 文件、所有的二進(jìn)制文件、zip 文件(如果有的話)和數(shù)據(jù)文件
coll = COLLECT(
    exe,
    a.binaries,
    a.zipfiles,
    a.datas,
    strip=False,
    upx=True,
    upx_exclude=[],
    name='hello',   # 生成的文件夾的名字
)

加入 Hook

通過(guò)對(duì) spec 文件的了解,我們知道了,可以在 a.runtimehooks 列表中加入 python 腳本 hook ,它會(huì)在我們的主代碼執(zhí)行之前運(yùn)行,為我們準(zhǔn)備環(huán)境。

在這個(gè) hook 里面,我們就可以修改 sys.path ,自定義 Python 查找模塊的路徑,或者環(huán)境變量

那我們就寫(xiě)一個(gè) hook.py

import sys
from pprint import pprint

print(f'\n\n模塊查找路徑:')
pprint(sys.path)

print('\n')

PyInstaller 完美打包 Python 腳本,輸出結(jié)構(gòu)清晰、便于二次編輯的打包程序

然后,用 pyinstaller build-hello.spec 進(jìn)行打包,再執(zhí)行得到的 hello.exe,得到如下輸出:

PyInstaller 完美打包 Python 腳本,輸出結(jié)構(gòu)清晰、便于二次編輯的打包程序

可見(jiàn) hook.py 確實(shí)在 hello.py 之前運(yùn)行了,且打印出了 sys.path ,即模塊查找路徑,有三個(gè):

  • dist/hello/base_library.zip 這個(gè)是程序所在目錄的 base_library.zip 文件
  • dist/hello/lib-dynload 這個(gè)是運(yùn)行程序時(shí)動(dòng)態(tài)生成的
  • dist/hello/ 這個(gè)是程序所在目錄

hook 修改 sys.path

因此,我們就可以在打包輸出文件夾中新建一個(gè) libs 文件夾,將所有的依賴(lài)文件全都放進(jìn)去,然后在 hook.py 里把 libs 路徑加入 sys.path ,然后我們的腳本運(yùn)行時(shí)就正確搜索到依賴(lài)包了。

改寫(xiě) hook.py

import sys
from pathlib import Path
from pprint import pprint

BASE_DIR = Path(__file__).parent

for p in sys.path.copy():
    relative_p = Path(p).relative_to(BASE_DIR)
    new_p = BASE_DIR / 'libs' / relative_p
    sys.path.insert(0, str(new_p))

print(f'\n\n模塊查找路徑:')
pprint(sys.path)

print('\n')

然后,用 pyinstaller build-hello.spec 進(jìn)行打包,再執(zhí)行得到的 hello.exe,得到如下輸出:

PyInstaller 完美打包 Python 腳本,輸出結(jié)構(gòu)清晰、便于二次編輯的打包程序

從輸出可以看到模塊查找路徑,已經(jīng)修改成功,新增了 libs 文件夾。

既然模塊查找路徑添加成功。那我們就 手動(dòng) 把所有的依賴(lài)文件都移動(dòng)到 libs 子文件夾中,再運(yùn)行 hello.exe ,完美運(yùn)行:

PyInstaller 完美打包 Python 腳本,輸出結(jié)構(gòu)清晰、便于二次編輯的打包程序

需要注意的是:由于 hook 也是 python 腳本,運(yùn)行 hook 需要 python 環(huán)境,所以 python310.dllbase_library.zip 不能移動(dòng)到 libs 文件夾中。

我用的 Python 版本是3.10,所以會(huì)有一個(gè) python310.dll,具體的文件名會(huì)隨你安裝的 Python 版本而變化

查看依賴(lài)目標(biāo)位置

雖然我們?cè)诖虬髮⒁蕾?lài)文件移動(dòng)到 libs 文件夾,程序能正常運(yùn)行,但是我們肯定不希望每次打包都要 手動(dòng) 移動(dòng)一次。

實(shí)際上我們可以在 spec 文件中定義依賴(lài)文件和二進(jìn)制文件的存放位置。

pyinstaller 在執(zhí)行 spec 文件中的代碼時(shí),自動(dòng)分析找到所需的依賴(lài)文件后,會(huì)把他們的目標(biāo)路徑和原始路徑寫(xiě)到 a.binaries ,我們可以把它打印出來(lái)看一下。

修改 hello.spec 文件

# -*- mode: python ; coding: utf-8 -*-


block_cipher = None


a = Analysis(
    ['hello.py'],
    pathex=[],
    binaries=[],
    datas=[],
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=['hook.py'],
    excludes=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)


from pprint import pprint
pprint(a.binaries)  # 打印 a.binaries


pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
    pyz,
    a.scripts,
    [],
    exclude_binaries=True,
    name='hello',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    console=True,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)
coll = COLLECT(
    exe,
    a.binaries,
    a.zipfiles,
    a.datas,
    strip=False,
    upx=True,
    upx_exclude=[],
    name='hello',
)

然后,用 pyinstaller build-hello.spec 進(jìn)行打包過(guò)程中得到如下輸出:

[('api-ms-win-crt-runtime-l1-1-0.dll',
  'C:\\Portable_library\\java\\jdk-14.0.1\\bin\\api-ms-win-crt-runtime-l1-1-0.dll',
  'BINARY'),
 ('python310.dll',
  'C:\\Users\\Haujet\\AppData\\Local\\Programs\\Python\\Python310\\python310.dll',
  'BINARY'),
 ('api-ms-win-crt-heap-l1-1-0.dll',
  'C:\\Portable_library\\java\\jdk-14.0.1\\bin\\api-ms-win-crt-heap-l1-1-0.dll',
  'BINARY'),
 ('VCRUNTIME140.dll',
  'C:\\Users\\Haujet\\AppData\\Local\\Programs\\Python\\Python310\\VCRUNTIME140.dll',
  'BINARY'),
  # 剩下的項(xiàng)就省略了
  ]

可以看到,a.binaries 是一個(gè)列表,其中的元素是元組,元組有3個(gè)內(nèi)容:

  1. 依賴(lài)文件目標(biāo)路徑
  2. 依賴(lài)文件原始路徑
  3. 文件類(lèi)型

我們只需要修改 a.binaries ,在目標(biāo)路徑前加上 libs 就可以了,同時(shí),要確保 python310.dllbase_library.zip 不被修改。

修改依賴(lài)目標(biāo)位置

編輯 build-hello.spec 文件:

# -*- mode: python ; coding: utf-8 -*-


block_cipher = None


a = Analysis(
    ['hello.py'],
    pathex=[],
    binaries=[],
    datas=[],
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=['hook.py'],
    excludes=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)


import re
import os

# 用一個(gè)函數(shù)選擇性對(duì)依賴(lài)文件目標(biāo)路徑改名
def new_dest(package: str):
    if package == 'base_library.zip' or re.match(r'python\d+.dll', package):
        return package
    return 'libs' + os.sep + package

a.binaries = [(new_dest(x[0]), x[1], x[2]) for x in a.binaries]


# 打印 a.binaries,檢查依賴(lài)文件目標(biāo)路徑
from pprint import pprint
pprint(a.binaries)


pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
    pyz,
    a.scripts,
    [],
    exclude_binaries=True,
    name='hello',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    console=True,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)
coll = COLLECT(
    exe,
    a.binaries,
    a.zipfiles,
    a.datas,
    strip=False,
    upx=True,
    upx_exclude=[],
    name='hello',
)

然后,用 pyinstaller build-hello.spec 進(jìn)行打包,再執(zhí)行得到的 hello.exe,得到如下輸出:

[('libs\\VCRUNTIME140.dll',
  'C:\\Users\\Haujet\\AppData\\Local\\Programs\\Python\\Python310\\VCRUNTIME140.dll',
  'BINARY'),
 ('python310.dll',
  'C:\\Users\\Haujet\\AppData\\Local\\Programs\\Python\\Python310\\python310.dll',
  'BINARY'),
 ('libs\\_decimal.pyd',
  'C:\\Users\\Haujet\\AppData\\Local\\Programs\\Python\\Python310\\DLLs\\_decimal.pyd',
  'EXTENSION'),
  # 剩下的省略了
 ]

得到了干凈的輸出目錄, hello.exe 也能夠正常運(yùn)行:

PyInstaller 完美打包 Python 腳本,輸出結(jié)構(gòu)清晰、便于二次編輯的打包程序

但是如你所見(jiàn),這個(gè)程序脾氣不好,爆粗口,用戶可能會(huì)想要修改其中的代碼,但又不想配置環(huán)境、重新打包。

因此接下來(lái)我們就要把 hello.exe 作為程序入口,實(shí)際的邏輯寫(xiě)在 hello_main.py ,同時(shí)確保 hello_main.py 中的依賴(lài)都被正確打包到 libs 文件夾。這樣,用戶就可以通過(guò)編輯 hello_main.py 來(lái)修改程序行為了。

第二步:打包可修改程序

制作入口

新建文件 hello_main.py ,將 hello.py 的代碼邏輯復(fù)制進(jìn)去,并且要稍作修改:

# coding: utf-8

from rich import print

def main(*args, **kwargs):

    print('[red]Hello mother fucker! ')
    input('按下回車(chē)?yán)^續(xù)')

if __name__ == "__main__":
    main()

然后修改 hello.py,將其制作成程序入口,調(diào)用 hello_main.py 中的 main 函數(shù):

# coding: utf-8

import hello_main

hello_main.main()

PyInstaller 完美打包 Python 腳本,輸出結(jié)構(gòu)清晰、便于二次編輯的打包程序

然后,用 pyinstaller build-hello.spec 進(jìn)行打包,但是我們會(huì)發(fā)現(xiàn),打包出的程序與之前一模一樣,雖然打包出的 hello.exe 能正常運(yùn)行,但是我們卻找不到 hello_main.py

PyInstaller 完美打包 Python 腳本,輸出結(jié)構(gòu)清晰、便于二次編輯的打包程序

查看被打包的 py 模塊

找不到 hello_main.py 的原因是,它被打包進(jìn)了 hello.exe 中,所有被引用到的 py 文件都會(huì)被打包進(jìn) exe 文件中。

我們回顧一下開(kāi)頭 spec 文件中內(nèi)容的注釋?zhuān)?/p>

# 除此之外,a 還有一些沒(méi)有列出的屬性:
#   pure 是一個(gè)列表,包含了所有純 Python 模塊的信息,這些模塊會(huì)被打包到一個(gè) .pyz 文件中。
#   scripts 是一個(gè)列表,包含了你的 Python 腳本的信息。這些腳本會(huì)被打包到一個(gè) exe 文件中。

hello.py 是主腳本,會(huì)被加到 a.scripts 列表中,進(jìn)而打包到 exe 中,hello_main.py 則是作為被導(dǎo)入的 py 模塊,被加到了 a.pure 列表,后序被打包到 pyz 中。我們可以編輯 build-hello.spec,在打包過(guò)程中顯示出有哪些 py 文件被打包了:

a = Analysis(
    ['hello.py'],
    pathex=[],
    binaries=[],
    datas=[],
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=['hook.py'],
    excludes=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)

import re
import os

# 用一個(gè)函數(shù)選擇性對(duì)依賴(lài)文件目標(biāo)路徑改名
def new_dest(package: str):
    if package == 'base_library.zip' or re.match(r'python\d+.dll', package):
        return package
    return 'libs' + os.sep + package

a.binaries = [(new_dest(x[0]), x[1], x[2]) for x in a.binaries]


# 打印 a.pure,顯示哪些 py 文件被打包
from pprint import pprint
pprint(a.pure)

pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

... # 后面的代碼省略了

然后,用 pyinstaller build-hello.spec 進(jìn)行打包,在輸出中可以搜索到:

[
...
 ('http.cookiejar',  '...\\Python310\\lib\\http\\cookiejar.py',  'PYMODULE'),
 ('hello_main',      'D:\\PyInstaller優(yōu)雅打包\\hello_main.py', 'PYMODULE'),
 ('rich',            '...Python310\\lib\\site-packages\\rich\\__init__.py','PYMODULE'),
 ...
 ]

hello_main 赫然在列。

阻止 py 模塊被打包

既然 hello_main.py 是因?yàn)楸蛔詣?dòng)加入到 a.pure 列表導(dǎo)致被打包的,那我們就可以在 spec 文件中將它從 a.pure 中剔除。

此外,我們還需要將 hello_main.py 添加到 a.datas 列表中,將它作為普通文件被復(fù)制到打包文件夾,編輯 build-hello.spec

# -*- mode: python ; coding: utf-8 -*-


block_cipher = None


a = Analysis(
    ['hello.py'],
    pathex=[],
    binaries=[],
    datas=[],
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=['hook.py'],
    excludes=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)

import re
import os

# 用一個(gè)函數(shù)選擇性對(duì)依賴(lài)文件目標(biāo)路徑改名,重定向到 libs 文件夾
def new_dest(package: str):
    if package == 'base_library.zip' or re.match(r'python\d+.dll', package):
        return package
    return 'libs' + os.sep + package

a.binaries = [(new_dest(x[0]), x[1], x[2]) for x in a.binaries]

# 將需要排除的模塊寫(xiě)到一個(gè)列表(不帶 .py)
my_modules = ['hello_main', ]

# 將被排除的模塊添加到 a.datas
for name in my_modules:
    source_file = name + '.py'
    dest_file = name + '.py'
    a.datas.append((source_file, dest_file, 'DATA'))

# 篩選 a.pure
a.pure = [x for x in a.pure if x[0] not in my_modules]

# 打印 a.dates ,顯示哪些文件被復(fù)制到打包文件夾
from pprint import pprint
pprint(a.datas)

pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
    pyz,
    a.scripts,
    [],
    exclude_binaries=True,
    name='hello',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    console=True,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)
coll = COLLECT(
    exe,
    a.binaries,
    a.zipfiles,
    a.datas,
    strip=False,
    upx=True,
    upx_exclude=[],
    name='hello',
)

此時(shí),hook.py 中的 print 語(yǔ)句可以刪掉了。

然后,用 pyinstaller build-hello.spec 進(jìn)行打包,輸出中得到:

[
 ('base_library.zip', 'D:\\PyInstaller優(yōu)雅打包\\build\\hello\\base_library.zip', 'DATA'),
 ('hello_main.py', 'hello_main.py', 'DATA')
]

PyInstaller 完美打包 Python 腳本,輸出結(jié)構(gòu)清晰、便于二次編輯的打包程序

同時(shí)也可以在打包輸出文件夾中看到 hello_main.py 了,并且程序能正常執(zhí)行:

PyInstaller 完美打包 Python 腳本,輸出結(jié)構(gòu)清晰、便于二次編輯的打包程序

編輯 py 后再運(yùn)行

現(xiàn)在,用戶就可以編輯 hello_main.py 后直接從 hello.exe 運(yùn)行了,不需要重新打包(需要引入新庫(kù)的情況除外)。

用戶終于可以動(dòng)手把這個(gè)脾氣暴躁的程序教育成一個(gè)健康積極的程序了:

PyInstaller 完美打包 Python 腳本,輸出結(jié)構(gòu)清晰、便于二次編輯的打包程序

第三步:理解 PyInstaller 打包過(guò)程

以上兩步已經(jīng)可以把簡(jiǎn)單的程序打包成目錄簡(jiǎn)潔、可以修改的 exe 了。

但如果你的項(xiàng)目復(fù)雜一點(diǎn),用了一些第三方依賴(lài)庫(kù),如 numpy 或 librosa ,僅僅用以上兩步,可能還會(huì)出錯(cuò),無(wú)法運(yùn)行。

只要理解了 PyInstaller 打包過(guò)程,就很好解決了。

不要再用命令行打包了

網(wǎng)上膚淺的教程會(huì)告訴你,使用 pyinstaller <參數(shù)> <參數(shù)> hello.py 這樣的命令行去打包,需要添加文件或者排除模塊,就加到命令行里面,這樣十分繁瑣,命令行非常不好寫(xiě)。

但經(jīng)過(guò)以上兩步,你應(yīng)該已經(jīng)了解了,PyInstaller 實(shí)際上是接收命令行參數(shù)后,生成了一個(gè) spec 文件,然后執(zhí)行 spec 文件里的 Python 代碼,來(lái)完成打包的。

因此,我們沒(méi)有必要去編寫(xiě)復(fù)雜的命令行,費(fèi)時(shí)費(fèi)力,我們直接編輯 spec 文件,進(jìn)行打包,用熟悉的 Python 語(yǔ)言去修改打包過(guò)程,并且能夠跨平臺(tái)執(zhí)行打包程序。

spec 打包代碼做了什么

在自動(dòng)生成的 build-hello.spec 中,你可以看到里面新建了 5 個(gè)變量,在這里我用做一個(gè)初步說(shuō)明:

# 變量 block_cipher 用于存儲(chǔ)加密的密碼,默認(rèn)為 None
block_cipher = None


# 變量 a 是一個(gè) Analysis 對(duì)象
# 把要打包的腳本傳給他,初始化的過(guò)程中,他會(huì)分析依賴(lài)情況
# 最后會(huì)生成 a.pure  a.scripts  a.binaries  a.datas 這樣4個(gè)關(guān)鍵列表,以及 a.zipped_data (不重要)
# 其中:
#     a.pure 是依賴(lài)的純 py 文件,
#     a.scripts 是要依次執(zhí)行的腳本文件,
#     a.binaries 是依賴(lài)的二進(jìn)制文件,
#     a.datas 是要復(fù)制的普通文件
# 分析的這一步最耗時(shí)間
a = Analysis(
    ['hello.py'],
    pathex=[],
    binaries=[],
    datas=[],			
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=['hook.py'],
    excludes=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)


# 變量 pyz 是一個(gè) PYZ 對(duì)象,默認(rèn)給他傳入 a.pure 和 a.zipped_data 
# 初始化過(guò)程中,它會(huì)把 a.pure  a.zipped_data 打包成一個(gè) pyz 文件
# 如果有密碼,還會(huì)加密打包
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)


# 變量 exe 是一個(gè) EXE 對(duì)象,
# 給它傳入打包好的 pyz 文件、a.scripts、程序名、圖標(biāo)、是否顯示Console、是否debug
# 最后他會(huì)打包生成一個(gè) exe 文件
exe = EXE(
    pyz,
    a.scripts,
    [],
    exclude_binaries=True,
    name='hello',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    console=True,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)

# 變量 coll 是一個(gè) COLLECT 對(duì)象,
# 給它傳入:
# 	exe  
# 	a.binaries  二進(jìn)制文件
# 	a.dattas	普通文件
# 	name        輸出文件夾名字
# 在實(shí)例化的過(guò)程中,會(huì)把傳入的這些項(xiàng)目,都復(fù)制到 name 文件夾中
coll = COLLECT(
    exe,
    a.binaries,
    a.zipfiles,
    a.datas,
    strip=False,
    upx=True,
    upx_exclude=[],
    name='hello',
)

簡(jiǎn)述一下就是:

  • 將加密的密碼保存到變量 block_cipher
  • 分析傳入的腳本,生成 Analysis 對(duì)象,保存到變量 a
  • 傳入 a 中的純 py 依賴(lài)文件,生成 PYZ 對(duì)象,初始化過(guò)程中打包出 pyz 格式文件,相關(guān)信息保存到變量 pyz
  • 傳入 a 中要執(zhí)行的腳本、pyz ,生成 EXE 對(duì)象,初始化過(guò)程中打包出 exe 格式文件,相關(guān)信息保存到變量 exe
  • 生成 COLL 對(duì)象,初始化過(guò)程將要收集的文件復(fù)制到目標(biāo)文件夾,相關(guān)信息保存到變量 coll

重中之重「變量 a」

因此,要復(fù)制哪些文件,哪些文件會(huì)被打包,文件會(huì)被復(fù)制到哪里,重中之重就是變量 a

下面來(lái)看一下變量 a 接收哪些參數(shù):


# 變量 a 是一個(gè) Analysis 對(duì)象
# 第一個(gè)參數(shù)接收傳入要分析的 py 文件,你可以傳入多個(gè) py 文件
# binaries 		接收要復(fù)制的二進(jìn)制文件,如 dll so 文件
# datas  		接收要復(fù)制的普通文件或文件夾
# hiddenimports 接收要強(qiáng)制復(fù)制的模塊名(即便沒(méi)有分析到)
# runtime_hooks 接收 hook 腳本,打包好的 exe 執(zhí)行時(shí),hook 腳本會(huì)最先運(yùn)行
# excludes		接收強(qiáng)制不要復(fù)制的模塊名(即便分析到了)
# cipher		用于加密的密碼,默認(rèn)值就是上面的 block_cipher 變量 
a = Analysis(
    ['hello.py', 'goodbye.py'], 			# 你可以分析多個(gè) py 文件,但打包后只會(huì)執(zhí)行第一個(gè)
    pathex=[],
    binaries=[],
    datas=[('readme.md', 'readme.md')], # 接收元組列表,文件、文件夾皆可,形式:(src, dest)  	
    hiddenimports=[],					# 這玩意兒不好用,就不要用了
    hookspath=[],
    hooksconfig={},
    runtime_hooks=['hook.py'],			# 在 hook 中,我們可以為腳本的執(zhí)行提前布置環(huán)境變量、依賴(lài)包搜索路徑
    excludes=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)

用 a.datas 完整復(fù)制依賴(lài)庫(kù)

舉個(gè)例子,假如我們的 hello.py 中用到了 numpy (或 librosa)中的一些功能,它自動(dòng)分析后,打包出的依賴(lài)庫(kù)運(yùn)行總是有些問(wèn)題,有時(shí)候會(huì)缺一些文件,需要我手動(dòng)把 numpy 依賴(lài)庫(kù)完整的復(fù)制過(guò)去才能解決。

我當(dāng)然不希望每次打包完之后都去手動(dòng)復(fù)制文件夾,那樣多累呀。

那我就把依賴(lài)庫(kù) numpy 所在文件夾加到 datas 參數(shù)中,讓 numpy 作為普通文件夾被復(fù)制到 libs 文件夾:


from importlib.util import find_spec	# 用于查找模塊所在路徑
from os.path import dirname
from os import path
from pprint import pprint
import os, re


# 空列表,用于準(zhǔn)備要復(fù)制的數(shù)據(jù)
datas = []

# 這是要額外復(fù)制的模塊
manual_modules = ['numpy', 'librosa']
for m in manual_modules:
    if not find_spec(m): continue
    p1 = dirname(find_spec(m).origin)
    p2 = path.join('libs', m)
    datas.append((p1, p2))  	# 以 (src, dest) 元組的形式添加到 datas 列表

# 這是要額外復(fù)制的文件
my_files = ['readme.md', ]
for f in my_files:
    datas.append((f, '.'))		# dest 填成了 . 表示復(fù)制到打包目標(biāo)路徑的根目錄

#===============================================================================

a = Analysis(
    ['hello.py'],
    pathex=[],
    binaries=[],
    datas=datas,				# 把我們準(zhǔn)備好的 datas 列表傳入
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=['build_hook.py'],
    excludes=['numpy', 'librosa'],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)

pprint(a.datas)
exit()

a 初始化完成后,a.datas 中的元素就不再是 (src, dest) 元組的形式了,而是會(huì)變成 (dest, src, 'DATA') 這樣的形式,如:

 ('base_library.zip',
  'D:\\Pyinstaller優(yōu)雅打包\\build\\hello\\base_library.zip',
  'DATA'),
('libs\\numpy\\version.py',
  'D:\\Python310\\site-packages\\numpy\\version.py',
  'DATA'),
('libs\\numpy\\matlib.py',
  'D:\\Python310\\site-packages\\numpy\\matlib.py',
  'DATA'),

這是因?yàn)樵诔跏蓟倪^(guò)程中,Analysis 類(lèi)中的代碼分析了我們傳入的 datas 列表,如果列表元素是個(gè)文件夾,就遞歸查找里面的文件,如果是文件,就查找絕對(duì)路徑。最后,生成一個(gè)全新的 a.datas 列表,里面的元素是 (dest, src, 'DATA') 的形式。

用 a.datas 復(fù)制二進(jìn)制文件

在分析完成后,變量 a 里會(huì)有一個(gè) a.binaries 列表,打印出來(lái)是這樣的:

 ('python310.dll',
  'D:\\Python310\\python310.dll',
  'BINARY'),
 ('numpy\\core\\_multiarray_tests.cp310-win_amd64.pyd',
  'D:\\Python310\\site-packages\\numpy\\core\\_multiarray_tests.cp310-win_amd64.pyd',
  'EXTENSION'),

它是用于將 py 文件依賴(lài)的二進(jìn)制文件(如 dllso 文件)復(fù)制到打包目錄。

但他有個(gè)缺陷,二進(jìn)制文件被 a.binaries 復(fù)制到打包目錄后,一些依賴(lài)庫(kù),運(yùn)行時(shí)就只會(huì)到根目錄去查找這些二進(jìn)制文件,而不去我們自定義的 libs 目錄。

為了解決這個(gè)問(wèn)題,我們可以在分析完成后把 a.binaries 列表里面要復(fù)制的二進(jìn)制文件,放到 a.datas 列表,作為普通文件復(fù)制到 libs 文件夾,并且把 a.binaries 清空:

# 前面的部分省略了

a = Analysis(
    ['hello.py'],
    pathex=[],
    binaries=[],
    datas=datas,				# 把我們準(zhǔn)備好的 datas 列表傳入
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=['build_hook.py'],
    excludes=['numpy', 'librosa'],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)


# 把 a.binaries 中的二進(jìn)制文件放到 a.datas ,作為普通文件復(fù)制到 libs 目錄
for b in a.binaries:
    c1 = (b[0]=='Python')                       # 不修改 Pyhton 		的目標(biāo)路徑
    c2 = re.fullmatch(r'python\d+\.dll', b[0])  # 不修改 python310.dll 的目標(biāo)路徑
    if any([c1, c2]):
        a.datas.append((b[0], b[1], 'DATA'))
    else:
        a.datas.append((path.join('libs', b[0]), b[1], 'DATA'))
a.binaries.clear()

用 a.datas 復(fù)制純 py 依賴(lài)

a.pure 中存儲(chǔ)了所有依賴(lài)的純 py 文件,默認(rèn)是會(huì)被打包到 exe 文件中,這個(gè)列表打印出來(lái)是這樣的形式:

 ('copy', 'D:\\Python310\\lib\\copy.py', 'PYMODULE'),
 ('ast','D:\\Python310\\lib\\ast.py','PYMODULE'),
 ('rich.segment',
  'D:\\Python310\\lib\\site-packages\\rich\\segment.py',
  'PYMODULE'),
# 剩下的省略了

如果第三方依賴(lài)庫(kù)的 py 文件被打包到 exe 中,那么有的第三方依賴(lài)庫(kù)也會(huì)只到根目錄查找二進(jìn)制文件,而不到 libs 文件夾中查找二進(jìn)制文件。

因此我們可以把 a.binaries 中的純 py 文件放到 a.datas 列表中,定向到 libs 文件夾中,當(dāng)然,在這一步,如果你不希望自己編寫(xiě)的 py 文件被別人看到,就可以加一個(gè)篩選條件,讓自己寫(xiě)的依賴(lài)庫(kù)保留在 a.pure 中:

# 把所有的 py 文件依賴(lài)用 a.datas 復(fù)制到 libs 文件夾
# 可選地保留某些要打包的依賴(lài)
private_module = []                         # hello.exe 不保留任何依賴(lài)
temp = a.pure.copy(); a.pure.clear()
for name, src, type in temp:
    condition = [name.startswith(m) for m in private_module]
    if condition and any(condition):
        a.pure.append((name, src, type))    # 把需要保留打包的 py 文件重新添加回 a.pure
    else:
        name = name.replace('.', os.sep)
        init = path.join(name, '__init__.py')
        pos = src.find(init) if init in src else src.find(name)
        dst = src[pos:]
        dst = path.join('libs', dst)
        a.datas.append((dst, src, 'DATA'))  # 不需要打包的第三方依賴(lài) py 文件引到 libs 文件夾

hook 是怎樣起作用的

分析完依賴(lài)庫(kù),生成 a 之后,有一個(gè)屬性 a.scripts,他就是 exe 所要執(zhí)行的腳本順序,打印出來(lái)是這樣的:

[('hook', 'D:\\PyInstaller優(yōu)雅打包\\hook.py', 'PYSOURCE'),
 ('pyi_rth_inspect',
  'D:\\Python310\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py',
  'PYSOURCE'),
 ('pyi_rth_pkgres',
  'D:\\Python310\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pkgres.py',
  'PYSOURCE'),
 ('pyi_rth_pkgutil',
  'D:\\Python310\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pkgutil.py',
  'PYSOURCE'),
 ('pyi_rth_multiprocessing',
  'D:\\Python310\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_multiprocessing.py',
  'PYSOURCE'),
 ('pyi_rth_traitlets',
  'D:\\Python310\\Lib\\site-packages\\_pyinstaller_hooks_contrib\\hooks\\rthooks\\pyi_rth_traitlets.py',
  'PYSOURCE'),
 ('hello', 'D:\\PyInstaller優(yōu)雅打包\\hello.py', 'PYSOURCE')]

你可以看到,hook.py 被列到了第一位,也就是 exe 一定會(huì)先執(zhí)行 hook.py,在后邊還有一些名字很奇怪的腳本,它們是 pyinstaller 自動(dòng)添加的,a.scripts 中的腳本會(huì)被依次執(zhí)行,配置好環(huán)境,最后執(zhí)行 hello.py

如果你給 a 傳入了多個(gè)腳本分析,這些腳本都會(huì)被依次添加到 a.scripts 列表中。

此刻,你就應(yīng)該能夠了解到給 exe 變量傳入的 a.scripts 參數(shù)是做什么用的了,就是腳本執(zhí)行順序。

打包 hello 最終 spec

在了解了整個(gè)打包過(guò)程的細(xì)節(jié)后,就可以為 hello.py 寫(xiě)一個(gè)最終版的打包 spec 了,在這個(gè) spec 里,要做到:

  • 默認(rèn)所有第三方依賴(lài)的 py 文件都被復(fù)制到 libs 文件夾中
  • 默認(rèn)所有第三方依賴(lài)的二進(jìn)制文件都被復(fù)制到 libs 文件夾中
  • 可以添加篩選,讓一些我們自己寫(xiě)的 py 依賴(lài)被打包到 exe,以起到隱藏代碼的作用
  • 自動(dòng)復(fù)制普通文件,如 readme.md

來(lái)看一下最終版的 build-hello.spec

# -*- mode: python ; coding: utf-8 -*-


block_cipher = None


# ===========================添加要額外復(fù)制的文件和文件夾==================================

from importlib.util import find_spec	# 用于查找模塊所在路徑
from os.path import dirname
from os import path
from pprint import pprint
import os, re


# 空列表,用于準(zhǔn)備要復(fù)制的數(shù)據(jù)
datas = []

# 這是要額外復(fù)制的模塊
manual_modules = []
for m in manual_modules:
    if not find_spec(m): continue
    p1 = dirname(find_spec(m).origin)
    p2 = m
    datas.append((p1, p2))

# 這是要額外復(fù)制的文件夾
my_folders = ['assets']
for f in my_folders:
    datas.append((f, f))

# 這是要額外復(fù)制的文件
my_files = ['hello_main.py', 'readme.md']
for f in my_files:
    datas.append((f, '.'))      # 復(fù)制到打包導(dǎo)出的根目錄
    
    
    
    
# ==================新建 a 變量,分析腳本============================

a = Analysis(
    ['hello.py'],               # 分析 hello.py
    pathex=[],
    binaries=[],
    datas=datas,                # 把我們準(zhǔn)備好的 datas 列表傳入
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=['hook.py'],  # 一定要傳入 hook.py 用于修改模塊查找路徑
    excludes=['IPython'],       # 有時(shí) pyinstaller 會(huì)抽風(fēng),加入一些不需要的包,在這里排除掉
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)

#===================分析完成后,重定向 a 的二進(jìn)制、py文件到 libs 文件夾================================


# 把 a.datas 中不屬于自定義的文件重定向到 libs 文件夾
temp = a.datas.copy(); a.datas.clear()
for dst, src, type in temp:
    c1 =  (dst == 'base_library.zip')                 # 判斷文件是否為 base_library.zip
    c2 = any([dst.startswith(f) for f in my_folders]) # 判斷文件是否屬于 my_folders
    c3 = any([dst.startswith(f) for f in my_files])	  # 判斷文件是否屬于 my_files
    if any([c1, c2, c3]):
        a.datas.append((dst, src, type))
    else:
        a.datas.append((path.join('libs', dst), src, type))

# 把 a.binaries 中的二進(jìn)制文件放到 a.datas ,作為普通文件復(fù)制到 libs 目錄
for dst, src, type in a.binaries:
    c1 = (dst=='Python')                       # 不修改 Pyhton 
    c2 = re.fullmatch(r'python\d+\.dll', dst)  # 不修改 python310.dll
    if any([c1, c2]):
        a.datas.append((dst, src, 'DATA'))
    else:
        a.datas.append((path.join('libs', dst), src, 'DATA'))
a.binaries.clear()

# 把所有的 py 文件依賴(lài)用 a.datas 復(fù)制到 libs 文件夾
# 可選地保留某些要打包的依賴(lài)
private_module = []                         # hello.exe 不保留任何依賴(lài)
temp = a.pure.copy(); a.pure.clear()
for name, src, type in temp:
    condition = [name.startswith(m) for m in private_module]
    if condition and any(condition):
        a.pure.append((name, src, type))    # 把需要保留打包的 py 文件重新添加回 a.pure
    else:
        name = name.replace('.', os.sep)
        init = path.join(name, '__init__.py')
        pos = src.find(init) if init in src else src.find(name)
        dst = src[pos:]
        dst = path.join('libs', dst)
        a.datas.append((dst, src, 'DATA'))  # 不需要打包的第三方依賴(lài) py 文件引到 libs 文件夾






# ========================為 a 生成 exe =========================


pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
    pyz,
    a.scripts,                  # 運(yùn)行 hello 的 scripts
    [],
    exclude_binaries=True,
    name='hello',               # 程序的創(chuàng)口貼名字叫 hello
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    console=True,               # 運(yùn)行時(shí)彈出終端窗口
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    icon=[],                    # 這里可以給 exe 加圖標(biāo),如果你有圖標(biāo)文件的話
)


# =============用 coll 把 exe 和其所屬的文件收集到目標(biāo)文件夾=========================

coll = COLLECT(
    exe,            # hello.exe
    a.binaries,     # hello.exe 的二進(jìn)制文件(實(shí)際上已被清空了)
    a.zipfiles, 
    a.datas,        # hello.exe 的依賴(lài)文件和自定義復(fù)制文件,都被我們導(dǎo)到了這里
    strip=False,
    upx=True,
    upx_exclude=[],
    name='hello',   # 輸出路徑在 dist 文件夾里的 hello 文件夾
)

這里的 hello.py 使用了第三方庫(kù) rich 輸出彩色文字,所以后打包體積會(huì)有點(diǎn)大,共計(jì) 22MB,打包的結(jié)果:

PyInstaller 完美打包 Python 腳本,輸出結(jié)構(gòu)清晰、便于二次編輯的打包程序

如果不用第三方庫(kù) rich ,只用自帶的 print ,打包出的體積就小了,只有 14MB:

PyInstaller 完美打包 Python 腳本,輸出結(jié)構(gòu)清晰、便于二次編輯的打包程序

打包多個(gè) exe

了解到這里,你想一下,能不能用一個(gè) spec 文件一次性打包多個(gè) exe 呢?

比如,我這個(gè)項(xiàng)目里,有 hello.pygoodbye.py,他們的依賴(lài)都差不多,如果分別打包到兩個(gè)文件夾,很多依賴(lài)包就會(huì)被兩次復(fù)制。

既然他們依賴(lài)包都差不多,那就把這兩個(gè) py 都打包到一個(gè)文件夾,生成兩個(gè) exe 不就成了。在一個(gè) spec 文件里新建兩個(gè) a ,各自分析不同的腳本,生成 2 個(gè) exe,最后,把兩個(gè) exea.datas 都傳給 coll,一次性打包到同一個(gè)目錄。

即便 hello.pygoodbye 的依賴(lài)有重疊也沒(méi)關(guān)系,他們都會(huì)被復(fù)制到 libs 文件夾,后來(lái)的會(huì)把先前的覆蓋掉,不會(huì)有重復(fù)的依賴(lài)。

新建一個(gè) build-hello-goodbye.spec ,里面寫(xiě)上這些內(nèi)容,注意閱讀注釋來(lái)理解:

# -*- mode: python ; coding: utf-8 -*-


block_cipher = None


# ===========================添加要額外復(fù)制的文件和文件夾==================================

from importlib.util import find_spec	# 用于查找模塊所在路徑
from os.path import dirname
from os import path
from pprint import pprint
import os, re


# 空列表,用于準(zhǔn)備要復(fù)制的數(shù)據(jù)
datas = []

# 這是要額外復(fù)制的模塊
manual_modules = []
for m in manual_modules:
    if not find_spec(m): continue
    p1 = dirname(find_spec(m).origin)
    p2 = m
    datas.append((p1, p2))

# 這是要額外復(fù)制的文件夾
my_folders = ['assets']
for f in my_folders:
    datas.append((f, f))

# 這是要額外復(fù)制的文件
my_files = ['hello_main.py', 'readme.md']
for f in my_files:
    datas.append((f, '.'))      # 復(fù)制到打包導(dǎo)出的根目錄
    
    
    
    
# ==================新建兩個(gè) a 變量,分析兩個(gè)腳本============================

a = Analysis(
    ['hello.py'],               # 分析 hello.py
    pathex=[],
    binaries=[],
    datas=datas,                # 把我們準(zhǔn)備好的 datas 列表傳入
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=['hook.py'],  # 一定要傳入 hook.py 用于修改模塊查找路徑
    excludes=['IPython'],       # 有時(shí) pyinstaller 會(huì)抽風(fēng),加入一些不需要的包,在這里排除掉
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)

a2 = Analysis(
    ['goodbye.py'],             # 分析 goodbye.py
    pathex=[],
    binaries=[],
    datas=[],				    # 要傳入的額外文件由第一個(gè) a 處理就行了,這里留空
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=['hook.py'],  # 一定要傳入 hook.py 用于修改模塊查找路徑
    excludes=['IPython'],       # 有時(shí) pyinstaller 會(huì)抽風(fēng),加入一些不需要的包,在這里排除掉
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)


#===================分析完成后,重定向 a 的二進(jìn)制、py文件到 libs 文件夾================================


# 把 a.datas 中不屬于自定義的文件重定向到 libs 文件夾
temp = a.datas.copy(); a.datas.clear()
for dst, src, type in temp:
    c1 =  (dst == 'base_library.zip')                 # 判斷文件是否為 base_library.zip
    c2 = any([dst.startswith(f) for f in my_folders]) # 判斷文件是否屬于 my_folders
    c3 = any([dst.startswith(f) for f in my_files])	  # 判斷文件是否屬于 my_files
    if any([c1, c2, c3]):
        a.datas.append((dst, src, type))
    else:
        a.datas.append((path.join('libs', dst), src, type))

# 把 a.binaries 中的二進(jìn)制文件放到 a.datas ,作為普通文件復(fù)制到 libs 目錄
for dst, src, type in a.binaries:
    c1 = (dst=='Python')                       # 不修改 Pyhton 
    c2 = re.fullmatch(r'python\d+\.dll', dst)  # 不修改 python310.dll
    if any([c1, c2]):
        a.datas.append((dst, src, 'DATA'))
    else:
        a.datas.append((path.join('libs', dst), src, 'DATA'))
a.binaries.clear()

# 把所有的 py 文件依賴(lài)用 a.datas 復(fù)制到 libs 文件夾
# 可選地保留某些要打包的依賴(lài)
private_module = []                         # hello.exe 不保留任何依賴(lài)
temp = a.pure.copy(); a.pure.clear()
for name, src, type in temp:
    condition = [name.startswith(m) for m in private_module]
    if condition and any(condition):
        a.pure.append((name, src, type))    # 把需要保留打包的 py 文件重新添加回 a.pure
    else:
        name = name.replace('.', os.sep)
        init = path.join(name, '__init__.py')
        pos = src.find(init) if init in src else src.find(name)
        dst = src[pos:]
        dst = path.join('libs', dst)
        a.datas.append((dst, src, 'DATA'))  # 不需要打包的第三方依賴(lài) py 文件引到 libs 文件夾




#============================重定向 a2 的二進(jìn)制、py文件================================

# 把 a2.datas 中不屬于自定義的文件重定向到 libs 文件夾
temp = a2.datas.copy(); a2.datas.clear()
for dst, src, type in temp:
    c1 =  (dst == 'base_library.zip')                 # 判斷文件是否為 base_library.zip
    c2 = any([dst.startswith(f) for f in my_folders]) # 判斷文件是否屬于 my_folders
    c3 = any([dst.startswith(f) for f in my_files])	  # 判斷文件是否屬于 my_files
    if any([c1, c2, c3]):
        a2.datas.append((dst, src, type))
    else:
        a2.datas.append((path.join('libs', dst), src, type))

# 把 a2.binaries 中的二進(jìn)制文件放到 a2.datas ,作為普通文件復(fù)制到 libs 目錄
for dst, src, type in a2.binaries:
    c1 = (dst=='Python')                       # 不修改 Pyhton 
    c2 = re.fullmatch(r'python\d+\.dll', dst)  # 不修改 python310.dll
    if any([c1, c2]):
        a2.datas.append((dst, src, 'DATA'))
    else:
        a2.datas.append((path.join('libs', dst), src, 'DATA'))
a2.binaries.clear()

# 把所有的 py 文件依賴(lài)用 a2.datas 復(fù)制到 libs 文件夾
# 可選地保留某些要打包的依賴(lài)
private_module = ['goodbye_main']           # 作為示例,hello.exe 把 goodbye_main.py 保留打包
temp = a2.pure.copy(); a2.pure.clear()
for name, src, type in temp:
    condition = [name.startswith(m) for m in private_module]
    if condition and any(condition):
        a2.pure.append((name, src, type))   # 把需要保留打包的 py 文件重新添加回 a.pure
    else:
        name = name.replace('.', os.sep)
        init = path.join(name, '__init__.py')
        pos = src.find(init) if init in src else src.find(name)
        dst = src[pos:]
        dst = path.join('libs', dst)
        a2.datas.append((dst, src, 'DATA')) # 不需要打包的第三方依賴(lài) py 文件引到 libs 文件夾



# ========================為 a 和 a2 生成 exe =========================


pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
pyz2 = PYZ(a2.pure, a2.zipped_data, cipher=block_cipher)

exe = EXE(
    pyz,
    a.scripts,                  # 運(yùn)行 hello 的 scripts
    [],
    exclude_binaries=True,
    name='hello',               # 程序的創(chuàng)口貼名字叫 hello
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    console=True,               # 運(yùn)行時(shí)彈出終端窗口
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    icon=[],                    # 這里可以給 exe 加圖標(biāo),如果你有圖標(biāo)文件的話
)

exe2 = EXE(
    pyz2,
    a2.scripts,                 # 運(yùn)行 goodbye 的 scripts
    [],
    exclude_binaries=True,
    name='goodbye',             # 程序的窗口名字叫 goodbye
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    console=True,               # 運(yùn)行時(shí)彈出終端窗口
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    icon=[],                     # 這里可以給 exe 加圖標(biāo),如果你有圖標(biāo)文件的話
)

# =============用 coll 把兩個(gè) exe 和其所屬的文件收集到目標(biāo)文件夾=========================

coll = COLLECT(
    exe,            # hello.exe
    exe2,           # goodbye.exe
    a.binaries,     # hello.exe 的二進(jìn)制文件(實(shí)際上已被清空了)
    a2.binaries,    # goodbye.exe 的二進(jìn)制文件(實(shí)際上已被清空了)
    a.zipfiles, 
    a2.zipfiles,
    a.datas,        # hello.exe 的依賴(lài)文件和自定義復(fù)制文件,都被我們導(dǎo)到了這里
    a2.datas,       # goodbye.exe 的依賴(lài)文件,都被我們導(dǎo)到了這里
    strip=False,
    upx=True,
    upx_exclude=[],
    name='build-hello-goodbye',   # 輸出路徑在 dist 文件夾里的 build-hello-goodbye 文件夾
)


        

build-hello-goodbye.spec 就是我們的多 exe 打包模板了。hello.pygoodbye.py 的依賴(lài)一模一樣,所以依賴(lài)庫(kù)大小不變,同時(shí)我們還把 goodbye_main.py 打包進(jìn)了 goodbye.exe 里,指定性地保護(hù)了我們自己寫(xiě)的代碼。

這是打包出的效果:

PyInstaller 完美打包 Python 腳本,輸出結(jié)構(gòu)清晰、便于二次編輯的打包程序

后記

多虧有 ChatGPT 這一個(gè)知識(shí)淵博、毫無(wú)厭倦的老師,耐心的回答我提出的每一個(gè)細(xì)節(jié)問(wèn)題,才能有這么一個(gè)完美的打包方案。

不過(guò)實(shí)際上,ChatGPT 也沒(méi)法提出這么一個(gè)完美的打包方案,它只是作為一個(gè)具有理解能力的活詞典,為我解釋一些細(xì)節(jié),所有這些內(nèi)容都是在深入理解之后才能夠構(gòu)思出來(lái)的,所以,在人工智能時(shí)代無(wú)需妄自菲薄,AI 只是一個(gè)更好用的工具。

最后總結(jié)出的 build-hello.spec 以及 build-hello-goodbye.spec 這兩個(gè)模塊,我分別在 Windows 和 MacOS 上進(jìn)行了測(cè)試,打包結(jié)果都很完美,以后再有別的項(xiàng)目可以放心拿來(lái)改。

整個(gè)項(xiàng)目最新代碼已經(jīng)放到:https://github.com/HaujetZhao/PyInstaller-Perfect-Build-Method ,歡迎查閱。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-472003.html

到了這里,關(guān)于PyInstaller 完美打包 Python 腳本,輸出結(jié)構(gòu)清晰、便于二次編輯的打包程序的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來(lái)自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • Python ---- pyInstaller 的常用打包命令

    1. 簡(jiǎn)介 PyInstaller是一個(gè)第三方庫(kù),它能夠在Windows、Linux、 Mac OS X 等操作系統(tǒng)下將 Python 源文件打包,通過(guò)對(duì)源文件打包, Python 程序可以在沒(méi)有安裝 Python 的環(huán)境中運(yùn)行,也可以作為一個(gè) 獨(dú)立文件方便傳遞和管理。 PyInstaller支持Python 2.7和Python 3.3+??梢栽赪indows、Mac OS X和Linu

    2024年02月14日
    瀏覽(24)
  • Python文件打包(pyinstaller,nuitka)

    ##背景:最近用Pyqt5編寫(xiě)界面后考慮到一些用戶沒(méi)有程序的運(yùn)行環(huán)境,故而需要打包為exe程序提供給用戶使用,在經(jīng)過(guò)一番資料的查詢(xún)以及踩坑后,寫(xiě)下了這篇博客,目前主要有兩種主流的方法。分別是使用Pyinstaller與Nuitka進(jìn)行打包。下面分享一下兩種方法的具體使用與心得。

    2024年02月16日
    瀏覽(31)
  • python打包Windows.exe程序(pyinstaller)

    python打包Windows.exe程序(pyinstaller) pip install pyinstaller 使用pip命令來(lái)安裝pyinstaller模塊。 -F: pyinstaller -F hello.py -p hello2.py -D: pyinstaller -D hello.py -p hello2.py -i : pyinstaller -i tb.ico -F hello.py -p hello2.py 其中前一個(gè)文件hello是主文件,后一個(gè)文件是會(huì)被調(diào)用到的文件,可以有多個(gè)。

    2024年02月13日
    瀏覽(29)
  • Python pyinstaller打包exe最完整教程

    Python pyinstaller打包exe最完整教程

    python提供了多種方法用于將普通的*.py程序文件編譯成exe文件(有時(shí)這里的“編譯”也稱(chēng)作“打包”)。exe文件即可執(zhí)行文件,打包后的*.exe應(yīng)用不用依賴(lài)python環(huán)境,可以在他人的電腦上運(yùn)行。 pyinstaller是一個(gè)第三方模塊,專(zhuān)用于python程序的exe打包。此外python還有一些別的方法

    2024年02月08日
    瀏覽(28)
  • 【Python】pyinstaller全新的打包方案,堪稱(chēng)完善!

    【Python】pyinstaller全新的打包方案,堪稱(chēng)完善!

    如果我要寫(xiě)一個(gè) Python 項(xiàng)目,打包成 exe 運(yùn)行(方便在沒(méi)有 Python 的電腦上使用),我需要打包出的根目錄結(jié)構(gòu)美觀,沒(méi)有多余的、雜亂的依賴(lài)文件在那里礙眼,而且需要在發(fā)現(xiàn) bug 時(shí),我還需要能夠修改里面的代碼后,無(wú)需再次打包,就能正常運(yùn)行,該怎么做呢? 就以一個(gè)

    2024年02月16日
    瀏覽(20)
  • Python PyInstaller將程序打包為exe程序

    Python PyInstaller將程序打包為exe程序

    1.執(zhí)行 pip install pyinstaller,結(jié)果如下圖? 2.然后執(zhí)行 pyinstaller -F -w Hello.py 執(zhí)行完之后會(huì)產(chǎn)生兩個(gè)目錄 在dist目錄下就是可執(zhí)行文件,雙擊即可看到效果

    2024年02月04日
    瀏覽(34)
  • python pyinstaller打包常見(jiàn)問(wèn)題(一):無(wú)法生成exe文件/打包閃退

    python pyinstaller打包常見(jiàn)問(wèn)題(一):無(wú)法生成exe文件/打包閃退

    小游戲程序,本地環(huán)境能正常運(yùn)行 程序調(diào)用了wav文件,即音效資源文件 程序調(diào)用wav路徑正確,也是采用絕對(duì)路徑 程序制作完成后,進(jìn)行了以下三步打包操作: 在需要打包的程序的目錄上,我進(jìn)入終端輸入以下命令進(jìn)行打包: 2.1、打開(kāi)生成的snake.spec文件 2.2、修改Analysis 的

    2024年01月22日
    瀏覽(94)
  • 【Python三種打包方式】分別使用PyInstaller & setupTools & 拷貝環(huán)境實(shí)現(xiàn)項(xiàng)目打包

    【Python三種打包方式】分別使用PyInstaller & setupTools & 拷貝環(huán)境實(shí)現(xiàn)項(xiàng)目打包

    pyinstaller 和 setuptools 打包有什么區(qū)別(來(lái)自 ChatGPT 的答案) pyinstaller 和 setuptools 都是 Python 中常用的打包工具,但它們的主要目的和工作方式略有不同。 pyinstaller 是一個(gè)可以將 Python 代碼 打包成一個(gè)可執(zhí)行文件的工具 ,它會(huì)將你的 Python 代碼和所有依賴(lài)項(xiàng)打包成一個(gè)單獨(dú)的可

    2024年02月03日
    瀏覽(29)
  • 【Python】Pyinstaller打包Linux運(yùn)行文件,暴露配置文件

    Pyinstaller可以將python程序打包成可執(zhí)行文件,在windows上是exe文件,在linux上是二進(jìn)制可執(zhí)行文件。 工作中有需求是把本地python工程打包成后臺(tái)可運(yùn)行的服務(wù),考慮到部署環(huán)境的不同,可以使用docker鏡像的方式,也可以使用pyinstaller打包成可執(zhí)行文件: docker鏡像的好處是運(yùn)行起

    2024年02月05日
    瀏覽(18)
  • 通過(guò)pyinstaller將python項(xiàng)目打包成exe執(zhí)行文件

    目錄 第一步:安裝pyinstaller 第二步:獲取一個(gè)ico圖標(biāo)(也即是自己這個(gè)exe文件最后的圖標(biāo)) 第三步:打包 pip install pyinstaller 先從這里獲取一個(gè)png圖片:iconfont-阿里巴巴矢量圖標(biāo)庫(kù) 再轉(zhuǎn)化成ico圖片(推薦256 x 256):PNG轉(zhuǎn)ICO - 在線轉(zhuǎn)換圖標(biāo)文件 ?可以把得到的ico圖片放在pytho

    2024年02月09日
    瀏覽(32)

覺(jué)得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包