Python:使用pyinstaller打包含有gettext locales語言環境的項目
- 2022 年 1 月 27 日
- 筆記
- gettext, pyinstaller, Python
測試
問題
如何使用 pyinstaller 打包使用了 gettext 本地化的項目,最終只生成一個 exe 文件
起因
最近在用 pyhton 做一個圖片處理的小工具,順便接觸了一下 gettext,用來實現本地化化中英文轉換。項目主要結構如下:
.
|--src # 源碼
| |--package1
| |--package2
| |--locales # 本地化文件
| | |--en # 英文
| | | |--LC_MESSAGES
| | | |--en.mo
| | |--zh # 中文
| | |--LC_MESSAGES
| | |--en.mo
| |--GUI.py # 介面
| |--main.py # 主程式
直接使用 pyinstaller -F src\main.py
命令進行打包,打包後運行在 dist 文件夾中生成的 main.exe 會報錯。原因是 gettext 找不到本地化文件。但如果試著將 locales 文件夾複製到 main.exe 的目錄下程式能正常運行,說明 pyinstaller 在打包時不會將 locales 文件夾打包進去。
複製 locales 文件夾到可執行文件目錄下固然可以運行,但這樣用起來會很麻煩。
解決方案
目標是將 locales 目錄一起打包進 exe 文件中,查閱 pyinstaller 的官方文檔,了解到執行之前的 pyinstaller -F src\\main.py
命令會在目錄下生成一個 .spec 文件,pyinstaller 通過該文件的內容來構建應用程式。
the first thing PyInstaller does is to build a spec (specification) file myscript.spec. That file is stored in the –specpath directory, by default the current directory.
The spec file tells PyInstaller how to process your script. It encodes the script names and most of the options you give to the pyinstaller command. The spec file is actually executable Python code. PyInstaller builds the app by executing the contents of the spec file.
使用記事本打開,.spec 文件裡面大致長這樣(來自官方例子)
block_cipher = None
a = Analysis(['minimal.py'],
pathex=['/Developer/PItests/minimal'],
binaries=None,
datas=None,
hiddenimports=[],
hookspath=None,
runtime_hooks=None,
excludes=None,
cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,... )
coll = COLLECT(...)
其中,Analysis 裡面有個 datas 參數,用於存放非二進位文件,也就是我們想讓程式包含的靜態文件。我們只要把 locales 目錄填到這裡面打包就會添加進去。當然不是填一個路徑就好了,data 的格式如下:
–add-data <SRC;DEST or SRC:DEST>
SRC 就是未打包前的文件路徑,DEST 是打包後文件的路徑。以我的項目為例,打包前 locales 在 src/locales,打包後我想講裡面的文件放到臨時目錄的根目錄下就填 ./locales,臨時目錄是什麼後面講。於是在我的 .spec文件里 datas 處就寫成 datas=[("src/locales","./locales")]
。如果有多個路徑就以這樣形式 datas=[(src1, dest1), (src2, dest2), ...]
就OK。
這樣打包部分的配置就改完了,不要急,還要改下源程式碼。exe 文件在運行時會生成一個臨時目錄,我們之前 datas 中的文件也會在該目錄下。看看你的源碼,如果調用資源用的是相對路徑,那讀取的是 exe 文件當前的目錄,必然是找不到資源的。所以要把源碼中相對路徑改成臨時目錄的絕對路徑。
sys 中的 _MEIPASS 屬性存儲了臨時目錄路徑,直接獲取即可。如果程式運行環境是打包後的,那麼在 sys 中會添加一個 frozen 屬性,通過能不能獲取 frozen 屬性可以判斷當前環境是打包前還是打包後,具體詳情請查閱 pyinstaller 官方文檔(末尾有地址)。打包前就不需要獲取臨時目錄路徑了,直接用文件所在目錄路徑就行。
注意:打包後環境 file 屬性不生效
import sys
import os
if getattr(sys, 'frozen', None):
dir = sys._MEIPASS
else:
dir = os.path.dirname(__file__)
獲取路徑 dir
,可以使用 os.path.join()
來拼接路徑,把源碼中調用 datas 中資源地方的路徑改成 os.path.join(dir, <打包後相對路徑>)
如我的項目中原來的 './locales'
處就變成了 os.path.join(dir, 'locales')
最後一步,打包!不要再輸之前的命令了,要使用改過之後的 .spec 文件進行打包,輸入 pyinstaller -F 文件名.spec
就完成了。
參考資料
- pyinstaller 文檔:Using Spec Files
- pyinstaller 文檔:Run-time Information