起因
眾所周知,Python 2 中的 UnicodeEncodeError 與 UnicodeDecodeError 是比較棘手的問題,有時候遇到這類問題的發生,總是一頭霧水,感覺莫名其妙。甚至,《Fluent Python》的作者還提出了所謂“三明治模型”的東西來幫助解決此類問題(其實大可不必如此麻煩,後文有述)。
今天線上上遇到一個與此有關的小問題,感覺很有趣,水文一篇記錄之。
Bug 轉到我這裡時,看到現象自然是UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
這類莫名其妙的提示。然後翻 log,迅速找到對應的程式碼行,大概類似下面這種:
thrift_obj = ThriftKeyValue(key=str(xx_obj.name)) # 出錯行, xx_obj.name 是一個 str
複製程式碼
一開始,看見str(xx_obj.name)
,也不知道是手誤,還是故意為之,反正是學不會這種操作(應該每個專案裡面,或多或少都有這樣的神奇程式碼吧)。
分析
看異常的字面意思,大致就是:有某個串,正在被 ASCII 編碼器編碼,但是顯然該串超出了 ASCII 編碼器所規定的範圍,於是出錯。於是推測:
- 哪裡應該有個什麼Unicode串(什麼串無所謂,反正只要超出 ASCII 的範圍就行),這裡應該是
xx_obj.name
。 - 某處正在發生
編碼動作
,而且是偷偷地在搞(最煩這種隱式轉換了,Python 2 中很多),從程式碼看不出在哪裡。
左看右看,應該是 str() 這個內建函式,於是簡單地試了一下如下程式碼:
In [5]: u = u'中國'
In [6]: str(u)
---------------------------------------------------------------------------
UnicodeEncodeError Traceback (most recent call last)
<ipython-input-6-b3b94fb7b5a0> in <module>()
----> 1 str(u)
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
In [7]: b = u.encode('utf-8')
In [8]: str(b)
Out[8]: '\xe4\xb8\xad\xe5\x9b\xbd'
複製程式碼
果然如此。查閱文件一看,沒啥有價值的資訊,描述太模糊了:
class str(object='')
Return a string containing a nicely printable representation of an object. For strings, this returns the string itself. The difference with repr(object) is that str(object) does not always attempt to return a string that is acceptable to eval(); its goal is to return a printable string. If no argument is given, returns the empty string, ''.
For more information on strings see Sequence Types — str, unicode, list, tuple, bytearray, buffer, xrange which describes sequence functionality (strings are sequences), and also the string-specific methods described in the String Methods section. To output formatted strings use template strings or the % operator described in the String Formatting Operations section. In addition see the String Services section. See also unicode().
複製程式碼
我們的程式碼裡面(Python 2),每個 py 檔案都有這麼一行:
from __future__ import unicode_literals, absolute_import
複製程式碼
所以我推測 xx_obj.name 是要給 unicode 串,打 log 一看,果然如此。
解決
至此,要麼將 xx_obj.name 轉化成 str() 能認識的東西,在這裡至少不能是 unicode,應該是 bytes。不過我沒有這麼做,太醜陋了,二是改成這樣:
thrift_obj = ThriftKeyValue(key=xx_obj.name) # 這裡沒必要呼叫 str() ,估計前面能跑正常,是因為 name 恰好總是 ASCII 字元
複製程式碼
Bug 修復,其他功能也表現正常。
總結
前文講到,Python 2 中有較多這種隱式轉換,而且也沒啥文件說明,特別是加上 Windows環境和 print 操作時,報錯資訊更是看得人不明所以。《Fluent Python》中有講到所謂“三明治模型”來解決這一問題,還是蠻有啟發的。
不過,我一般遵循的原則是:只用 Unicode,讓任何地方都是 Unicode。方式如下:
- 所有 py 檔案必須有如下檔案頭:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
from __future__ import unicode_literals, absolute_import
複製程式碼
- 接到外界的位元組串(從網路,從檔案等),先轉成 Unicode,不過抽取成函式更好,免得重複編碼:
API 的起名優點冗餘,主要是為了做到 “見名知義”
class UnicodeUtils(object):
@classmethod
def get_unicode_str(cls, bytes_str, try_decoders=('utf-8', 'gbk', 'utf-16')):
"""轉換成字串(一般是Unicode)"""
if not bytes_str:
return u''
if isinstance(bytes_str, (unicode,)):
return bytes_str
for decoder in try_decoders:
try:
unicode_str = bytes_str.decode(decoder)
except UnicodeDecodeError:
pass
else:
return unicode_str
raise DecodeBytesFailedException('decode bytes failed. tried decoders: %s' % list(try_decoders))
@classmethod
def encode_to_bytes(cls, unicode_str, encoder='utf-8'):
"""轉換成位元組串"""
if unicode_str is None:
return b''
if isinstance(unicode_str, unicode):
return unicode_str.encode(encoding=encoder)
else:
u = cls.get_unicode(unicode_str)
return u.encode(encoding=encoder)
複製程式碼
- 送到外界的東西,全部轉成 UTF-8 編碼的位元組串,見上面程式碼