Python 讀取 Big5 編碼的 ZIP 壓縮檔
很難預想到,即便在 2022 年的現代作業系統中,仍然會遇到與古老 Big5 編碼有關的問題。然而就 Windows 來說確實是如此,時至今日為止,至少在隨處可見的 Windows 10 繁中環境裡,其預設的環境編碼依然是 CP950(微軟自行維護的 Big5 實作)。因此根據壓縮軟體的具體實作而異,所生產出來的 ZIP 壓縮檔,寫入的檔名清單不盡然都會是 Unicode,也有可能依然是 CP950。
延伸閱讀:libarchive 函式庫關於檔名處理的說明文件。
話說好段時間前 DR 曾因工作環境需要,寫了一支 Python 程式,用於讀取 *.zip、*.rar 及 *.7z 這幾種常見壓縮檔中的檔案清單,藉此對上傳附件的正確性做某種程度的檢查。其中針對 ZIP 的讀取,除了 Unicode 無須特別處理外,也是有考量到 CP950 的編碼。所以程式碼裡會嘗試對 ZipFile.infolist() 所回傳的物件清單,執行以下的檔名解碼動作:
filename.encode("cp437").decode("cp950)
起初似乎都運作正常,直到某日出現無法正確解碼的錯誤(illegal multibyte sequence)。於是將問題檔名列出來一看,赫然驚覺難不成是遇到了 Big5「許蓋功」的問題?在進一步的查看下,確實發覺 filename 背後的位元組不太正常,應該是十六進位值 5C 的地方,都變成了 2F。舉例來說,中文字「許」在 Big5 編碼裡應該是 B35C,但在 filename 裡面的值卻是 B32F,導致解碼失敗。
然而當下也搞不清楚這是怎麼發生的,而且其它 Windows 下的壓縮軟體也都能夠正常開啟該壓縮檔。於是索性先將解碼動作修改如下,程式能跑最重要:
filename.replace("\x2F", "\x5C").encode("cp437").decode("cp950)
直到最近比較有空之後,才想說回頭來看看究竟是怎麼回事。然而一開始的調查方向有點偏差,是聚焦在壓縮軟體處理檔名的過程,但翻找軟體的原始碼並未看出什麼特別的跡象。後來在 Linux 下測試時,發現相同的 Python 程式碼讀取同一個壓縮檔,並未出現如 Windows 那樣,數值異常以致解碼錯誤的情形,才發覺應該是在 Python 裡面發生了什麼奇怪的事情。
原來在 Python 的 zipfile 模組裡,會找尋 filename 裡面的路徑分隔符號(os.sep),並統一為正斜線「/」。由於 Windows 下的 os.sep 為反斜線「\」,因此就形成了「\」( 5C)皆被替換為「/」(2F)的情形。反之在 Linux(或者 WSL)下就不會有此情形,也就不會出現錯誤。
所以在使用 zipfile 模組來讀取 ZIP 壓縮檔時,更加直觀且相對健全的 Big5 檔名應對方式應是像這樣:
filename.replace("/", "\\").encode("cp437").decode("cp950").replace("\\", "/")
zip_encoding_test.py 是一支用於驗證前述情形的 Python 程式。程式碼雖有些繁雜,但基本功能很單純。倘若在 Windows 下執行時,它會執行 tar 指令,產生一個內含「許蓋功」的 test.zip。接著讀取該壓縮檔,分別列出 ZipInfo 物件中 orig_filename 及 filename 的十六進位值,藉此檢視 filename 屬性在 Windows 環境下所發生的變化。