python2.7 的中文編碼處理,解決UnicodeEncodeError: 'ascii' codec can't encode character 問題

                                    python2.7 的中文編碼處理

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

很快,我就遇到了異常:

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

爲了解決問題,我花時間去研究了一下 Python 的字符編碼處理。

1.引入

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

# -*- 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 方法轉換:

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

# 用 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 編碼時容易遇到的兩個陷阱。第一個是有關字符串連接的:

# -*- 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 有中文,一定會出現錯誤。

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

# 正確,所有的字符串都是 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() 的值是可以用“後門”方式修改的,我不是特別推薦這個解決方案,但是還是貼一下,因爲後面有用:

# -*- 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 也設置成相同的字符集。)

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

# -*- 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 環境變量保持一致:

# -*- 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。

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

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

# -*- 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 輸出。

# 這個是 str 的字符串  
s = '關關雎鳩'  
  
# 輸出 str 字符串, 異常  
print s   # UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128)  

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

解決辦法就是你要麼給 str 加上 u 申明成 unicode,要麼通過“後門”去修改 sys.getdefaultencoding():

# 使得 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 的時候。

2.python 自動解編碼機制導致報錯

2.1.stirng 和 unicode 對象合併

>>> s + u''
Traceback (most recent call last):
  File "<input>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
>>>

2.2.列表合併

>>> as_list = [u, s]
>>> ''.join(as_list)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)

2.3.格式化字符串

>>> '%s-%s'%(s,u)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
>>>

2.4.打印 unicode 對象

#test.py
# -*- coding: utf-8 -*-
u = u'中文'
print u
#outpt
Traceback (most recent call last):
  File "/Users/zhyq0826/workspace/zhyq0826/blog-code/p20161030_python_encoding/uni.py", line 3, in <module>
    print u
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)

2.5.輸出到文件

>>> f = open('text.txt','w')
>>> f.write(u)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
>>>

1,2,3 的例子中,python 自動用 ascii 把 string 解碼爲 unicode 對象然後再進行相應操作,所以都是 decode 錯誤, 4 和 5 python 自動用 ascii 把 unicode 對象編碼爲字符串然後輸出,所以都是 encode 錯誤。

只要涉及到 unicode 對象和 string 的轉換以及 unicode 對象輸出、輸入的地方可能都會觸發 python 自動進行解碼/編碼,比如寫入數據庫、寫入到文件、讀取 socket 等等。

到此,這兩個異常產生的真正原因了基本已經清楚了: unicode 對象需要編碼爲相應的 string(字符串)纔可以存儲、傳輸、打印,字符串需要解碼爲對應的 unicode 對象才能完成 unicode 對象的各種操作,lenfind 等。

string.decode('utf-8') --> unicode
unicode.encode('utf-8') --> string

3.如何避免這些的錯誤

1.理解編碼或解碼的轉換方向

無論何時發生編碼錯誤,首先要理解編碼方向,然後再針對性解決。

2.設置默認編碼爲 utf-8

在文件頭寫入

# -*- coding: utf-8 -*-

python 會查找: coding: name or coding=name,並設置文件編碼格式爲 name,此方式是告訴 python 默認編碼不再是 ascii ,而是要使用聲明的編碼格式。

3.輸入對象儘早解碼爲 unicode,輸出對象儘早編碼爲字節流

無論何時有字節流輸入,都需要儘早解碼爲 unicode 對象。任何時候想要把 unicode 對象寫入到文件、數據庫、socket 等外界程序,都需要進行編碼。

4.使用 codecs 模塊來處理輸入輸出 unicode 對象

codecs 模塊可以自動的完成解編碼的工作。

>>> import codecs
>>> f = codecs.open('text.txt', 'w', 'utf-8')
>>> f.write(u)
>>> f.close()

參考:https://www.cnblogs.com/huchong/p/9037142.html

https://github.com/scrapinghub/python-crfsuite/issues/96

http://in355hz.iteye.com/blog/1860787

http://sanyuesha.com/2016/11/06/python-string-unicode/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章