【變量】python中的字符集和編碼

字符集和編碼簡介

在編程中常常可以見到各種字符集和編碼,包括ASCII,MBCS,Unicode等字符集。確切的說,其實字符集和編碼是兩個不同的概念,只是有些地方有重合罷了。對于ASCII,MBCS等字符集,基本上一個字符集方案只采用一種編碼方案,而對于Unicode,字符集和編碼方案是明確區(qū)分的。

1 ASCII

其中ASCII標(biāo)準(zhǔn)本身就規(guī)定了字符和字符編碼方式,采用單字節(jié)編碼,總共可以編碼128個字符,如空格的編碼是32,小寫字母a是97,所以ASCII既是字符集又是編碼方案。
計算機(jī)世界里一開始只有英文,而單字節(jié)可以表示256個不同的字符,可以表示所有的英文字符和許多的控制符號。不過ASCII只用到了其中的一半。

2 MBCS

對于英文來說,128個符號編碼已經(jīng)夠用了,然而對于其它語言比如中文,顯然就不夠了。后來每個語言就制定了一套自己的編碼,由于單字節(jié)能表示的字符太少,而且同時也需要與ASCII編碼保持兼容,所以這些編碼紛紛使用了多字節(jié)來表示字符,如GBxxx、BIGxxx等等,他們的規(guī)則是,如果第一個字節(jié)是\x80以下,則仍然表示ASCII字符;而如果是\x80以上,則跟下一個字節(jié)一起(共兩個字節(jié))表示一個字符。因此就出現(xiàn)了多字節(jié)字符集MBCS(Multi-Byte Character Set)。如GB2312,GBK,GB18030,BIG5等編碼都屬于MBCS。由于MBCS大都使用2個字節(jié)編碼,所以有時候也叫作DBCS(Double-Byte Character Set)。我們在Linux系統(tǒng)中看到含有中文的文件編碼常常是CP936,那這個其實就是GBK編碼了,這個名字的由來是因為IBM曾經(jīng)發(fā)明了一個Code Page的概念,把這些多字節(jié)編碼收入其中,GBK編碼正好位于936頁,所以就簡稱CP936了。
GB2312是中國規(guī)定的漢字編碼,也可以說是簡體中文的字符集編碼。
GBK是GB2312的擴(kuò)展,除了兼容GB2312外,它還能顯示繁體中文,還有日文的假名。

3 Unicode

而后大家覺得各種編碼太多不方便,不如所有語言字符都使用一套字符集來表示,于是就出現(xiàn)了Unicode。Unicode/UCS(Unicode Character Set)標(biāo)準(zhǔn)只是一個字符集標(biāo)準(zhǔn),但是它并沒有規(guī)定字符的存儲和傳輸方式。Unicode是一種字符集而不是具體的編碼,它主要有3種編碼方式:最初Unicode標(biāo)準(zhǔn)使用2個字節(jié)表示一個字符,編碼方案是UTF-16。還有使用4個字節(jié)表示一個字符的編碼方案UTF-32。而后來使用英文字符的國家覺得不好,原理一個字符存儲的現(xiàn)在變成了2個字符,空間增大了一倍,由此UTF-8編碼。UTF-8編碼中,英文占一個字節(jié),中文占3個字節(jié)。
如上面所提到的,Unicode字符集采用UTF-8,UTF-16等方式進(jìn)行編碼存儲。那么這樣的話,計算機(jī)如何知道文件采用哪種方式編碼呢?Unicode規(guī)范中又定義,在每個文件最前面加入一個表示編碼順序的字符BOM(Byte Order Mark)。比如石鍋拌飯中的‘石’的UTF-16編碼是77F3,采用UTF-16方式存儲使用2個字節(jié),一個字節(jié)是77,一個字節(jié)是F3.存儲的時候如果77在前面,F(xiàn)3在后面,則稱為big endian方式。反之,則是Little endian方式。
你可能聽說過UTF-8不需要BOM,這種說法是不對的,只是絕大多數(shù)編輯器在沒有BOM時都是以UTF-8作為默認(rèn)編碼讀取。即使是保存時默認(rèn)使用ANSI(MBCS)的記事本,在讀取文件時也是先使用UTF-8測試編碼,如果可以成功解碼,則使用UTF-8解碼。記事本這個別扭的做法造成了一個BUG:如果你新建文本文件并輸入"姹塧"然后使用ANSI(MBCS)保存,再打開就會變成"漢a",你不妨試試 :)。
現(xiàn)在,捋一捋ASCII編碼和Unicode編碼的區(qū)別:ASCII編碼是1個字節(jié),而Unicode編碼通常是2個字節(jié)。

字母A用ASCII編碼是十進(jìn)制的65,二進(jìn)制的01000001;
字符0用ASCII編碼是十進(jìn)制的48,二進(jìn)制的00110000,注意字符'0'和整數(shù)0是不同的;
漢字'中'已經(jīng)超出了ASCII編碼的范圍,用Unicode編碼是十進(jìn)制的20013,二進(jìn)制的01001110 00101101;
你可以猜測,如果把ASCII編碼的A用Unicode編碼,只需要在前面補(bǔ)0就可以,因此,A的Unicode編碼是00000000 01000001。

在計算機(jī)內(nèi)存中,統(tǒng)一使用Unicode編碼,當(dāng)需要保存到硬盤或者需要傳輸?shù)臅r候,就轉(zhuǎn)換為UTF-8編碼。
用記事本編輯的時候,從文件讀取的UTF-8字符被轉(zhuǎn)換為Unicode字符到內(nèi)存里,編輯完成后,保存的時候再把Unicode轉(zhuǎn)換為UTF-8保存到文件:


Paste_Image.png

瀏覽網(wǎng)頁的時候,服務(wù)器會把動態(tài)生成的Unicode內(nèi)容轉(zhuǎn)換為UTF-8再傳輸?shù)綖g覽器:


Paste_Image.png

所以你看到很多網(wǎng)頁的源碼上會有類似<meta charset="UTF-8"/>的信息,表示該網(wǎng)頁正是用的UTF-8編碼。

4 ANSI

此外,還有一種不得不提的是ANSI,ANSI在windows系統(tǒng)中極為常見,其實ANSI是Windows code pages,這個模式根據(jù)當(dāng)前的locale選定具體編碼,如果系統(tǒng)locale是簡體中文則采用GBK編碼,繁體中文為BIG5編碼,日文則是JIS編碼。
此外windows中喜歡把BOM_UTF16_LE編碼稱作Unicode, 把BOM_UTF8稱作UTF-8。也有人說UTF-8不需要BOM來標(biāo)示,其實是不對的,這是因為編輯器一般默認(rèn)使用UTF-8來測試字符編碼而已,如果可以成功解碼,就用UTF-8進(jìn)行解碼。即便最開始采用的是ANSI保存的,打開文件時還是最先使用UTF-8來解碼。比如你用windows的記事本程序新建一個文件,寫入“姹塧”并用ANSI編碼保存,再次打開文件,會發(fā)現(xiàn)“姹塧”會變成“漢a”。

1 Python 編碼基礎(chǔ)

1.1 str和unicode

python中有兩種數(shù)據(jù)模型來支持字符串這種數(shù)據(jù)類型,str和unicode,它們的基類都是basestring。比如s='中文'就是str類型的字符串,而u=u'中文'就是一個unicode類型的字符串。unicode是由str類型的字符串解碼后得到,unicode也可以編碼成str類型。即

str --> decode --> unicode
unicode --> encode --> str

關(guān)于encode和decode,

s.encode不妨理解為s.encodeTo().
s.decode不妨理解為s.decodeFrom().

encode可以指定error handler,以防腳本在執(zhí)行的時候因為編碼問題而中斷。

s.encode('gb18030', 'ignore')
s.encode('gb18030', 'replace')

嚴(yán)格來說,str也許應(yīng)該叫做字節(jié)串,它是unicode經(jīng)過編碼后的字節(jié)組成的序列,因為對于UTF-8編碼的str類型‘中文’,使用len()函數(shù)得到的結(jié)果是6,因為UTF-8編碼的str類型‘中文’實際是'\xe4\xb8\xad\xe6\x96\x87'。
unicode才是真正意義上的字符串,對字節(jié)串str使用正確的字符編碼進(jìn)行解碼后獲得。而對于unicode類型u'中文'(實際是u'\u4e2d\u6587'),使用len()函數(shù)得到結(jié)果是2。

1.2 頭部編碼聲明

在python源代碼文件中如果有用到非ascii字符,比如中文,那么需要在源碼文件頭部聲明源代碼字符編碼,格式如下:

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

這個格式看起來比較復(fù)雜,其實python只檢查#,coding,編碼等字符串,可以簡寫成#coding:utf-8。

2 Python2.x常見編碼問題

2.1 頭部編碼聲明和文件編碼問題

文件頭部編碼聲明決定了python解釋器解析源碼中的str的編碼選擇方式,比如頭部聲明的是utf-8編碼,則代碼中s='中文',python就會按照utf-8編碼格式來解析,通過repr(s)可以看到字符編碼是"\xe4\xb8\xad\xe6\x96\x87",如果頭部聲明的編碼是gbk編碼,則python會對s采用gbk編碼解析,結(jié)果是"\xd6\xd0\xce\xc4"。
需要注意的是,文件本身的編碼要跟文件頭部聲明編碼一致,不然就會出現(xiàn)問題。文件本身的編碼在Linux下面可以在vim下用命令set fenc來查看。如果文件本身編碼是gbk,而源碼文件頭部聲明的編碼是utf-8,這樣如果源碼中有中文就會有問題了,因為本身中文str存儲是按照gbk編碼來的,而python在解析str的時候又以為是utf-8編碼,這樣就會報SyntaxError: (unicode error) 'utf-8' codec can't decode byte錯誤。

2.2 默認(rèn)編碼問題

下面看個python默認(rèn)編碼導(dǎo)致的問題:

#-*-coding: utf-8 -*-
import sys
print sys.getdefaultencoding()
u = u"中文"
print 'repr(u) is', repr(u)

s = "中文"
print 'repr(s) is', repr(s)

u2 = s.decode("utf-8")
print 'u2 is', repr(u2)

#s2 = u.decode("utf-8") #編碼錯誤
#u2 = s.encode("utf-8") #編碼錯誤

ascii
repr(u) is u'\u4e2d\u6587'
repr(s) is '\xe4\xb8\xad\xe6\x96\x87'
u2 is u'\u4e2d\u6587'

Traceback (most recent call last):
  File "C:/Users/kaicz/Desktop/bianma.py", line 13, in <module>
    s2 = u.decode("utf-8")
  File "C:\Python27\lib\encodings\utf_8.py", line 16, in decode
    return codecs.utf_8_decode(input, errors, True)
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)

Traceback (most recent call last):
  File "C:/Users/kaicz/Desktop/bianma.py", line 14, in <module>
    u2 = s.encode("utf-8")
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)

注意實例中注釋掉的2行代碼,對于unicode最好不要直接調(diào)用decode,str最好不要直接調(diào)用encode方法。因為如果是直接調(diào)用,則相當(dāng)于u.encode(default_encoding).decode('utf-8'),default_encoding是python的unicode實現(xiàn)中用的默認(rèn)編碼,即sys.getdefaultencoding()得到的編碼,如果你沒有設(shè)置過,那么默認(rèn)編碼就是ascii,如果你的unicode本身超出了ascii編碼范圍就會報錯。同理,如果對str直接調(diào)用encode方法,那么默認(rèn)會先對str進(jìn)行解碼,即s.decode(default_encoding).encode('utf-8'),如果str本身是中文,而default_encoding是ascii的話,解碼就會出錯,從而導(dǎo)致上面這兩行會分別報UnicodeEncodeError: 'ascii' codec can't encode characters in position...錯誤和UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position...錯誤。
上面例子中注釋掉的兩行代碼如果執(zhí)行就會報錯,當(dāng)然,如果本身str或者unicode都在ascii編碼范圍,就沒有問題。比如s='abc'; s.encode('utf-8')就不會有問題,語句執(zhí)行后會返回一個跟s的id不同的str。
那如果要解決實例1中的問題,有兩種方法,其一是明確指定編碼,如下所示,

#coding: utf-8
u = u"中文"
print repr(u) # u'\u4e2d\u6587'

s = "中文"
print repr(s) # '\xe4\xb8\xad\xe6\x96\x87'

u2 = s.decode("utf-8")
print repr(u2) # u'\u4e2d\u6587'

s2 = u.encode("utf-8").decode("utf-8")  # OK                                    
u2 = s.decode("utf8").encode("utf-8")   # OK

第二種方法就是更改python的默認(rèn)編碼為文件編碼格式,如下所示(這里要reload sys模塊,是因為python初始化后刪除了setdefaultencoding方法):

#coding:utf-8                                                                   

import sys 
reload(sys)
sys.setdefaultencoding("utf-8") #更改默認(rèn)編碼為utf-8

u = u"中文"
print repr(u) # u'\u4e2d\u6587'

s = "中文"
print repr(s) # '\xe4\xb8\xad\xe6\x96\x87'

u2 = s.decode("utf-8")
print repr(u2) # u'\u4e2d\u6587'

s2 = u.decode("utf-8")
u2 = s.encode("utf-8")

市面上有些庫,例如BeautifulSoup,有時候不正確地使用了ascii來解決中文編碼,要修正這種行為在文件頭加上,

import sys
reload(sys) #載入setdefaultencoding方法
sys.setdefaultencoding('utf-8')

2.3 讀寫文件編碼

采用python的open()方法打開文件時,read()讀取的是str,編碼就是文件本身的編碼。而調(diào)用write()寫文件時,如果參數(shù)是unicode,則需要用指定編碼encode,如果write()參數(shù)是unicode而且沒有指定編碼,則采用python默認(rèn)編碼encode后再寫入。

#coding:utf-8                                                                   
f = open("testfile")
s = f.read()
f.close()
print type(s) # <type 'str'>

u = s.decode("utf-8") #testfile是utf-8編碼
f = open("testfile", "w")
f.write(u.encode("gbk")) #以gbk編碼寫入,testfile為gbk編碼
f.close()

此外,python codecs模塊提供了一個open()方法,可以指定編碼打開文件,使用這個方法打開文件讀取返回是unicode。寫入時,如果write參數(shù)是unicode,則使用打開文件時的編碼寫入,如果是str,則先使用默認(rèn)編碼成unicode后再以打開文件的編碼寫入(這里需要注意如果str是中文,而默認(rèn)編碼sys.getdefaultencoding()是ascii的話就會報解碼錯誤)。

#coding:gbk
import codecs

f = codecs.open('testfile', encoding='utf-8')
u = f.read()
f.close()
print type(u) # <type 'unicode'>

f = codecs.open('testfile', 'a', encoding='utf-8')
f.write(u) #寫入unicode

# 寫入gbk編碼的str,自動進(jìn)行解碼編碼操作
s = '漢'
print repr(s) # '\xba\xba'
# 這里會先將GBK編碼的str解碼為unicode再編碼為UTF-8寫入
#f.write(s) #默認(rèn)編碼為ascii時,這會報解碼錯誤。
f.close()

2.4 與編碼相關(guān)的方法

#coding: gbk
import sys
import locale

def p(f):
    print '%s.%s(): %s' %(f.__module__, f.__name__, f())
# 返回當(dāng)前系統(tǒng)所使用的默認(rèn)字符編碼
p(sys.getdefaultencoding)

# 返回用于轉(zhuǎn)換Unicode文件名至系統(tǒng)文件名所使用的編碼
p(sys.getfilesystemencoding)

# 獲取默認(rèn)的區(qū)域設(shè)置并返回元祖(語言, 編碼)
p(locale.getdefaultlocale)

# 返回用戶設(shè)定的文本數(shù)據(jù)編碼
# 文檔提到this function only returns a guess
p(locale.getpreferredencoding)

# \xba\xba是'漢'的GBK編碼
# mbcs是不推薦使用的編碼,這里僅作測試表明為什么不應(yīng)該用
print r"'\xba\xba'.decode('mbcs'):", repr('\xba\xba'.decode('mbcs'))

在筆者的Windows上的結(jié)果(區(qū)域設(shè)置為中文(簡體, 中國))

sys.getdefaultencoding(): ascii
sys.getfilesystemencoding(): mbcs
locale.getdefaultlocale(): ('zh_CN', 'cp936')
locale.getpreferredencoding(): cp936
'\xba\xba'.decode('mbcs'): u'\u6c49'

3 python開發(fā)過程中涉及到的編碼

在開發(fā)python程序的過程中,會涉及到三個方面的編碼:

  • Python程序文件的編碼;
  • Python程序運(yùn)行時環(huán)境(IDE)的編碼;
  • Python程序讀取外部文件、網(wǎng)頁的編碼;

3.1 Python程序文件的編碼

例如,Python2自帶的IDE,當(dāng)創(chuàng)建了一個文件保存的時候提示:

Paste_Image.png

這是因為Python2編輯器默認(rèn)的編碼是ASCII,它是無法識別中文的,所以會彈出這樣的提示。這也是我們在大多數(shù)情況下寫Python2程序的時候習(xí)慣在程序的第一行加上:#coding: utf-8。

3.2 Python程序運(yùn)行時環(huán)境(IDE)的編碼

執(zhí)行下面一段程序

#coding: utf-8
from selenium import webdriver

driver = webdriver.Firefox()
driver.get("http://www.baidu.com")
# 返回百度頁面底部備案信息
text = driver.find_element_by_id("cp").text
print(text)
driver.close()

在windows cmd下執(zhí)行:


Paste_Image.png

我們要獲取的信息是:?2015 Baidu 使用百度前必讀 意見反饋 京ICP證030173號
Windows cmd用的是cp936,也就是中文的GB2312,在GBK的字符集里沒有"?",這就導(dǎo)致通過GBK解析的時候出現(xiàn)編碼問題。
那假設(shè),我還就想在cmd下執(zhí)行這個python程序了,那么可以去修改cmd的默認(rèn)編碼類型為utf-8,對應(yīng)的編碼為CHCP 65001(utf-8)。在cmd下輸入:chcp 65001命令回車。


Paste_Image.png

然后,修改cmd的字體為"Lucida Console",再來執(zhí)行程序就可以被正確輸出了。
Paste_Image.png

3.3 Python程序讀取外部文件、網(wǎng)頁的編碼

N/A

3.4 chardet模塊

chardet是一個非常優(yōu)秀的編碼識別模塊。
通過pip安裝
>pip install chardet

>>> from chardet import detect
>>> a = "中文"
>>> detect(a)
{'confidence': 0.682639754276994, 'encoding': 'KOI8-R'}
>>> 

3.5 Python的字符串,Python對Unicode的支持

因為Python的誕生比Unicode標(biāo)準(zhǔn)發(fā)布的時間還要早,所以最早的Python只支持ASCII編碼,普通的字符串'ABC'在Python內(nèi)部都是ASCII編碼的。Python提供了ord()和chr()函數(shù),可以把字母和對應(yīng)的數(shù)字相互轉(zhuǎn)換:

>>> ord('A')
65
>>> chr(65)
'A'
>>> 

Python在后來添加了對Unicode的支持,以Unicode表示的字符串用u'...'表示,比如:

>>> print u'中文'
中文
>>> u'中'
u'\u4e2d'
>>> len(u'中')
1
>>> 

寫u'中'和u'\u4e2d'是一樣的,\u后面是十六進(jìn)制的Unicode嗎。因此, u'A'和u'\u0041'也是一樣的。
兩種字符串如何相互轉(zhuǎn)換?字符串'xxx'雖然是ASCII編碼,但也可以看成是UTF-8編碼,而u'xxx'則只能是Unicode編碼。把u'xxx'轉(zhuǎn)換為UTF-8編碼的'xxx'用encode('utf-8')方法:

>>> u'ABC'.encode('utf-8')
'ABC'
>>> u'中文'.encode('utf-8')
'\xe4\xb8\xad\xe6\x96\x87'
>>> 

英文字符轉(zhuǎn)換后表示的UTF-8的值和Unicode值相等(但占用的存儲空間不同),而中文字符轉(zhuǎn)換后1個Unicode字符將變?yōu)?個UTF-8字符,你看到的\xe4就是其中一個字節(jié),因為的值是228,沒有對應(yīng)的字母可以顯示,所以以十六進(jìn)制顯示字節(jié)的數(shù)字。len()函數(shù)可以返回字符串的長度:

>>> len(u'ABC')
3
>>> len('ABC')
3
>>> len(u'中文')
2
>>> len('\xe4\xb8\xad\xe6\x96\x87')
6
>>> 

反過來,把UTF-8編碼表示的字符串'xxx'轉(zhuǎn)換為Unicode字符串u'xxx',用decode('utf-8')方法:

>>> 'abc'.decode('utf-8')
u'abc'
>>> '\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8')
u'\u4e2d\u6587'
>>> print '\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8')
中文

由于Python源代碼也是一個文本文件,所以,當(dāng)你的源代碼中包含中文的時候,在保存源代碼時,就需要務(wù)必指定保存為UTF-8編碼。當(dāng)Python解釋器讀取源代碼時,為了讓它按照UTF-8編碼讀取文件,我們通常在文件開頭寫上這兩行:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

第一行注釋是為了告訴Linux/OS X系統(tǒng),這是一個Python可執(zhí)行程序,Windows系統(tǒng)會忽略這個注釋;
第二行注釋是為了告訴Python解釋器,按照UTF-8編碼讀取源代碼,否則,你在源代碼中寫的中文輸出可能會有亂碼。
如果你使用Notepad++進(jìn)行編輯,除了要加上# -- coding: utf-8 --外,中文字符串必須是Unicode字符串:

Paste_Image.png

申明了UTF-8編碼并不意味著你的.py文件就是UTF-8編碼的,必須并且要確保Notepad++正在使用UTF-8 without BOM編碼:
Paste_Image.png

如果.py文件本身使用UTF-8編碼,并且也申明了# -- coding: utf-8 --,打開命令提示符測試就可以正常顯示中文:
Paste_Image.png

4 實際問題

相信很多用Sublime Text來寫Python2的同學(xué)都遇到過一下這個問題:在Sublime Text里用Ctrl+B運(yùn)行代碼print u'中文',想要打印出unicode類型的字符串時,會出現(xiàn)以下報錯,UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)。
分析:
Python在向控制臺(console)print的時候,因為控制臺只能看得懂由bytes(字節(jié)序列)組成的字符串,而Python中"unicode"對象存儲的是code points(碼點),因此Python需要將輸出中的“unicode”對象用編碼轉(zhuǎn)換為儲存bytes(字節(jié)序列)的"str"對象后,才能進(jìn)行輸出。
而在報錯里看到UnicodeEncodeError, 那就說明 Python 在將 unicode 轉(zhuǎn)換為 str 時使用了錯誤的編碼。而為什么是 'ascii' 編碼呢?那是因為 Python 2 的默認(rèn)編碼就是 ASCII,可以通過以下命令來查看 Python 的默認(rèn)編碼。

>>> import sys
>>> print sys.getdefaultencoding()
ascii

所以此時在Sublime Text里運(yùn)行print u'中文',實際上等于是運(yùn)行了,

print u'中文'.encode('ascii')

ASCII編碼無法對unicode的中文進(jìn)行編碼,因此就報錯了。
那為什么同樣的代碼 print u'中文' 在 Mac 的終端里卻能正常輸出中文,難道是因為終端下的 Python 2 的默認(rèn)編碼不是 ASCII?非也,在終端下運(yùn)行 sys.getdefaultencoding() 結(jié)果一樣是 ascii。那同樣是 ascii 為什么會有不同的結(jié)果?難倒這里 Python 用了另外一個編碼來轉(zhuǎn)換?
是的,其實 Python 在 print unicode 時真正涉及到的是另一組編碼:stdin/stdout/stderr 的編碼,也就是標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯誤輸出的編碼??梢酝ㄟ^以下命令來查看,這里是在我的終端下運(yùn)行的結(jié)果:

>>> import sys
>>> print sys.stdin.encoding
UTF-8
>>> print sys.stdout.encoding
UTF-8
>>> print sys.stderr.encoding
UTF-8

在正常情況下,Python2在print unicode時用來轉(zhuǎn)換的編碼并不是Python的默認(rèn)編碼sys.getdefaultencoding(),而是sys.stdout.encoding所設(shè)的編碼。
因為在我的終端下 Python 的 sys.stdout.encoding 編碼是 UTF-8,所以在終端里運(yùn)行 print u'中文' 時,實際上是等于運(yùn)行了:

print u'中文'.encode('UTF-8')

編碼正確,運(yùn)行正常,因此沒有報錯。
在類 UNIX 系統(tǒng)下,Python 應(yīng)該是通過環(huán)境變量 LC_CTYPE 來判斷 stdin/stdout/stderr 的編碼的。因此一般只要將 shell 的 LANG 環(huán)境變量設(shè)置對為 _.UTF-8 后,應(yīng)該就能在終端里直接 print 出 unicode 類型的字符串了,而不需要在 print 時手動加上 .encode('utf-8') 進(jìn)行編碼了。
但在 Sublime Text 里事情就沒那么美好了。在 Sublime Text 里運(yùn)行查看 stdout 編碼的命令,發(fā)現(xiàn):

import sys
print sys.stdout.encoding
-----------------------------
None
[Finished in 0.1s]

結(jié)果甚至不是 'ascii' 而是 None??赡苁且驗?Sublime Text 的 Build System 是用 subprocess.Popen 來運(yùn)行 Python 的,導(dǎo)致 Python 無法判斷出正確的 stdin/stdout/stderr 編碼,于是都變成 None 了。

這種情況也發(fā)生在輸出的目標(biāo)是管道的情況下:

$ python -c 'import sys; print sys.stdout.encoding' | tee /tmp/foo.txt
None

那么在這種 sys.stdout.encoding 為 None 情況下的 print unicode 怎么辦呢?答案就是 Python 只能很無奈地使用 sys.getdefaultencoding() 的默認(rèn)編碼 ascii 來對 unicode 進(jìn)行轉(zhuǎn)換了。這樣就出現(xiàn)了本文開頭所說的那個 UnicodeEncodeError 問題。

總結(jié)

總結(jié)一下Python 2向控制臺print輸出時的流程:

  1. Python啟動時,當(dāng)它發(fā)現(xiàn)當(dāng)前的輸出是連接到控制臺的時候,它會根據(jù)一些環(huán)境變量,例如環(huán)境變量LC_CTYPE,來設(shè)法判斷出sys.stdin/stdout/stderr.encoding編碼值。
  2. 當(dāng)Python無法判斷出所需的編碼時,它會將sys.stdin/stdout/stderr.encoding的值設(shè)置為None。
  3. print時判斷字符串是否是unicode類型。
  4. 如果是的話,并且sys.stdout.encoding不為None時,就使用sys.stdout.encoding編碼對unicode編碼成str后輸出。
  5. 如果sys.stdout.encoding為None的話,就使用sys.getdefaultencoding()默認(rèn)編碼來對unicode進(jìn)行轉(zhuǎn)換成str后輸出。
if sys.stdout.encoding:
    print unicode.encode(sys.stdout.encoding)
else:
    print unicode.encode(sys.getdefaultencoding())

解決方法

解決方法1:

先說最不正確的解決方法: 在文件頭部加上,

import sys
reload(sys)
sys.setdefaultencoding('utf-8')

這種方法通過dirty hack的方式在Python剛剛啟動時更改了Python的默認(rèn)編碼為utf-8。此后:

>>> print sys.getdefaultencoding()
utf-8

但就本文所討論的問題來說,這個方法并不是真正地直接解決了問題。就如上所說,Python只是在sys.stdout.encoding為None時,才會使用默認(rèn)編碼來轉(zhuǎn)換需要print的unicode字符串。那萬一在sys.stdout.encoding存在,但為ascii的情況下呢?這樣即使更改了Python的默認(rèn)編碼,同樣還是會出現(xiàn)UnicodeEncodeError報錯。所以對本問題來說,這個方法治標(biāo)不治本。
除此之外,很多人都用這個方法來解決Python 2下遇到的其它各種各樣的編碼問題。但實際上很多大牛都不推薦用這個方法來解決Python 2的編碼問題,這里引用下 StackOverflow 相關(guān)回答 里的一句話:

the use of sys.setdefaultencoding() has always been discouraged

為什么這個方法不被推薦呢?我們來看下Python文檔里對這個function是怎么說的:

This function is only intended to be used by the site module implementation and, where needed, by sitecustomize. Once used by the site module, it is removed from the sys module's namespace.

可以看到這個方法原本就不是面向用戶的方法,并沒有打算讓用戶用這個方法來更改Python 2的默認(rèn)編碼。
那為什么不建議我們更改 Python 的默認(rèn)編碼呢?這里引用 Python 核心開發(fā)者、Python Unicode 支持的設(shè)計者和實現(xiàn)者: Marc-André Lemburg,他在一個郵件列表上的回復(fù)

The only supported default encodings in Python are:

Python 2.x: ASCII

Python 3.x: UTF-8

If you change these, you are on your own and strange things will
start to happen. The default encoding does not only affect
the translation between Python and the outside world, but also
all internal conversions between 8-bit strings and Unicode.

Hacks like what's happening in the pango module (setting the
default encoding to 'utf-8' by reloading the site module in
order to get the sys.setdefaultencoding() API back) are just
downright wrong and will cause serious problems since Unicode
objects cache their default encoded representation.

Please don't enable the use of a locale based default encoding.

If all you want to achieve is getting the encodings of
stdout and stdin correctly setup for pipes, you should
instead change the .encoding attribute of those (only).

--

Marc-Andre Lemburg

eGenix.com

從此可見,Python 2 唯一支持的內(nèi)部編碼只有 ASCII,更改其默認(rèn)編碼為其它編碼可能會導(dǎo)致各種各樣奇怪的問題。在這里他也說了使用 sys.setdefaultencoding() 的方法是徹徹底底的錯誤,正確的方法應(yīng)該是更改 stdout 和 stdin 的編碼。
所以這個方法是最不正確的填坑方法,請大家慎用。

解決方法 2:

然后說說應(yīng)當(dāng)是姿勢最正確的,也是大家都懂的方法:
在 print的時候顯式地用正確的編碼來對 unicode 類型的字符串進(jìn)行encode('正確的編碼')為 str 后, 再進(jìn)行輸出。而在 print的時候,這個正確的編碼一般就是 sys.stdout.encoding的值。但也正如上述所說,這個值并不是一直是可靠的,因此需要根據(jù)所使用的平臺和控制臺環(huán)境來判斷出這個正確的編碼。而在 Mac 下這個正確的編碼一般都是 utf-8,因此若不考慮跨環(huán)境的話,可以無腦地一直用 encode('utf-8') 和 decode('utf-8') 來進(jìn)行輸入輸出轉(zhuǎn)換。
在我的經(jīng)驗中,這個策略也是解決 Python 2 其它 unicode 相關(guān)編碼問題的最佳方法。在 PyCon 2012 的一個演講中(關(guān)于 Python Unicode 問題很好的一個演講,這里有演講稿的中文翻譯版),對這個方法有一個很形象的比喻:

Paste_Image.png

因為在程序中進(jìn)進(jìn)出出的只有存儲 bytes(字節(jié)序列)的 str。因此最好的策略是將輸入的 bytes 馬上解碼成 unicode,而在程序內(nèi)部中均使用 unicode,而當(dāng)在進(jìn)行輸出的時候,盡早將之編碼成 bytes。
也就是要形成一個 Unicode 三明治(如圖), bytes 在外,Unicode 在內(nèi)。在邊界的地方盡早進(jìn)行 decode 和 encode。不要在內(nèi)部混用 str 和 unicode,盡可能地讓程序處理的字符串都為Unicode。

解決方法 3:

雖然解決方法2是最正確的方式,但是有時候在Sublime Text里調(diào)試些小腳本,實在是懶得在每個print語句后面寫一個尾巴.encode('utf-8')。那么有沒有辦法能讓Sublime Text像在終端里一樣直接就能print u'中文'呢?也就是說能不能解決sys.stdin/stdout/stderr.encoding為None的情況呢?
答案肯定是有的,一種方法是用類似更改默認(rèn)編碼的方法一樣,用 dirty hack 的方式在 Python 代碼中去顯式地更改sys.stdin/stdout/stderr.encoding 的值。一樣是不推薦,我也沒嘗試過,在這里就不詳說了。
另一種方法則是通過設(shè)置 PYTHONIOENCODING 環(huán)境變量來強(qiáng)制要求 Python 設(shè)置 stdin/stdout/stderr 的編碼值為我們想要的,這是一個相對比較干凈的解決方法。見文檔:

PYTHONIOENCODING

Overrides the encoding used for stdin/stdout/stderr, in the syntax encodingname:errorhandler. The :errorhandler part is optional and has the same meaning as in str.encode().

New in version 2.6.

在 Mac 下對全局 GUI 程序設(shè)置環(huán)境變量的方法是:使用 launchctl setenv <<key> <value>, ...>命令對所有 launchd 啟動的未來子進(jìn)程設(shè)置環(huán)境變量。
在這里順便科普下,為什么對所有 launchd 啟動的未來子進(jìn)程設(shè)置環(huán)境變量可以使得對 Mac 下所有 GUI 程序生效。這是因為 launchd 是 OS X 系統(tǒng)啟動后運(yùn)行的第一個非內(nèi)核進(jìn)程。我們可以在 activity monitor(活動監(jiān)視器)里看到,它的 pid 是很帥氣的 1。而之后所有的進(jìn)程都將是它的子進(jìn)程。另外還可以通過 launchd 在 Mac 下實現(xiàn)類 crontab 的功能。
launchctl setenv命令設(shè)置的全局環(huán)境變量會在電腦重啟后失效,因此就需要通過上面說的 launchd 的開機(jī)啟動任務(wù)的功能來在重啟后再設(shè)置一遍環(huán)境變量,其配置方法可以參考這里。也因為這個原因,我并沒有使用這個方法來設(shè)置 PYTHONIOENCODING環(huán)境變量。
而 Sublime Text 提供了一個設(shè)置 Build System 環(huán)境變量的方法,這個方法各平臺的 Sublime Text 都適用。
設(shè)置 Sublime Text 的 Python Build System 環(huán)境變量的步驟如下:

  1. 將 Sublime Text 默認(rèn)的 Python Build System 的配置文件Python.sublime-build(找到這個文件的最好方法是安裝插件 PackageResourceViewer)復(fù)制一份到 Sublime Text 的 /Packages/User 文件夾下(在 Mac 和 Sublime Text 3 下這個路徑是 ~/Library/Application Support/Sublime Text 3/Packages/User)。
  2. 打開編輯新復(fù)制來的 Python.sublime-build 文件,如下加上一行設(shè)置 PYTHONIOENCODING環(huán)境變量為 UTF-8 編碼的內(nèi)容,并保存:
{
    "shell_cmd": "python -u \"$file\"",
    "file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
+   "env": {"PYTHONIOENCODING": "utf8"},
    "selector": "source.python"
}

這樣一來終于在這么長的文章后能在 Sublime Text 里直接運(yùn)行 print u'中文',而不用再出現(xiàn)萬惡的 UnicodeEncodeError 了。
既然都研究到這了,不妨我們試試把 PYTHONIOENCODING 設(shè)置成其它編碼看看會出現(xiàn)什么情況,例如設(shè)置成簡體中文 Windows 的默認(rèn)編碼 cp936:"env": {"PYTHONIOENCODING": "cp936"}

import sys
print sys.stdout.encoding
print u'你好'
----------------------------------
cp936
[Decode error - output not utf-8]
[Finished in 0.1s]

[Decode error - output not utf-8],這就是 Sublime Text 在 Windows 下可能會出現(xiàn)的問題。這是因為 Sublime Text 的 Build System 默認(rèn)是用utf-8 編碼去解讀運(yùn)行的輸出的,而我們指定了讓 Python 用 cp936 編碼來生成 str 字符串進(jìn)行輸出,那么就會出現(xiàn) Sublime Text 無法識別輸出的情況了。同樣在對終端 export PYTHONIOENCODING=cp936后,在終端下 print u'你好' 輸出的就會是 ???這樣的亂碼解決辦法之一就是同樣在 Python.sublime-build 文件里設(shè)置 "env": {"PYTHONIOENCODING": "utf8"}來使得輸出統(tǒng)一為 utf-8?;蛘呤歉?Sublime Text 的 Build System 所接受的輸出編碼,將其改為一致的 cp936 編碼,同樣也是更改 Python.sublime-build 文件,加入一行:

{
    "shell_cmd": "python -u \"$file\"",
    "file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
+   "encoding": "cp936",
    "selector": "source.python"
}

那我們再試試把這兩個設(shè)置同時都加到 Python.sublime-build 文件里,也就是讓 Python 輸出 utf8 編碼的字符串,而讓 Sublime Text 用 cp936 編碼來解讀,看看會發(fā)生什么情況?

{
    "shell_cmd": "python -u \"$file\"",
    "file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
+   "env": {"PYTHONIOENCODING": "utf8"},
+   "encoding": "cp936",
    "selector": "source.python"
}
print u'你好'
----------------------
浣犲ソ
[Finished in 0.1s]

笑,居然不是 [Decode error - output not cp936],而是這么喜感的 “浣犲ソ”!
這是因為 “你好” 的 utf-8 編碼剛好和 “浣犲ソ” 的 cp936 編碼重合了,都是 '\xe6\xb5\xa3\xe7\x8a\xb2\xe3\x82\xbd',所以使用 cp936 編碼去解讀的 Sublime Text 就認(rèn)為這段字符串就是 “浣犲ソ” 而顯示了出來。

>>> print repr('浣犲ソ')  # cp936 編碼
'\xe6\xb5\xa3\xe7\x8a\xb2\xe3\x82\xbd'
>>> print repr(u'你好'.encode('utf-8'))  # utf-8 編碼
'\xe6\xb5\xa3\xe7\x8a\xb2\xe3\x82\xbd'

5 QA

1.1 請教個問題,文件頭部定義的編碼#--coding: utf-8 -- 和sys.setdefaultencoding()定義的編碼是一回事嗎?

#--coding:utf-8--指明'當(dāng)前文件'的encoding。sys.getdefaultencoding()是python默認(rèn)把字符轉(zhuǎn)換為unicode的encoding。舉個例子,假如你的某個源代碼文件里有韓文,并且由于不知什么原因,源代碼必須用韓文euc-kr。這時你得在源文件開頭聲明#coding: euc-kr。
然后這個文件里有這么一段代碼:

a = b'hello'
b = u"#*%韓%#@#%文!@#$字$%符" 
c = a + b

虛擬機(jī)在執(zhí)行'c = a + b'這句的時候,發(fā)現(xiàn)a是一個byte string,b是一個unicode string。它就會用default encoding先去decode一下a,再去和b進(jìn)行拼接。所以實際執(zhí)行的是'c = a.decode(DEFAULT_ENCODING) + b'。
其中的DEFAULT_ENCODING在python2下默認(rèn)是'ascii',可以用reload(sys): sys.setdefaultencoding('utf-8')改成'utf-8',在python3下默認(rèn)就是'utf-8'。

參考:

  1. http://www.cnblogs.com/fnng/p/5008884.html
  2. http://www.liaoxuefeng.com
  3. https://www.v2ex.com/t/163786
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容