引入問(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 把 hello.py
打包,pyinstaller ./hello.py
命令會(huì)得到 build
和 dist
文件夾,以及 hello.spec
文件:
為了方便文件名排序,在下面我把
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)文件之間,非常丑陋:
我們的目標(biāo),就是要把這些依賴(lài)包都移到一個(gè)子文件夾中,讓打包文件夾變得整潔,同時(shí)讓程序正常運(yùn)行。
最后我們可以打包成這個(gè)樣子:
首先,所有的依賴(lài)模塊都被移動(dòng)到了 libs 文件夾,整個(gè)打包根目錄清清爽爽,只留下了必要的 python310.dll
和 base_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 build-hello.spec
進(jìn)行打包,再執(zhí)行得到的 hello.exe
,得到如下輸出:
可見(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
,得到如下輸出:
從輸出可以看到模塊查找路徑,已經(jīng)修改成功,新增了 libs
文件夾。
既然模塊查找路徑添加成功。那我們就 手動(dòng) 把所有的依賴(lài)文件都移動(dòng)到 libs
子文件夾中,再運(yùn)行 hello.exe
,完美運(yùn)行:
需要注意的是:由于
hook
也是python
腳本,運(yùn)行hook
需要python
環(huán)境,所以python310.dll
和base_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)容:
- 依賴(lài)文件目標(biāo)路徑
- 依賴(lài)文件原始路徑
- 文件類(lèi)型
我們只需要修改 a.binaries
,在目標(biāo)路徑前加上 libs
就可以了,同時(shí),要確保 python310.dll
和 base_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)行:
但是如你所見(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 build-hello.spec
進(jìn)行打包,但是我們會(huì)發(fā)現(xiàn),打包出的程序與之前一模一樣,雖然打包出的 hello.exe
能正常運(yùn)行,但是我們卻找不到 hello_main.py
:
查看被打包的 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
中的
然后,用 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')
]
同時(shí)也可以在打包輸出文件夾中看到 hello_main.py
了,并且程序能正常執(zhí)行:
編輯 py 后再運(yùn)行
現(xiàn)在,用戶就可以編輯 hello_main.py
后直接從 hello.exe
運(yùn)行了,不需要重新打包(需要引入新庫(kù)的情況除外)。
用戶終于可以動(dòng)手把這個(gè)脾氣暴躁的程序教育成一個(gè)健康積極的程序了:
第三步:理解 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)制文件(如 dll
或 so
文件)復(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é)果:
如果不用第三方庫(kù) rich
,只用自帶的 print
,打包出的體積就小了,只有 14MB:
打包多個(gè) exe
了解到這里,你想一下,能不能用一個(gè) spec
文件一次性打包多個(gè) exe
呢?
比如,我這個(gè)項(xiàng)目里,有 hello.py
和 goodbye.py
,他們的依賴(lài)都差不多,如果分別打包到兩個(gè)文件夾,很多依賴(lài)包就會(huì)被兩次復(fù)制。
既然他們依賴(lài)包都差不多,那就把這兩個(gè) py
都打包到一個(gè)文件夾,生成兩個(gè) exe
不就成了。在一個(gè) spec
文件里新建兩個(gè) a
,各自分析不同的腳本,生成 2 個(gè) exe
,最后,把兩個(gè) exe
和 a.datas
都傳給 coll
,一次性打包到同一個(gè)目錄。
即便 hello.py
和 goodbye
的依賴(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.py
和 goodbye.py
的依賴(lài)一模一樣,所以依賴(lài)庫(kù)大小不變,同時(shí)我們還把 goodbye_main.py
打包進(jìn)了 goodbye.exe
里,指定性地保護(hù)了我們自己寫(xiě)的代碼。
這是打包出的效果:
后記
多虧有 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)改。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-472003.html
整個(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)!