當前位置:
首頁 > 知識 > 也談 Python 的中文編碼問題

也談 Python 的中文編碼問題

作者:in355hz

來源://in355hz.iteye.com/blog/1860787

最近業務中需要用 Python 寫一些腳本。儘管腳本的交互只是命令行 + 日誌輸出,但是為了讓界面友好些,我還是決定用中文輸出日誌信息。

很快,我就遇到了異常:

為了解決問題,我花時間去研究了一下 Python 的字元編碼處理。網上也有不少文章講 Python 的字元編碼,但是我看過一遍,覺得自己可以講得更明白些。

下面先複述一下 Python 字元串的基礎,熟悉此內容的可以跳過。

對應 C/C++ 的 char 和 wchar_t, Python 也有兩種字元串類型,str 與 unicode:

前面的申明:# -- coding: utf-8 -- 表明,上面的 Python 代碼由 utf-8 編碼。

為了保證輸出不會在 linux 終端上顯示亂碼,需要設置好 linux 的環境變數:export LANG=en_US.UTF-8

如果你和我一樣是使用 SecureCRT,請設置 Session Options/Terminal/Appearance/Character Encoding 為 UTF-8 ,保證能夠正確的解碼 linux 終端的輸出。

兩個 Python 字元串類型間可以用 encode / decode 方法轉換:

為什麼從 unicode 轉 str 是 encode,而反過來叫 decode?

因為 Python 認為 16 位的 unicode 才是字元的唯一內碼,而大家常用的字符集如 gb2312,gb18030/gbk,utf-8,以及 ascii 都是字元的二進位(位元組)編碼形式。把字元從 unicode 轉換成二進位編碼,當然是要 encode。

反過來,在 Python 中出現的 str 都是用字符集編碼的 ansi 字元串。Python 本身並不知道 str 的編碼,需要由開發者指定正確的字符集 decode。

(補充一句,其實 Python 是可以知道 str 編碼的。因為我們在代碼前面申明了 # -- coding: utf-8 --,這表明代碼中的 str 都是用 utf-8 編碼的,我不知道 Python 為什麼不這樣做。)

如果用錯誤的字符集來 encode/decode 會怎樣?

這就遇到了我在本文開頭貼出的異常:UnicodeEncodeError: "ascii" codec can"t encode characters in position 0-3: ordinal not in range(128)

現在我們知道了這是個字元串編碼異常。接下來, 為什麼 Python 這麼容易出現字元串編/解碼異常?

這要提到處理 Python 編碼時容易遇到的兩個陷阱。第一個是有關字元串連接的:

簡單的字元串連接也會出現解碼錯誤?

陷阱一:在進行同時包含 str 與 unicode 的運算時,Python 一律都把 str 轉換成 unicode 再運算,當然,運算結果也都是 unicode。

由於 Python 事先並不知道 str 的編碼,它只能使用 sys.getdefaultencoding() 編碼去 decode。在我的印象里,sys.getdefaultencoding() 的值總是 "ascii" ——顯然,如果需要轉換的 str 有中文,一定會出現錯誤。

除了字元串連接,% 運算的結果也是一樣的:

我不理解為什麼 sys.getdefaultencoding() 與環境變數 $LANG 全無關係。如果 Python 用 $LANG 設置 sys.getdefaultencoding() 的值,那麼至少開發者遇到 UnicodeDecodeError 的幾率會降低 50%。

另外,就像前面說的,我也懷疑為什麼 Python 在這裡不參考 # -- coding: utf-8 -- ,因為 Python 在運行前總是會檢查你的代碼,這保證了代碼里定義的 str 一定是 utf-8 。

對於這個問題,我的唯一建議是在代碼里的中文字元串前寫上 u。另外,在 Python 3 已經取消了 str,讓所有的字元串都是 unicode ——這也許是個正確的決定。

其實,sys.getdefaultencoding() 的值是可以用「後門」方式修改的,我不是特別推薦這個解決方案,但是還是貼一下,因為後面有用:

可以看到,問題魔術般的解決了。但是注意! sys.setdefaultencoding() 的效果是全局的,如果你的代碼由幾個不同編碼的 Python 文件組成,用這種方法只是按下了葫蘆浮起了瓢,讓問題變得複雜。

另一個陷阱是有關標準輸出的。

剛剛怎麼來著?我一直說要設置正確的 linux $LANG 環境變數。那麼,設置錯誤的 $LANG,比如 zh_CN.GBK 會怎樣?(避免終端的影響,請把 SecureCRT 也設置成相同的字符集。)

顯然會是亂碼,但是不是所有輸出都是亂碼。

這也是為什麼要設置 linux $LANG 環境變數與 SecureCRT 一致,否則這些字元會被 SecureCRT 再轉換一次,才會交給桌面的 Windows 系統用編碼 CP936 或者說 GBK 來顯示。

比如,用管道方式運行上面的 example4.py 代碼:

由於 ascii 字符集不能用來表示中文字元,這裡當然會編碼失敗。

怎麼解決這個問題? 不知道別人是怎麼搞定的,總之我用了一個醜陋的辦法:

這個方法仍然有個副作用:直接輸出中文 str 會失敗,因為 codecs 模塊的 writer 與 sys.stdout 的行為相反,它會把所有的 str 用 sys.getdefaultencoding() 的字符集轉換成 unicode 輸出。

顯然,sys.getdefaultencoding() 的值是 "ascii", 編碼失敗。

解決辦法就像 example3.py 里說的,你要麼給 str 加上 u 申明成 unicode,要麼通過「後門」去修改 sys.getdefaultencoding():

總而言之,在 Python 2 下進行中文輸入輸出是個危機四伏的事,特別是在你的代碼里混合使用 str 與 unicode 時。

有些模塊,例如 json,會直接返回 unicode 類型的字元串,讓你的 % 運算需要進行字元解碼而失敗。而有些會直接返回 str, 你需要知道它們的真實編碼,特別是在 print 的時候。

為了避免一些陷阱,上文中說過,最好的辦法就是在 Python 代碼里永遠使用 u 定義中文字元串。另外,如果你的代碼需要用管道 / 子進程方式運行,則需要用到 example6.py 里的技巧。

題圖:pexels,CC0 授權。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 編程派 的精彩文章:

12步輕鬆搞定Python裝飾器

TAG:編程派 |