一、字符編碼初探
字符編碼其實(shí)就是將人類能識(shí)別的字符與計(jì)算機(jī)能識(shí)別的數(shù)字對(duì)應(yīng)起來。ASCII(American Standard Code for Information Interchange)美國信息交換標(biāo)準(zhǔn)代碼,是最早最通用的單字節(jié)編碼標(biāo)準(zhǔn)。
ASCII單字節(jié)編碼表示范圍有限,是不能滿足表示中文的,于是基于ASCII擴(kuò)展,制定了GB2312標(biāo)準(zhǔn)(GB是國標(biāo)的意思)。現(xiàn)在最常用的中文編碼標(biāo)準(zhǔn)GBK又是GB2312的升級(jí),能表示更多的字符。
計(jì)算機(jī)的發(fā)展和普及在各個(gè)國家和地區(qū)各有不同,各國也是制定了自己的編碼標(biāo)準(zhǔn),這些基于ASCII擴(kuò)展而來,使用多字節(jié)表示字符的延伸編碼方式稱為 ANSI 編碼。在簡體中文windows系統(tǒng)中,ANSI編碼代表GBK,而韓文系統(tǒng)中ANSI編碼代表EUC-KR。
由于不同國家和地區(qū)編碼標(biāo)準(zhǔn)不一致,也導(dǎo)致了它們之間存在復(fù)雜的編碼轉(zhuǎn)換,于是誕生了unicode編碼方式,以提供統(tǒng)一的編碼標(biāo)準(zhǔn),所以u(píng)nicode也叫萬國碼,其標(biāo)準(zhǔn)稱呼應(yīng)該是Universal Multiple-Octet Coded Character Set,簡稱UCS。而unicode又存在多種編碼方式的實(shí)現(xiàn),其中UTF-8是最常用的一種UTF(UCS Transfer Format)標(biāo)準(zhǔn)。
二、Python2/python3的默認(rèn)編碼
python2的默認(rèn)編碼是ascii,python3則是utf-8。可以通過如下方式獲取:
python2 -c "import sys;print(sys.getdefaultencoding())"
python3 -c "import sys;print(sys.getdefaultencoding())"
可以在python文件開頭設(shè)置默認(rèn)編碼,python3默認(rèn)就使用了utf-8,所以不需要該編碼聲明。
# -*- coding: UTF-8 -*-
# coding=utf-8
python2雖然指定了編碼,但還是不能很好地處理中文,在終端輸出、文件讀寫、json處理等都難免遇到問題。要解決各種編碼問題,需要明確當(dāng)前編碼是什么,python編碼的相關(guān)特性,數(shù)據(jù)來源是什么編碼,數(shù)據(jù)輸出又是什么編碼。
三、起點(diǎn)-python2打印輸出中文
我們?cè)趐ython2文件開頭設(shè)置了編碼方式為utf-8,如果cmd終端字符編碼不是utf-8,要正常打印輸出中文,還 需要將字符串先解碼,再編碼成終端的編碼格式輸出 。
比如,中文windows系統(tǒng)的cmd終端默認(rèn)是gbk中文編碼,chcp查看活動(dòng)代碼頁編號(hào)是936,也就是gbk編碼,要正常輸出中文,字符串需要先解碼,再編碼成終端的gbk格式打印,如下。
# -*- coding: UTF-8 -*-
#python2
import sys
print("中文".decode('utf-8').encode(sys.stdout.encoding)) #文件開頭已經(jīng)指定默認(rèn)編碼為utf-8,但是終端是gbk,所以需要先decode('utf-8') 再 encode(sys.stdout.encoding)
四、特性初識(shí)-python2/python3的unicode類型
python2與python3在定義unicode類型時(shí)是通用的。
#定義unicode類型
u = u'中文'
u = u"中文"
u = u'''中文'''
u = u"""中文"""
我們知道unicode是通用的,所以無論使用python2還是python3執(zhí)行如下代碼時(shí)均不會(huì)出現(xiàn)亂碼。
print(u"中文")
print(u'\u4e2d\u6587')#均輸出中文
所以前面python2 打印輸出 的問題其實(shí)可以簡寫。
print("中文".decode('utf-8')) #"中文".decode('utf-8')是unicode類?
五、區(qū)別-python2/python3的str、bytes類型
python2與python3在定義str、bytes類型時(shí)是通用的。
#定義str類型
s = 'test'
s = "test"
s = '''test'''
s = """test"""
?
#定義bytes類型
b = b'test'
b = b"test"
b = b'''test'''
b = b"""test"""
首先, bytes類型是字節(jié)序列,一個(gè)事實(shí)上的bytes類型每個(gè)元素就是一個(gè)字節(jié) 。
打開一個(gè)gbk編碼的文本,可以看到其對(duì)應(yīng)十六進(jìn)制字符碼。
utf-8編碼的文本,字符碼則不同。可以看到, 能表示更多字符的編碼標(biāo)準(zhǔn),需要更多的字節(jié)來表示字符 。
首先查看如下python2代碼運(yùn)行情況。
可以看到,python2處理str和bytes時(shí)是混用的,可以進(jìn)行+運(yùn)算,但它們都是字節(jié)序列。 python2沒有將str和bytes型數(shù)據(jù)做明顯的區(qū)分,是一種隱式的混用,并且python2處理str類型時(shí)優(yōu)先將其視為bytes 。事實(shí)上str.decode是bytes.decode,從而轉(zhuǎn)換成unicode。 str/bytes/unicode三者關(guān)系:str(bytes)—decode—>unicode—encode—>str(bytes) 。
不同于python2, python3對(duì)str和bytes型數(shù)據(jù)作了明顯區(qū)分,str表示文本,默認(rèn)就是原生unicode的utf-8編碼格式,bytes型數(shù)據(jù)就表示二進(jìn)制數(shù)據(jù) ,用下標(biāo)取bytes類型的單個(gè)元素返回的是int類型,而在python2中用下標(biāo)取bytes類型的單個(gè)元素返回的還是bytes。 bytes/str/unicode三者關(guān)系:bytes—decode—>str(unicode)—encode—>bytes ,不能像python2那樣string.decode。
s = '中文'
b = bytes(s,encoding='utf-8') # s.encode('utf-8')
s = str(b,encoding='utf-8') # b.decode('utf-8')
上面python2的實(shí)驗(yàn),標(biāo)準(zhǔn)輸出編碼是gbk,將\xd6\xd0\xce\xc4復(fù)制到python3驗(yàn)證編碼區(qū)別,如下python3的運(yùn)行情況,可以看到gbk可以正常解碼,再進(jìn)行encode(‘utf-8′)得到bytes字節(jié)序列是不是與前面utf-8文本中”中文”的十六進(jìn)制數(shù)據(jù)相同。
六、文件路徑/文件名/文件讀寫
1.文件路徑的困惑
存在如下一段代碼,遍歷目錄里面的文件。
# -*- coding: UTF-8 -*-
import os
all_files = []
for filepath, dirnames, filenames in os.walk(directory):
for filename in filenames:
tmppath = os.path.join(filepath, filename)
all_files.Append(tmppath)
print(all_files)
python2/python3結(jié)果對(duì)比。python3將str視為unicode處理的,所以正常顯示。可以看到python2是以gbk編碼來識(shí)別目錄的。
當(dāng)前活動(dòng)代碼頁是gbk,那python2識(shí)別目錄是否會(huì)受到當(dāng)前cmd終端編碼方式影響?改變當(dāng)前活動(dòng)代碼頁為utf-8后,目錄還是以gbk編碼識(shí)別,encode成utf-8才能正常打印出目錄名。
文章開頭了解到ANSI編碼的特點(diǎn),是否可以推測(cè)python2是以當(dāng)前系統(tǒng)ANSI編碼獲取目錄名的呢?linux shell驗(yàn)證確實(shí)如此。
2.python2的文件名亂碼
python2運(yùn)行如下代碼,文件名出現(xiàn)了亂碼。
# -*- coding: UTF-8 -*-
# python2/python3
with open('中文2.txt','w+') as f:
f.write("中文")?
前面推測(cè)python2是以當(dāng)前系統(tǒng)ANSI編碼獲取目錄名,所以創(chuàng)建文件是否也是這樣,由于聲明了編碼方式是utf-8,而文件名”中文2.txt”字符串是以bytes、utf-8格式存儲(chǔ)的,與gbk不一致,所以導(dǎo)致亂碼。嘗試將代碼改成如下,文件名正常。
# -*- coding: UTF-8 -*-
# python2
with open('中文2.txt'.decode('utf-8').encode('gbk'),'w+') as f: #當(dāng)前編碼是utf-8 所以先decode,再encode為系統(tǒng)的gbk編碼
f.write("中文")
那么找文件讀又會(huì)是怎樣?
# -*- coding: UTF-8 -*-
# python2
with open('中文2.txt','r') as f:
print(f.read().decode('utf-8'))
python2執(zhí)行報(bào)錯(cuò)No such file or directory,沒有文件或目錄。
修改文件名為 ‘中文2.txt’.decode(‘utf-8′).encode(‘gbk’) 后才正常找到了文件,由此可見, python2在識(shí)別目錄、open創(chuàng)建、讀取文件時(shí)均以系統(tǒng)ANSI編碼識(shí)別的 ,處理中文名稱時(shí), 需要將目錄/文件名字符串先解碼,再編碼成系統(tǒng)的ANSI編碼格式 。
3.寫入是否正常
打開前面python2生成的兩個(gè)文件,都是utf-8格式,內(nèi)容中文顯示正常!體會(huì)到python2的詭異編碼了嗎?至于這一點(diǎn),忘了吧…
python3就沒那么奇葩了,寫入文本后,中文windows系統(tǒng)是ANSI編碼,而linux是utf-8編碼。
python3 open文件操作若不指定編碼,則默認(rèn)以系統(tǒng)ANSI編碼寫入、讀取文本。 建議如果不是以二進(jìn)制讀取和寫入,open文件時(shí)可指定文本的編碼方式。
f = open("中文1.txt",'a+',encoding='utf-8')
4.讀取小結(jié)
不管python2還是python3,解決編碼問題的核心都是要解決編碼統(tǒng)一。通過”python2/python3的默認(rèn)編碼”小節(jié),我們認(rèn)識(shí)到python2與python3默認(rèn)編碼的區(qū)別,實(shí)際上 python文件開頭的編碼聲明聲明的是當(dāng)前腳本內(nèi)字符串的編碼 ,所以才有了python2打印輸出中文時(shí)需要先decode(‘utf-8′),再encode(‘gbk’)為cmd終端編碼格式,以及中文文件名的編碼轉(zhuǎn)換,至于python3,統(tǒng)一了編碼,str就是原生unicode,具有普適性,就沒有那么多編碼轉(zhuǎn)換。總之,讀取文件時(shí),需要明確文件的編碼,當(dāng)前python腳本文件的編碼聲明,輸出的編碼。如果一個(gè)utf-8格式文本文件內(nèi)包含”\xd6\xd0\xce\xc4″、”\u4e2d\u6587″等字符串,讀取后又如何處理呢?
python2 可以以string-escape和unicode-escape方式解碼。
# -*- coding: UTF-8 -*-
# python2
with open('a.txt','r') as f:
lines = f.readlines()
print(lines) #讀取后會(huì)加上轉(zhuǎn)義 ['\xd6\xd0\xce\xc4rn', '\u4e2d\u6587']
for line in lines:
if '\x' in line:
print(line.decode('string-escape'))
if '\u' in line:
print(line.decode('unicode-escape'))
python3 可以參考如下方式處理。
# -*- coding: UTF-8 -*-
import codecs
with open('a.txt','r',encoding='utf-8') as f:
lines = f.readlines()
print(lines)
for line in lines:
if '\x' in line:
bline = bytes(bytearray.fromhex(line.strip().replace('\x','')))
print(bline.decode('gbk')) #\xd6\xd0\xce\xc4 是"中文"gbk格式的字符碼
if '\u' in line:
print(codecs.decode(line,'unicode_escape'))
七、python2 json的問題
存在如下代碼。
# -*- coding: UTF-8 -*-
# python2
import json
a = {'a':'test','語言':'中文'}
with open('a.txt','a+') as f:
f.write(json.dumps(a))
運(yùn)行之后,json.dumps會(huì)將中文以u(píng)nicode的字符碼形式dump,并不是真正的中文,需要指定ensure_ascii=False參數(shù)來dump真正的中文。
json.dumps(a,ensure_ascii=False)
如下,dumps后文件為utf-8格式,如果讀取進(jìn)行json.loads,得到的字典”鍵”和”值”就都會(huì)是unicode類型的。
# -*- coding: UTF-8 -*-
import json
with open('a.txt','r+') as f:
print(json.loads(f.read()))
結(jié)果如下。
需要對(duì)獲取到的字典”鍵”和”值”進(jìn)行解碼的話,這里可以參考一段代碼處理。
def unicode_convert(input):
if isinstance(input, dict):
return {unicode_convert(key): unicode_convert(value) for key, value in input.items()}
elif isinstance(input, unicode):
return input.encode('utf-8')
else:
return input
八、總結(jié)
4月20日,Python2的最后一個(gè)版本發(fā)布:2.7.18。可以說python2已是過去式,python3才是未來。可為什么文章大部分內(nèi)容卻還是python2的呢?一是確實(shí)python2的字符編碼問題多,解決這些問題能更好的理解python編碼機(jī)制;二是即便python2不再有,但編碼問題一定一直會(huì)存在,不管是python自己生成處理的數(shù)據(jù)還是其它源數(shù)據(jù)。從解決python2的編碼問題到了解python2與python3的差異,總結(jié)出以下解決編碼問題的關(guān)鍵點(diǎn),如有不當(dāng),還望指正。
1.python2的默認(rèn)編碼是ascii,python3則是utf-8。
2.python文件開頭的編碼聲明聲明的是當(dāng)前腳本內(nèi)字符串的編碼,要避免編碼錯(cuò)誤,需要統(tǒng)一數(shù)據(jù)源,聲明的編碼類型,數(shù)據(jù)輸出三者的編碼。
3.python2沒有將str和bytes型數(shù)據(jù)做明顯的區(qū)分,是一種隱式的混用,并且python2處理str類型時(shí)優(yōu)先將其視為bytes。str/bytes/unicode三者關(guān)系:str(bytes)—decode—>unicode—encode—>str(bytes)。
4.python3對(duì)str和bytes型數(shù)據(jù)作了明顯區(qū)分,str表示文本,默認(rèn)就是原生unicode的utf-8編碼格式,bytes型數(shù)據(jù)就表示二進(jìn)制數(shù)據(jù)。bytes/str/unicode三者關(guān)系:bytes—decode—>str(unicode)—encode—>bytes。
5.python2在識(shí)別目錄、open創(chuàng)建、讀取文件時(shí)均以系統(tǒng)ANSI編碼識(shí)別的。
6.python3 open文件操作若不指定編碼,則默認(rèn)以系統(tǒng)ANSI編碼寫入、讀取文本。