python怎么判断是不是汉字危机_谈 Python 的中文编码处理
最近業(yè)務(wù)中需要用 Python 寫一些腳本。盡管腳本的交互只是命令行 + 日志輸出,但是為了讓界面友好些,我還是決定用中文輸出日志信息。
很快,我就遇到了異常:
Python代碼??
UnicodeEncodeError:?'ascii'?codec?can't?encode?characters?in?position?0-3:?ordinal?not?in?range(128)
為了解決問題,我花時間去研究了一下 Python 的字符編碼處理。網(wǎng)上也有不少文章講 Python 的字符編碼,但是我看過一遍,覺得自己可以講得更明白些。
下面先復(fù)述一下 Python 字符串的基礎(chǔ),熟悉此內(nèi)容的可以跳過。
對應(yīng) C/C++ 的 char 和 wchar_t, Python 也有兩種字符串類型,str 與 unicode:
Python代碼??
#?-*-?coding:?utf-8?-*-
#?file:?example1.py
import?string
#?這個是?str?的字符串
s?=?'關(guān)關(guān)雎鳩'
#?這個是?unicode?的字符串
u?=?u'關(guān)關(guān)雎鳩'
print?isinstance(s,?str)??????#?True
print?isinstance(u,?unicode)??#?True
print?s.__class__???#?
print?u.__class__???#?
前面的申明:# -*- coding: utf-8 -*-?表明,上面的 Python 代碼由 utf-8 編碼。
為了保證輸出不會在 linux 終端上顯示亂碼,需要設(shè)置好 linux 的環(huán)境變量:export LANG=en_US.UTF-8
如果你和我一樣是使用 SecureCRT,請設(shè)置 Session Options/Terminal/Appearance/Character Encoding 為 UTF-8 ,保證能夠正確的解碼 linux 終端的輸出。
兩個 Python 字符串類型間可以用 encode / decode 方法轉(zhuǎn)換:
Python代碼??
#?從?str?轉(zhuǎn)換成?unicode
print?s.decode('utf-8')???#?關(guān)關(guān)雎鳩
#?從?unicode?轉(zhuǎn)換成?str
print?u.encode('utf-8')???#?關(guān)關(guān)雎鳩
為什么從 unicode 轉(zhuǎn) str 是 encode,而反過來叫 decode?
因為 Python 認(rèn)為 16 位的 unicode 才是字符的唯一內(nèi)碼,而大家常用的字符集如 gb2312,gb18030/gbk,utf-8,以及 ascii 都是字符的二進制(字節(jié))編碼形式。把字符從 unicode 轉(zhuǎn)換成二進制編碼,當(dāng)然是要 encode。
反過來,在 Python 中出現(xiàn)的 str 都是用字符集編碼的 ansi 字符串。Python 本身并不知道 str 的編碼,需要由開發(fā)者指定正確的字符集 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')??#?正確,因為?'關(guān)關(guān)雎鳩'?可以用中文?gbk?字符集表示
#?'\xb9\xd8\xb9\xd8\xf6\xc2\xf0\xaf'
#?直接?print?上面的?str?會顯示亂碼,修改環(huán)境變量為?zh_CN.GBK?可以看到結(jié)果是對的
#?用?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?字符流的結(jié)果,顯然只是亂碼
#?u'\u934f\u51b2\u53e7\u95c6\u5ea8\u7b2d'
這就遇到了我在本文開頭貼出的異常:UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-3: ordinal not in range(128)
現(xiàn)在我們知道了這是個字符串編碼異常。接下來, 為什么 Python 這么容易出現(xiàn)字符串編/解碼異常?
這要提到處理 Python 編碼時容易遇到的兩個陷阱。第一個是有關(guān)字符串連接的:
Python代碼??
#?-*-?coding:?utf-8?-*-
#?file:?example2.py
#?這個是?str?的字符串
s?=?'關(guān)關(guān)雎鳩'
#?這個是?unicode?的字符串
u?=?u'關(guān)關(guān)雎鳩'
s?+?u??#?失敗,UnicodeDecodeError:?'ascii'?codec?can't?decode?byte?0xe5?in?position?0:?ordinal?not?in?range(128)
簡單的字符串連接也會出現(xiàn)解碼錯誤?
陷阱一:在進行同時包含 str 與 unicode 的運算時,Python 一律都把 str 轉(zhuǎn)換成 unicode 再運算,當(dāng)然,運算結(jié)果也都是 unicode。
由于 Python 事先并不知道 str 的編碼,它只能使用 sys.getdefaultencoding() 編碼去 decode。在我的印象里,sys.getdefaultencoding() 的值總是 'ascii' ——顯然,如果需要轉(zhuǎn)換的 str 有中文,一定會出現(xiàn)錯誤。
除了字符串連接,% 運算的結(jié)果也是一樣的:
Python代碼??
#?正確,所有的字符串都是?str,?不需要?decode
"中文:%s"?%?s???#?中文:關(guān)關(guān)雎鳩
#?失敗,相當(dāng)于運行:"中文:%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???#?中文:關(guān)關(guān)雎鳩
#?失敗,相當(dāng)于運行: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() 與環(huán)境變量 $LANG 全無關(guān)系。如果 Python 用 $LANG 設(shè)置 sys.getdefaultencoding() 的值,那么至少開發(fā)者遇到 UnicodeDecodeError 的幾率會降低 50%。
另外,就像前面說的,我也懷疑為什么 Python 在這里不參考?# -*- coding: utf-8 -*-?,因為 Python 在運行前總是會檢查你的代碼,這保證了代碼里定義的 str 一定是 utf-8 。
對于這個問題,我的唯一建議是在代碼里的中文字符串前寫上 u。另外,在 Python 3 已經(jīng)取消了 str,讓所有的字符串都是 unicode ——這也許是個正確的決定。
其實,sys.getdefaultencoding() 的值是可以用“后門”方式修改的,我不是特別推薦這個解決方案,但是還是貼一下,因為后面有用:
Python代碼??
#?-*-?coding:?utf-8?-*-
#?file:?example3.py
import?sys
#?這個是?str?的字符串
s?=?'關(guān)關(guān)雎鳩'
#?這個是?unicode?的字符串
u?=?u'關(guān)關(guān)雎鳩'
#?使得?sys.getdefaultencoding()?的值為?'utf-8'
reload(sys)??????????????????????#?reload?才能調(diào)用?setdefaultencoding?方法
sys.setdefaultencoding('utf-8')??#?設(shè)置?'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'
可以看到,問題魔術(shù)般的解決了。但是注意! sys.setdefaultencoding() 的效果是全局的,如果你的代碼由幾個不同編碼的 Python 文件組成,用這種方法只是按下了葫蘆浮起了瓢,讓問題變得復(fù)雜。
另一個陷阱是有關(guān)標(biāo)準(zhǔn)輸出的。
剛剛怎么來著?我一直說要設(shè)置正確的 linux $LANG 環(huán)境變量。那么,設(shè)置錯誤的 $LANG,比如 zh_CN.GBK 會怎樣?(避免終端的影響,請把 SecureCRT 也設(shè)置成相同的字符集。)
顯然會是亂碼,但是不是所有輸出都是亂碼。
Python代碼??
#?-*-?coding:?utf-8?-*-
#?file:?example4.py
import?string
#?這個是?str?的字符串
s?=?'關(guān)關(guān)雎鳩'
#?這個是?unicode?的字符串
u?=?u'關(guān)關(guān)雎鳩'
#?輸出?str?字符串,?顯示是亂碼
print?s???#?鍏沖叧闆庨笭
#?輸出?unicode?字符串,顯示正確
print?u??#?關(guān)關(guān)雎鳩
為什么是 unicode 而不是 str 的字符顯示是正確的? 首先我們需要了解 print。與所有語言一樣,這個 Python 命令實際上是把字符打印到標(biāo)準(zhǔn)輸出流 —— sys.stdout。而 Python 在這里變了個魔術(shù),它會按照 sys.stdout.encoding 來給 unicode 編碼,而把 str 直接輸出,扔給操作系統(tǒng)去解決。
這也是為什么要設(shè)置 linux $LANG 環(huán)境變量與 SecureCRT 一致,否則這些字符會被 SecureCRT 再轉(zhuǎn)換一次,才會交給桌面的 Windows 系統(tǒng)用編碼 CP936 或者說 GBK 來顯示。
通常情況,sys.stdout.encoding 的值與 linux $LANG 環(huán)境變量保持一致:
Python代碼??
#?-*-?coding:?utf-8?-*-
#?file:?example5.py
import?sys
#?檢查標(biāo)準(zhǔn)輸出流的編碼
print?sys.stdout.encoding??#?設(shè)置?$LANG?=?zh_CN.GBK,??輸出?GBK
#?設(shè)置?$LANG?=?en_US.UTF-8,輸出?UTF-8
#?這個是?unicode?的字符串
u?=?u'關(guān)關(guān)雎鳩'
#?輸出?unicode?字符串,顯示正確
print?u??#?關(guān)關(guān)雎鳩
但是,這里有?陷阱二:一旦你的 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 字符集不能用來表示中文字符,這里當(dāng)然會編碼失敗。
怎么解決這個問題? 不知道別人是怎么搞定的,總之我用了一個丑陋的辦法:
Python代碼??
#?-*-?coding:?utf-8?-*-
#?file:?example6.py
import?os
import?sys
import?codecs
#?無論如何,請用?linux?系統(tǒng)的當(dāng)前字符集輸出:
if?sys.stdout.encoding?is?None:
enc?=?os.environ['LANG'].split('.')[1]
sys.stdout?=?codecs.getwriter(enc)(sys.stdout)??#?替換?sys.stdout
#?這個是?unicode?的字符串
u?=?u'關(guān)關(guān)雎鳩'
#?輸出?unicode?字符串,顯示正確
print?u??#?關(guān)關(guān)雎鳩
這個方法仍然有個副作用:直接輸出中文 str 會失敗,因為 codecs 模塊的 writer 與 sys.stdout 的行為相反,它會把所有的 str 用 sys.getdefaultencoding() 的字符集轉(zhuǎn)換成 unicode 輸出。
Python代碼??
#?這個是?str?的字符串
s?=?'關(guān)關(guān)雎鳩'
#?輸出?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?才能調(diào)用?setdefaultencoding?方法
sys.setdefaultencoding('utf-8')??#?設(shè)置?'utf-8'
#?這個是?str?的字符串
s?=?'關(guān)關(guān)雎鳩'
#?輸出?str?字符串,?OK
print?s???#?關(guān)關(guān)雎鳩
總而言之,在 Python 2 下進行中文輸入輸出是個危機四伏的事,特別是在你的代碼里混合使用 str 與 unicode 時。
有些模塊,例如 json,會直接返回 unicode 類型的字符串,讓你的 % 運算需要進行字符解碼而失敗。而有些會直接返回 str, 你需要知道它們的真實編碼,特別是在 print 的時候。
為了避免一些陷阱,上文中說過,最好的辦法就是在 Python 代碼里永遠(yuǎn)使用 u 定義中文字符串。另外,如果你的代碼需要用管道 / 子進程方式運行,則需要用到 example6.py 里的技巧。
(完)
與50位技術(shù)專家面對面20年技術(shù)見證,附贈技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的python怎么判断是不是汉字危机_谈 Python 的中文编码处理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python会议室系统预定_python
- 下一篇: shutil python_shutil