Python:使用pyinstaller打包含有gettext locales語言環境的專案

WindChen發表於2022-01-27

問題

  如何使用 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 就完成了。

參考資料

  1. pyinstaller 文件:Using Spec Files
  2. pyinstaller 文件:Run-time Information

相關文章