轉載自:http://in355hz.iteye.com/blog/1860787 最近業務中需要用 Python 寫一些腳本。儘管腳本的交互只是命令行 + 日誌輸出,但是為了讓界面友好些,我還是決定用中文輸出日誌信息。 很快,我就遇到了異常: Python代碼 UnicodeEncodeError ...
轉載自:http://in355hz.iteye.com/blog/1860787
最近業務中需要用 Python 寫一些腳本。儘管腳本的交互只是命令行 + 日誌輸出,但是為了讓界面友好些,我還是決定用中文輸出日誌信息。
很快,我就遇到了異常:
Python代碼
- UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-3: ordinal not in range(128)
為瞭解決問題,我花時間去研究了一下 Python 的字元編碼處理。網上也有不少文章講 Python 的字元編碼,但是我看過一遍,覺得自己可以講得更明白些。
下麵先覆述一下 Python 字元串的基礎,熟悉此內容的可以跳過。
對應 C/C++ 的 char 和 wchar_t, Python 也有兩種字元串類型,str 與 unicode:
Python代碼
- # -*- coding: utf-8 -*-
- # file: example1.py
- import string
- # 這個是 str 的字元串
- s = '關關雎鳩'
- # 這個是 unicode 的字元串
- u = u'關關雎鳩'
- print isinstance(s, str) # True
- print isinstance(u, unicode) # True
- print s.__class__ # <type 'str'>
- print u.__class__ # <type '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 方法轉換:
Python代碼
- # 從 str 轉換成 unicode
- print s.decode('utf-8') # 關關雎鳩
- # 從 unicode 轉換成 str
- 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代碼
- # 用 ascii 編碼含中文的 unicode 字元串
- u.encode('ascii') # 錯誤,因為中文無法用 ascii 字元集編碼
- # UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-3: ordinal not in range(128)
- # 用 gbk 編碼含中文的 unicode 字元串
- u.encode('gbk') # 正確,因為 '關關雎鳩' 可以用中文 gbk 字元集表示
- # '\xb9\xd8\xb9\xd8\xf6\xc2\xf0\xaf'
- # 直接 print 上面的 str 會顯示亂碼,修改環境變數為 zh_CN.GBK 可以看到結果是對的
- # 用 ascii 解碼 utf-8 字元串
- s.decode('ascii') # 錯誤,中文 utf-8 字元無法用 ascii 解碼
- # UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128)
- # 用 gbk 解碼 utf-8 字元串
- s.decode('gbk') # 不出錯,但是用 gbk 解碼 utf-8 字元流的結果,顯然只是亂碼
- # 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代碼
- # -*- coding: utf-8 -*-
- # file: example2.py
- # 這個是 str 的字元串
- s = '關關雎鳩'
- # 這個是 unicode 的字元串
- u = u'關關雎鳩'
- 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代碼
- # 正確,所有的字元串都是 str, 不需要 decode
- "中文:%s" % s # 中文:關關雎鳩
- # 失敗,相當於運行:"中文:%s".decode('ascii') % u
- "中文:%s" % u # UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128)
- # 正確,所有字元串都是 unicode, 不需要 decode
- u"中文:%s" % u # 中文:關關雎鳩
- # 失敗,相當於運行:u"中文:%s" % s.decode('ascii')
- 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代碼
- # -*- coding: utf-8 -*-
- # file: example3.py
- import sys
- # 這個是 str 的字元串
- s = '關關雎鳩'
- # 這個是 unicode 的字元串
- u = u'關關雎鳩'
- # 使得 sys.getdefaultencoding() 的值為 'utf-8'
- reload(sys) # reload 才能調用 setdefaultencoding 方法
- sys.setdefaultencoding('utf-8') # 設置 'utf-8'
- # 沒問題
- s + u # u'\u5173\u5173\u96ce\u9e20\u5173\u5173\u96ce\u9e20'
- # 同樣沒問題
- "中文:%s" % u # u'\u4e2d\u6587\uff1a\u5173\u5173\u96ce\u9e20'
- # 還是沒問題
- u"中文:%s" % s # u'\u4e2d\u6587\uff1a\u5173\u5173\u96ce\u9e20'
可以看到,問題魔術般的解決了。但是註意! sys.setdefaultencoding() 的效果是全局的,如果你的代碼由幾個不同編碼的 Python 文件組成,用這種方法只是按下了葫蘆浮起了瓢,讓問題變得複雜。
另一個陷阱是有關標準輸出的。
剛剛怎麼來著?我一直說要設置正確的 linux $LANG 環境變數。那麼,設置錯誤的 $LANG,比如 zh_CN.GBK 會怎樣?(避免終端的影響,請把 SecureCRT 也設置成相同的字元集。)
顯然會是亂碼,但是不是所有輸出都是亂碼。
Python代碼
- # -*- coding: utf-8 -*-
- # file: example4.py
- import string
- # 這個是 str 的字元串
- s = '關關雎鳩'
- # 這個是 unicode 的字元串
- u = u'關關雎鳩'
- # 輸出 str 字元串, 顯示是亂碼
- print s # 鍏沖叧闆庨笭
- # 輸出 unicode 字元串,顯示正確
- 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代碼
- # -*- coding: utf-8 -*-
- # file: example5.py
- import sys
- # 檢查標準輸出流的編碼
- print sys.stdout.encoding # 設置 $LANG = zh_CN.GBK, 輸出 GBK
- # 設置 $LANG = en_US.UTF-8,輸出 UTF-8
- # 這個是 unicode 的字元串
- u = u'關關雎鳩'
- # 輸出 unicode 字元串,顯示正確
- print u # 關關雎鳩
但是,這裡有 陷阱二:一旦你的 Python 代碼是用管道 / 子進程方式運行,sys.stdout.encoding 就會失效,讓你重新遇到 UnicodeEncodeError。
比如,用管道方式運行上面的 example4.py 代碼:
Python代碼
- python -u example5.py | more
- UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-3: ordinal not in range(128)
- None
可以看到,第一:sys.stdout.encoding 的值變成了 None;第二:Python 在 print 時會嘗試用 ascii 去編碼 unicode.
由於 ascii 字元集不能用來表示中文字元,這裡當然會編碼失敗。
怎麼解決這個問題? 不知道別人是怎麼搞定的,總之我用了一個醜陋的辦法:
Python代碼
- # -*- coding: utf-8 -*-
- # file: example6.py
- import os
- import sys
- import codecs
- # 無論如何,請用 linux 系統的當前字元集輸出:
- if sys.stdout.encoding is None:
- enc = os.environ['LANG'].split('.')[1]
- sys.stdout = codecs.getwriter(enc)(sys.stdout) # 替換 sys.stdout
- # 這個是 unicode 的字元串
- u = u'關關雎鳩'
- # 輸出 unicode 字元串,顯示正確
- print u # 關關雎鳩
這個方法仍然有個副作用:直接輸出中文 str 會失敗,因為 codecs 模塊的 writer 與 sys.stdout 的行為相反,它會把所有的 str 用 sys.getdefaultencoding() 的字元集轉換成 unicode 輸出。
Python代碼
- # 這個是 str 的字元串
- s = '關關雎鳩'
- # 輸出 str 字元串, 異常
- 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代碼
- # 使得 sys.getdefaultencoding() 的值為 'utf-8'
- reload(sys) # reload 才能調用 setdefaultencoding 方法
- sys.setdefaultencoding('utf-8') # 設置 'utf-8'
- # 這個是 str 的字元串
- s = '關關雎鳩'
- # 輸出 str 字元串, OK
- print s # 關關雎鳩
總而言之,在 Python 2 下進行中文輸入輸出是個危機四伏的事,特別是在你的代碼里混合使用 str 與 unicode 時。
有些模塊,例如 json,會直接返回 unicode 類型的字元串,讓你的 % 運算需要進行字元解碼而失敗。而有些會直接返回 str, 你需要知道它們的真實編碼,特別是在 print 的時候。
為了避免一些陷阱,上文中說過,最好的辦法就是在 Python 代碼里永遠使用 u 定義中文字元串。另外,如果你的代碼需要用管道 / 子進程方式運行,則需要用到 example6.py 里的技巧。
(完)