也談 Python 的中文編碼處理

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

 

很快,我就遇到了異常:linux

 

Python代碼  收藏代碼json

  1. UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-3: ordinal not in range(128)  編碼

 

爲了解決問題,我花時間去研究了一下 Python 的字符編碼處理。網上也有很多文章講 Python 的字符編碼,可是我看過一遍,以爲本身能夠講得更明白些。spa

 

下面先複述一下 Python 字符串的基礎,熟悉此內容的能夠跳過。操作系統

 

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

 

Python代碼  收藏代碼日誌

  1. # -*- coding: utf-8 -*-  code

  2. # file: example1.py  進程

  3. import string  

  4.   

  5. # 這個是 str 的字符串  

  6. s = '關關雎鳩'  

  7.   

  8. # 這個是 unicode 的字符串  

  9. u = u'關關雎鳩'  

  10.   

  11. print isinstance(s, str)      # True  

  12. print isinstance(u, unicode)  # True  

  13.   

  14. print s.__class__   # <type 'str'>  

  15. print u.__class__   # <type 'unicode'>  

 

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

 

爲了保證輸出不會在 linux 終端上顯示亂碼,須要設置好 linux 的環境變量:export.UTF-8

 

若是你和我同樣是使用 SecureCRT,請設置 Session Options/Terminal/Appearance/Character Encoding 爲 UTF-8 ,保證可以正確的解碼 linux 終端的輸出。

 

兩個 Python 字符串類型間能夠用 encode / decode 方法轉換:

 

Python代碼  收藏代碼

  1. # 從 str 轉換成 unicode  

  2. print s.decode('utf-8')   # 關關雎鳩  

  3.   

  4. # 從 unicode 轉換成 str  

  5. print u.encode('utf-8')   # 關關雎鳩  

 

爲何從 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 會怎樣?

 

Python代碼  收藏代碼

  1. # 用 ascii 編碼含中文的 unicode 字符串  

  2. u.encode('ascii')  # 錯誤,由於中文沒法用 ascii 字符集編碼  

  3.                    # UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-3: ordinal not in range(128)  

  4.   

  5. # 用 gbk 編碼含中文的 unicode 字符串  

  6. u.encode('gbk')  # 正確,由於 '關關雎鳩' 能夠用中文 gbk 字符集表示  

  7.                  # '\xb9\xd8\xb9\xd8\xf6\xc2\xf0\xaf'  

  8.                  # 直接 print 上面的 str 會顯示亂碼,修改環境變量爲 zh_CN.GBK 能夠看到結果是對的  

  9.   

  10. # 用 ascii 解碼 utf-8 字符串  

  11. s.decode('ascii')  # 錯誤,中文 utf-8 字符沒法用 ascii 解碼  

  12.                    # UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128)  

  13.   

  14. # 用 gbk 解碼 utf-8 字符串  

  15. s.decode('gbk')  # 不出錯,可是用 gbk 解碼 utf-8 字符流的結果,顯然只是亂碼  

  16.                  # u'\u934f\u51b2\u53e7\u95c6\u5ea8\u7b2d'  

 

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

 

如今咱們知道了這是個字符串編碼異常。接下來, 爲何 Python 這麼容易出現字符串編/解碼異常? 

 

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

 

Python代碼  收藏代碼

  1. # -*- coding: utf-8 -*-  

  2. # file: example2.py  

  3.   

  4. # 這個是 str 的字符串  

  5. s = '關關雎鳩'  

  6.   

  7. # 這個是 unicode 的字符串  

  8. u = u'關關雎鳩'  

  9.   

  10. s + u  # 失敗,UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128)  

 

簡單的字符串鏈接也會出現解碼錯誤?

 

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

 

因爲 Python 事先並不知道 str 的編碼,它只能使用 sys.getdefaultencoding() 編碼去 decode。在個人印象裏,sys.getdefaultencoding() 的值老是 'ascii' ——顯然,若是須要轉換的 str 有中文,必定會出現錯誤。

 

除了字符串鏈接,% 運算的結果也是同樣的:

 

Python代碼  收藏代碼

  1. # 正確,全部的字符串都是 str, 不須要 decode  

  2. "中文:%s" % s   # 中文:關關雎鳩  

  3.   

  4. # 失敗,至關於運行:"中文:%s".decode('ascii') % u  

  5. "中文:%s" % u  # UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128)  

  6.   

  7. # 正確,全部字符串都是 unicode, 不須要 decode  

  8. u"中文:%s" % u   # 中文:關關雎鳩  

  9.   

  10. # 失敗,至關於運行:u"中文:%s" % s.decode('ascii')  

  11. u"中文:%s" % s  # UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128)  

 

我不理解爲何 sys.getdefaultencoding() 與環境變量 $LANG 全無關係。若是 Python 用 $LANG 設置 sys.getdefaultencoding() 的值,那麼至少開發者遇到 UnicodeDecodeError 的概率會下降 50%。

 

另外,就像前面說的,我也懷疑爲何 Python 在這裏不參考 # -*- coding: utf-8 -*- ,由於 Python 在運行前老是會檢查你的代碼,這保證了代碼裏定義的 str 必定是 utf-8 。

 

對於這個問題,個人惟一建議是在代碼裏的中文字符串前寫上 u。另外,在 Python 3 已經取消了 str,讓全部的字符串都是 unicode ——這也許是個正確的決定。

 

其實,sys.getdefaultencoding() 的值是能夠用「後門」方式修改的,我不是特別推薦這個解決方案,可是仍是貼一下,由於後面有用:

 

Python代碼  收藏代碼

  1. # -*- coding: utf-8 -*-  

  2. # file: example3.py  

  3. import sys  

  4.   

  5. # 這個是 str 的字符串  

  6. s = '關關雎鳩'  

  7.   

  8. # 這個是 unicode 的字符串  

  9. u = u'關關雎鳩'  

  10.   

  11. # 使得 sys.getdefaultencoding() 的值爲 'utf-8'  

  12. reload(sys)                      # reload 才能調用 setdefaultencoding 方法  

  13. sys.setdefaultencoding('utf-8')  # 設置 'utf-8'  

  14.   

  15. # 沒問題  

  16. s + u  # u'\u5173\u5173\u96ce\u9e20\u5173\u5173\u96ce\u9e20'  

  17.   

  18. # 一樣沒問題  

  19. "中文:%s" % u   # u'\u4e2d\u6587\uff1a\u5173\u5173\u96ce\u9e20'  

  20.   

  21. # 仍是沒問題  

  22. u"中文:%s" % s  # u'\u4e2d\u6587\uff1a\u5173\u5173\u96ce\u9e20'  

 

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

 

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

 

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

 

顯然會是亂碼,可是不是全部輸出都是亂碼。

 

Python代碼  收藏代碼

  1. # -*- coding: utf-8 -*-  

  2. # file: example4.py  

  3. import string  

  4.   

  5. # 這個是 str 的字符串  

  6. s = '關關雎鳩'  

  7.   

  8. # 這個是 unicode 的字符串  

  9. u = u'關關雎鳩'  

  10.   

  11. # 輸出 str 字符串, 顯示是亂碼  

  12. print s   # 鍏衝叧闆庨笭  

  13.   

  14. # 輸出 unicode 字符串,顯示正確  

  15. print u  # 關關雎鳩  

 

爲何是 unicode 而不是 str 的字符顯示是正確的? 首先咱們須要瞭解 print。與全部語言同樣,這個 Python 命令其實是把字符打印到標準輸出流 —— sys.stdout。而 Python 在這裏變了個魔術,它會按照 sys.stdout.encoding 來給 unicode 編碼,而把 str 直接輸出,扔給操做系統去解決。

 

這也是爲何要設置 linux $LANG 環境變量與 SecureCRT 一致,不然這些字符會被 SecureCRT 再轉換一次,纔會交給桌面的 Windows 系統用編碼 CP936 或者說 GBK 來顯示。

 

一般狀況,sys.stdout.encoding 的值與 linux $LANG 環境變量保持一致:

 

Python代碼  收藏代碼

  1. # -*- coding: utf-8 -*-  

  2. # file: example5.py  

  3. import sys  

  4.   

  5. # 檢查標準輸出流的編碼  

  6. print sys.stdout.encoding  # 設置 $LANG = zh_CN.GBK,  輸出 GBK  

  7.                            # 設置 $LANG = en_US.UTF-8,輸出 UTF-8  

  8.   

  9. # 這個是 unicode 的字符串  

  10. u = u'關關雎鳩'  

  11.   

  12. # 輸出 unicode 字符串,顯示正確  

  13. print u  # 關關雎鳩  

 

可是,這裏有 陷阱二:一旦你的 Python 代碼是用管道 / 子進程方式運行,sys.stdout.encoding 就會失效,讓你從新遇到 UnicodeEncodeError。

 

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

 

Python代碼  收藏代碼

  1. python -u example5.py | more  

  2.   

  3. UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-3: ordinal not in range(128)  

  4. None  

 

能夠看到,第一:sys.stdout.encoding 的值變成了 None;第二:Python 在 print 時會嘗試用 ascii 去編碼 unicode.

 

因爲 ascii 字符集不能用來表示中文字符,這裏固然會編碼失敗。

 

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

 

Python代碼  收藏代碼

  1. # -*- coding: utf-8 -*-  

  2. # file: example6.py  

  3. import os  

  4. import sys  

  5. import codecs  

  6.   

  7. # 不管如何,請用 linux 系統的當前字符集輸出:  

  8. if sys.stdout.encoding is None:  

  9.     enc = os.environ['LANG'].split('.')[1]  

  10.     sys.stdout = codecs.getwriter(enc)(sys.stdout)  # 替換 sys.stdout  

  11.   

  12. # 這個是 unicode 的字符串  

  13. u = u'關關雎鳩'  

  14.   

  15. # 輸出 unicode 字符串,顯示正確  

  16. print u  # 關關雎鳩  

 

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

 

Python代碼  收藏代碼

  1. # 這個是 str 的字符串  

  2. s = '關關雎鳩'  

  3.   

  4. # 輸出 str 字符串, 異常  

  5. print s   # UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128)  

 

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

 

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

 

Python代碼  收藏代碼

  1. # 使得 sys.getdefaultencoding() 的值爲 'utf-8'  

  2. reload(sys)                      # reload 才能調用 setdefaultencoding 方法  

  3. sys.setdefaultencoding('utf-8')  # 設置 'utf-8'  

  4.   

  5. # 這個是 str 的字符串  

  6. s = '關關雎鳩'  

  7.   

  8. # 輸出 str 字符串, OK  

  9. print s   # 關關雎鳩  

 

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

 

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

 

爲了不一些陷阱,上文中說過,最好的辦法就是在 Python 代碼裏永遠使用 u 定義中文字符串。另外,若是你的代碼須要用管道 / 子進程方式運行,則須要用到 example6.py 裏的技巧。

 

(完)

相關文章
相關標籤/搜索