在從普通程序員進(jìn)階到優(yōu)秀程序員的路上蚕钦,字符編碼是一個(gè)不得不跨過(guò)去的坎沧卢,我們幾乎所有的程序都會(huì)涉及到字符處理讥脐,如果跨不過(guò)這個(gè)坎,那么幾乎注定會(huì)面對(duì)一些坑幔戏。
本篇文章試圖通過(guò)實(shí)際的例子來(lái)闡釋字符編碼解碼的過(guò)程玛追,從而能夠更加清晰地認(rèn)識(shí)程序到底是怎樣處理字符的。在進(jìn)入正文之前,你需要先了解字符集和字符編碼的區(qū)別痊剖,需要知道什么是Unicode韩玩,什么是UTF-8,GBK等基本概念陆馁,如果你不了解找颓,請(qǐng)移步下面的幾篇文章:
字符編碼詳解
字符編碼筆記
之后,我們?cè)囅胍幌碌撸绦蛱幚碜址倪^(guò)程是怎樣的叮雳?我想最開(kāi)始一定是先打開(kāi)一個(gè)編輯器想暗,把程序?qū)懗鰜?lái)妇汗,然后將程序保存為一個(gè)源文件(Python中就是.py文件),所以我們先從文件的存儲(chǔ)開(kāi)始說(shuō)起说莫。
源文件的存儲(chǔ)
我們?cè)诰庉嬈髦袑懙拇a都是字符形式存在的杨箭,而當(dāng)我們要將這些字符存儲(chǔ)到硬盤時(shí),必須有一個(gè)編碼過(guò)程储狭,因?yàn)橛?jì)算機(jī)只能認(rèn)識(shí)0/1序列互婿,所以這些字符就必須通過(guò)一些編碼規(guī)則轉(zhuǎn)化成二進(jìn)制序列,然后再存儲(chǔ)到硬盤辽狈。比如我們寫了下面一段程序
s = '你好'
print repr(s), s
當(dāng)我們存儲(chǔ)該文件時(shí)慈参,如果是以GB2312編碼方式進(jìn)行存儲(chǔ)的,那么文件的二進(jìn)制表示是這樣的
? testProgram hexdump -C gb2312encodingfile.py
00000000 73 20 3d 20 27 c4 e3 ba c3 27 0a 70 72 69 6e 74 |s = '....'.print|
00000010 20 72 65 70 72 28 73 29 2c 20 73 0a | repr(s), s.|
0000001c
這里73代表s
20代表空格
3d代表=
27代表'
c4 e3代表你
ba c3代表好
刮萌,以此類推
在這里可以看出漢字在GB2312中是用兩個(gè)字節(jié)來(lái)表示的驮配。
我們?cè)偈褂胾tf-8來(lái)存儲(chǔ)同樣的一段代碼,看看其二進(jìn)制表示是什么樣子
? testProgram hexdump -C utf8encodingfile.py
00000000 73 20 3d 20 27 e4 bd a0 e5 a5 bd 27 0a 70 72 69 |s = '......'.pri|
00000010 6e 74 20 72 65 70 72 28 73 29 2c 20 73 0a |nt repr(s), s.|
0000001e
同樣的這里73代表s
20代表空格
3d代表=
27代表'
但是你好
漢字是用三個(gè)字節(jié)來(lái)表示的
e4 bd a0代表你
e5 a5 bd代表好
現(xiàn)在源文件已經(jīng)以二進(jìn)制碼流存儲(chǔ)到了硬盤着茸,那么源代碼又是如何執(zhí)行的呢壮锻?
源代碼執(zhí)行
源代碼執(zhí)行的時(shí)候,Python解釋器首先會(huì)將源文件load進(jìn)內(nèi)存當(dāng)中涮阔,然后一行行開(kāi)始讀取文件并解釋執(zhí)行猜绣。
但是這里需要注意的是,如果是str字符串敬特,python解釋器只會(huì)讀取其二進(jìn)制碼流掰邢,假設(shè)我們使用的是gb2312encodingfile.py,那么s指向的字符串你好
讀進(jìn)內(nèi)存后的表示就是c4 e3 ba c3
, 當(dāng)我們使用print打印的時(shí)候伟阔,如果是在Windows的console上執(zhí)行辣之,則可以正確執(zhí)行,顯示如下
? testProgram python gb2312encodingfile.py
'\xc4\xe3\xba\xc3' 你好
但是在Linux上或者mac上無(wú)法正確執(zhí)行减俏,顯示如下:
? testProgram python gb2312encodingfile.py
'\xc4\xe3\xba\xc3' ???
這是由于Windows console默認(rèn)是GBK編解碼的(GB2312的擴(kuò)展)召烂,所以可以將\xc4\xe3\xba\xc3
正確解碼顯示成漢字你好
,但是在Linux或者M(jìn)ac上娃承,console的默認(rèn)編解碼方式是UTF-8奏夫,所以也就無(wú)法將\xc4\xe3\xba\xc3
正確顯示出來(lái)怕篷。
另外一個(gè)小插曲是,如果代碼中有漢字酗昼,需要在文件開(kāi)頭聲明編碼方式(#-*- coding: utf-8 - 或者# coding=utf8)廊谓,否則解釋器默認(rèn)使用ASCII編碼方式去打開(kāi)源文件,這樣就會(huì)報(bào)錯(cuò)麻削,如下
? testProgram python gb2312encodingfile.py
File "gb2312encodingfile.py", line 1
SyntaxError: Non-UTF-8 code starting with '\xc4' in file gb2312encodingfile.py on line 1, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details
但是如果我們的字符串是unicode對(duì)象的字符串蒸痹,那么Python解釋器會(huì)將字符串的字節(jié)序列先進(jìn)行解碼,然后再將解碼后的字節(jié)序列的引用賦給s呛哟,可以更改utf8encodingfile.py代碼如下:
#-*- coding: utf-8 -*-
s = '你好'
print repr(s), s
u = u'你好'
print repr(u), u
保存后叠荠,使用hexdump查看其二進(jìn)制編碼如下:
? testProgram hexdump -C utf8encodingfile.py
00000000 23 2d 2a 2d 20 63 6f 64 69 6e 67 3a 20 75 74 66 |#-*- coding: utf|
00000010 2d 38 20 2d 2a 2d 0a 73 20 3d 20 27 e4 bd a0 e5 |-8 -*-.s = '....|
00000020 a5 bd 27 0a 70 72 69 6e 74 20 72 65 70 72 28 73 |..'.print repr(s|
00000030 29 2c 20 73 0a 0a 75 20 3d 20 75 27 e4 bd a0 e5 |), s..u = u'....|
00000040 a5 bd 27 0a 70 72 69 6e 74 20 72 65 70 72 28 75 |..'.print repr(u|
00000050 29 2c 20 75 0a |), u.|
00000055
仔細(xì)觀察會(huì)發(fā)現(xiàn)兩個(gè)你好
字符串都編碼成了e4 bd a0 e5 a5 bd
然后在mac上執(zhí)行,結(jié)果如下:
? testProgram python utf8encodingfile.py
'\xe4\xbd\xa0\xe5\xa5\xbd' 你好
u'\u4f60\u597d' 你好
可以看出s指向的字節(jié)序列是\xe4\xbd\xa0\xe5\xa5\xbd
扫责,而u指向的字節(jié)序列是\u4f60\u597d
(也就是將e4 bd a0 e5 a5 bd 解碼成了\u4f60\u597d)
但是如果我們更改的是gb2312encodingfile.py榛鼎,并使用gb2312編碼保存,再執(zhí)行這個(gè)程序看看會(huì)是什么結(jié)果鳖孤。
結(jié)果直接報(bào)錯(cuò):
? testProgram python gb2312encodingfile.py
File "gb2312encodingfile.py", line 5
u = u'???'
SyntaxError: (unicode error) 'utf8' codec can't decode byte 0xc4 in position 0: invalid continuation byte
這是由于Python解釋器嘗試用聲明的utf-8編碼方式去解碼gb2312編碼的字節(jié)序列者娱,所以造成了這樣的錯(cuò)誤。
至此我們已經(jīng)知道了Python如何讀寫源文件的苏揣,那么Python執(zhí)行的時(shí)候又是如何讀寫外部文件的呢黄鳍?
文件讀寫
現(xiàn)在我們使用如下代碼嘗試將字符串寫到文件當(dāng)中,注意源碼保存使用utf-8, 文件名為utf8encodingfile_write.py
#-*- coding: utf-8 -*-
s = '你好'
with open('stroutput.txt', 'w') as f:
f.write(s)
u = u'你好'
with open('unicodeoutput.txt', 'w') as f:
f.write(u)
在mac上執(zhí)行平匈,結(jié)果如下:
? testProgram python utf8encodingfile_write.py
Traceback (most recent call last):
File "utf8encodingfile_write.py", line 8, in <module>
f.write(u)
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)說(shuō)明系統(tǒng)在寫文件編碼的時(shí)候嘗試使用ASCII來(lái)進(jìn)行編碼框沟,可是我們明明已經(jīng)聲明了使用utf-8啊。
原來(lái)文件頭聲明使用utf-8吐葱,只是用于解釋器去解釋源碼文件的時(shí)候使用街望,當(dāng)我們調(diào)用write去寫一個(gè)文件的時(shí)候,會(huì)調(diào)用系統(tǒng)默認(rèn)的編碼設(shè)置來(lái)進(jìn)行編碼弟跑。我們來(lái)看下系統(tǒng)默認(rèn)的編碼是什么:
>>> import sys
>>> sys.getdefaultencoding()
'ascii'
果然灾前,系統(tǒng)默認(rèn)就是ascii編碼方式。
解決這個(gè)問(wèn)題有兩種方式孟辑,一種是修改系統(tǒng)默認(rèn)的編碼方式哎甲,另一種是在open的時(shí)候指定編碼方式,其中第二種顯然更加優(yōu)雅一些饲嗽。
# 通過(guò)修改系統(tǒng)默認(rèn)編碼方式來(lái)實(shí)現(xiàn)utf-8編碼
import sys
reload(sys) # 這里必須reload一下才能找到setdefaultencoding method
sys.setdefaultencoding('utf-8')
# 通過(guò)在codecs.open中設(shè)置編碼方式
import codecs
with codecs.open("filename", "w", encoding="utf-8") as f:
f.write(u)
同樣的炭玫,當(dāng)我們讀取一個(gè)文件的時(shí)候,也可以通過(guò)codecs.open來(lái)設(shè)定編解碼方式貌虾,但是首先我們需要知道這個(gè)要讀取的文件的編碼方式吞加,假設(shè)文件是以u(píng)tf-8的方式進(jìn)行編碼的,讀取的時(shí)候就可以如下:
import codecs
with open("somefile", "r", encoding="utf-8") as f:
content = f.read()
另外,有時(shí)我們并非從文件中讀取衔憨,而是直接使用了一個(gè)非標(biāo)準(zhǔn)字符叶圃,這是就需要使用decode先解碼
# if not decode, will raise exception: 'ascii' codec can't
# decode byte 0xe2 in position 0: ordinal not in range(128)
dash = '–'.decode("utf8")
if dash in title:
title = title.split(dash)[0]
至此,關(guān)于Python編碼就講完了践图,如果你有收獲掺冠,就請(qǐng)點(diǎn)個(gè)贊鼓勵(lì)下吧!