當前位置:
首頁 > 知識 > 不想再被鄙視?那就看進來! 一文搞懂 Python 2 字元編碼

不想再被鄙視?那就看進來! 一文搞懂 Python 2 字元編碼

點擊上方

Python開發

」,選擇「置頂公眾號」


關鍵時刻,第一時間送達!




程序員都自視清高,覺得自己是創造者,經常鄙視不太懂技術的產品或者QA。可悲的是,程序員之間也相互鄙視,

程序員的鄙視鏈流傳甚廣

,作為一個Python程序員,自然最關心的是下面這幅圖啦








我們項目組一值使用Python2.7,雖然我們也知道Python3的諸多好處,也曾經蠢蠢欲動過,但由於各種歷史原因,以及業務的壓力,我們只可能繼續使用Python2.7。更悲哀的是,我們組不是那麼international,所以代碼中還是涉及到大量的中文,因此偶爾也會遇到亂碼以及UnicodeError,於是生活在了鄙視鏈的末端。

因此,本文的目標是解釋清楚 python2.7 中unicode、str的編解碼關係,力求在鄙視鏈中前進一步。


注意

:本文實驗主要基於win7,Python2.7;以及Linux ,Python2.7。除非特殊說明,所有的命令都是在終端中互動式輸入;如果沒有強調平台,那麼就是window上的結果。下面是一些默認的環境信息(其重要性後文會介紹)


windows



>>>

import

sys

,

locale


>>>

sys

.

getdefaultencoding

()


"ascii"


>>>

locale

.

getdefaultlocale

()


(

"zh_CN"

,

"cp936"

)


>>>

sys

.

stdin

.

encoding


"cp936"


>>>

sys

.

stdout

.

encoding


"cp936"


>>>

sys

.

getfilesystemencoding

()


"mbcs"




注意,上面

CP936是GBK的別名

,在https://docs.python.org/2/library/codecs.html#standard-encodings 可以查看。


Linux



>>>

import

sys

,

locale


>>>

sys

.

getdefaultencoding

()


"ascii"


>>>

locale

.

getdefaultlocale

()


(

"zh_CN"

,

"UTF-8"

)


>>>

sys

.

stdin

.

encoding


"UTF-8"


>>>

sys

.

stdout

.

encoding


"UTF-8"


>>>

sys

.

getfilesystemencoding

()


"UTF-8"




從字元編碼說起


首先來說一說gbk gb2312 unicode utf-8這些術語,這些術語與語言無關。


計算機的世界只有0和1,因此任何字元(也就是實際的文字元號)也是由01串組成。計算機為了運算方便,都是8個bit組成一個位元組(Byte),字元表達的最小單位就是位元組,即一個字元佔用一個或者多個位元組。字元編碼(

character encodin

g)就是字集碼,編碼就是將字符集中的字元映射為一個唯一二進位的過程。


計算機發源於美國,使用的是英文字母(字元),所有26個字母的大小寫加上數字0到10,加上符號和控制字元,總數也不多,用一個位元組(8個bit)就能表示所有的字元,這就是ANSI的「Ascii」編碼(American Standard Code for Information Interchange,美國信息互換標準代碼)。比如,小寫字母『a』的ascii 碼是01100001,換算成十進位就是97,十六進位就是0x61。計算機中,一般都是用十六進位來描述字元編碼。


但是當計算機傳到中國的時候,ASCII編碼就行不通了,漢字這麼多,一個位元組肯定表示不下啊,於是有了GB 2312(中國國家標準簡體中文字符集)。GB2312使用兩個位元組來對一個字元進行編碼,其中前面的一個位元組(稱之為高位元組)從0xA1用到 0xF7,後面一個位元組(低位元組)從0xA1到0xFE,GB2312能表示幾千個漢字,而且與asill嗎也是兼容的。


但後來發現,GB2312還是不夠用,於是進行擴展,產生了GBK(即漢字內碼擴展規範), GBK同Gb2312一樣,兩個位元組表示一個字元,但區別在於,放寬了對低位元組的要求,因此能表示的範圍擴大到了20000多。後來,為了容納少數名族,以及其他漢字國家的文字,出現了GB13080。GB13080是兼容GBK與GB2312的,能容納更多的字元,與GBK與GB2312不同的是,GB18030採用單位元組、雙位元組和四位元組三種方式對字元編碼


因此,就我們關心的漢字而言,三種編碼方式的表示範圍是:


GB18030 》 GBK 》 GB2312


GBK是GB2312的超集,GB1803又是GBK的超集

。後面也會看到,一個漢字可以用GBK表示,但不一定能被GB2312所表示


當然,世界上還有更多的語言與文字,每種文字都有自己的一套編碼規則,這樣一旦跨國就會出現亂碼,亟待一個全球統一的解決辦法。這個時候ISO(國際標準化組織)出馬了,發明了」Universal Multiple-Octet Coded Character Set」,簡稱 UCS, 俗稱 「unicode」。目標很簡單:廢了所有的地區性編碼方案,重新搞一個包括了地球上所有文化、所有字母和符號 的編碼!


unicode每種語言中的每個字元設定了統一併且唯一的二進位編碼,以滿足跨語言、跨平台進行文本轉換、處理的要求。unicode編碼一定以u開頭。


但是,unicode只是一個編碼規範,是所有字元對應二進位的集合,而不是具體的編碼規則。或者說,

unicode是表現形式,而不是存儲形式,就是說沒用定義每個字元是如何以二進位的形式存儲的

。這個就跟GBK這些不一樣,GBK是表裡如下,表現形式即存儲形式。


比如漢字「嚴」的unicode編碼是u4e25,對應的二進位是1001110 00100101,但是當其經過網路傳輸或者文件存儲時,是沒法知道怎麼解析這些二進位的,容易和其他位元組混在一起。那麼怎麼存儲unicode呢,於是出現了UTF(UCS Transfer Format),這個是具體的編碼規則,即UTF的表現形式與存儲格式是一樣的。


因此,可以說,

GBK和UTF-8是同一個層面的東西,跟unicode是另一個層面的東西,unicode飄在空中,如果要落地,需要轉換成utf-8或者GBK

。只不過,轉換成Utf-8,大家都能懂,更懂用,而轉換成GBK,只有中國人才看得懂


UTF也有不同的實現,如UTF-8, UTF-16, 這裡以UTF-8為例進行講解(下面一小節引用了阮一峰的文章)。


unicode與utf-8


UTF-8最大的一個特點,就是它是一種變長的編碼方式。它可以使用1~4個位元組表示一個符號,根據不同的符號而變化位元組長度。UTF-8的編碼規則很簡單,只有二條:


1)對於單位元組的符號,位元組的第一位設為0,後面7位為這個符號的unicode碼。因此對於英語字母,UTF-8編碼和ASCII碼是相同的。


2)對於n位元組的符號(n>1),第一個位元組的前n位都設為1,第n+1位設為0,後面位元組的前兩位一律設為10。剩下的沒有提及的二進位位,全部為這個符號的unicode碼。


下表總結了編碼規則,字母x表示可用編碼的位。



Unicode

符號範圍

      |        

UTF

-

8

編碼方式


(

十六進位

)

           |        

(二進位)


----------------------+---------------------------------------------


0000

0000

-

0000

007F

|

0xxxxxxx


0000

0080

-

0000

07FF

|

110xxxxx

10xxxxxx


0000

0800

-

0000

FFFF

|

1110xxxx

10xxxxxx

10xxxxxx


0001

0000

-

0010

FFFF

|

11110xxx

10xxxxxx

10xxxxxx

10xxxxxx




以漢字「嚴」為例,演示如何實現UTF-8編碼。


已知「嚴」的unicode是4E25(100111000100101),根據上表,可以發現4E25處在第三行的範圍內(0000 0800-0000 FFFF),因此「嚴」的UTF-8編碼需要三個位元組,即格式是「1110xxxx 10xxxxxx 10xxxxxx」。然後,從「嚴」的最後一個二進位位開始,依次從後向前填入格式中的x,多出的位補0。這樣就得到了,「嚴」的UTF-8編碼是「11100100 10111000 10100101」,轉換成十六進位就是E4B8A5。


當編解碼遇上Python2.x


下面使用Python語言來驗證上面的理論。在這一章節中,當提到unicode,一般是指unicode type,即Python中的類型;也會提到unicode編碼、unicode函數,請大家注意區別。


另外,對於編碼,也有兩種意思。第一個是名字,指的是字元的二進位表示,如unicode編碼、gbk編碼。第二個是動詞,指的是從字元到二進位的映射過程。不過後文中,編碼作為動詞,狹義理解為從unicode類型轉換成str類型的過程,解碼則是相反的過程。

另外強調的是,unicode類型一定是unicode編碼,而str類型可能是gbk、ascii或者utf-8編碼。


unicode 與 str 區別


在python2.7中,有兩種「字元串」類型,分別是str 與 unicode,他們有同一個基類basestring。str是plain string,其實應該稱之為

位元組串

,因為是每一個位元組換一個單位長度。而unicode就是unicode string,這才是真正的

字元串

,一個字元(可能多個位元組)算一個單位長度。


python2.7中,unicode類型需要在文本之間加u表示。




>>>

us

=

u

"嚴"


>>>

print

type

(

us

),

len

(

us

)


<

type

"unicode"

>

1


>>>

s

=

"嚴"


>>>

print

type

(

s

),

len

(

s

)


<

type

"str"

>

2


>>>




從上可以看到,第一,us、s的類型是不一樣的;其二,同一個漢字,不同的類型其長度也是不一樣的,對於unicode類型的實例,其長度一定是字元的個數,而對於str類型的實例,其長度是字元對應的位元組數目。這裡強調一下,s(s = 『嚴』)的長度在不同的環境下是不一樣的!後文會解釋


__str__ __repr__的區別


這是python中兩個magic method,很容易讓新手迷糊,因為很多時候,二者的實現是一樣的,但是這兩個函數是用在不同的地方


_str__, 主要是用於展示,str(obj)或者print obj的時候調用,返回值一定是一個str 對象


__repr__, 是被repr(obj), 或者在終端直接打obj的時候調用



>>>

us

=

u

"嚴"


>>>

us


u

"嚴"


>>>

print

us





可以看到,不使用print返回的是一個更能反映對象本質的結果,即us是一個unicode對象(最前面的u表示,以及unicode編碼是用的u),且「嚴」的unicode編碼確實是4E25。而print調用可us.__str__,等價於print str(us),使得結果對用戶更友好。那麼unicode.__str__是怎麼轉換成str的呢,答案會在後面揭曉


unicode str utf-8關係


前面已經提到,unicode只是編碼規範(只是字元與二進位的映射集合),而utf-8是具體的編碼規則(不僅包含字元與二進位的映射集合,而且映射後的二進位是可以用於存儲和傳輸的),即utf-8負責把unicode轉換成可存儲和傳輸的二進位字元串即str類型,我們稱這個轉換過程為編碼。而從str類型到unicode類型的過程,我們稱之為解碼。


Python中使用decode()和encode()來進行解碼和編碼,以unicode類型作為中間類型。如下圖所示



  

decode

    

encode


str

--------->

unicode

--------->

str




即str類型調用decode方法轉換成unicode類型,unicode類型調用encode方法轉換成str類型

。for example






>>>

us

=

u

"嚴"


>>>

ss

=

us

.

encode

(

"utf-8"

)


>>>

ss


"??¥"


>>>

type

(

ss

)


<

type

"str"

>


>>>

ss

.

decode

(

"utf-8"

)

==

us


True




從上可以看出encode與decode兩個函數的作用,也可以看出』嚴』的utf8編碼是E4B8A5。


就是說我們使用unicode.encode將unicode類型轉換成了str類型,在上面也提到unicode.__str__也是將unicode類型轉換成str類型。二者有什麼卻比呢


unicode.encode 與 unicode.__str__的區別


首先看看文檔



str

.

encode

([

encoding

[,

errors

]])


  

Return

an encoded version of the string

.

Default encoding

is

the current default string

encoding

.


  


object

.

__str__

(

self

)


  

Called by the str

()

built

-

in

function

and

by the print

statement to compute

the

informal

string

representation of an object

.




注意:str.encode 這裡的str是basestring,是str類型與unicode類型的基類


可以看到encode方法是有可選的參數:encoding 和 errors,在上面的例子中encoding即為utf-8;而__str__是沒有參數的,我們可以猜想,對於unicode類型,__str__函數一定也是使用了某種encoding來對unicode進行編碼。


首先不禁要問,如果encode方法沒有帶入參數,是什麼樣子的:



>>>

us

.

encode

()


Traceback

(

most recent call

last

)

:


File

"<stdin>"

,

line

1

,

in

<

module

>


UnicodeEncodeError

:

"ascii"

codec

can

"t encode character u"

u4e25

"

in

position

0

:

ordinal

not

in

range

(

128

)




不難看出,默認使用的就是ascii碼來對unicode就行編碼,為什麼是ascii碼,其實就是

系統默認

編碼(sys.getdefaultencoding的返回值)。ascii碼顯然無法表示漢字,於是拋出了異常。而使用utf-8編碼的時候,由於utf能夠表示這個漢字,所以沒報錯。


如果直接列印ss(us.encode(『utf-8』)的返回值)會怎麼樣



>>>

print

ss





結果略有些奇怪,us.__str__(即直接列印us)的結果不一樣,那麼試試encoding = gbk呢?



>>>

print

us

.

encode

(

"gbk"

)



U got it! 事實上也是如此,python會採用

終端默認

的編碼(用locale.getdefaultlocale()查看,windows是為gbk)將unicode編碼成str類型。


在Linux(終端編碼為utf-8),結果如下:



>>>

us

=

u

"嚴"


>>>

print

us

.

encode

(

"utf-8"

)



>>>

print

us

.

encode

(

"gbk"

)


??


>>>

print

us



>>>



注意上面的亂碼!


unicode gbk之間的轉換


在上上小節,介紹了unicode可以通過utf-8編碼(encoding = utf-8),轉換成utf-8表示的str,在上一節也可以看出unicode也可以通過gbk編碼(encoding=gbk),轉換成gbk表示的str。這裡有點暈,留作第一個問題,後面解釋


unicode與utf8之間的相互轉換可以計算得知,但unicode與gbk之間的相互轉換沒有計算公式,就只能靠查表了,就是說有一張映射表,有某一個漢字對應的unicode表示與gbk表示的映射關係



>>

us

=

u

"嚴"


>>>

us


u

"嚴"


>>>

us

.

encode

(

"gbk"

)


"??"


>>>

us

.

encode

(

"gb2312"

)


"??"


>>>

us

.

encode

(

"gb18030"

)


"??"


>>>

s

=

"嚴"


>>>

s


"??"


>>>




從上不難看出,嚴的unicdoe編碼是4e25,GBK編碼是d1cf,因此us通過gbk編碼就是d1cf。同樣也能看到,GB18030,GBK,GB2312是兼容的


為什麼print us.encode(『utf-8』)列印出「涓」


ss = us.encode(『utf-8』), ss是一個str類型,直接列印結果有點奇怪,一個「涓」字,那一個str類型的「涓」是哪些二進位組成的呢



>>>

s

=

"涓"


>>>

s


"??"




可以看到,str類型的「涓」,其二進位是E4B8,跟』嚴』的utf8編碼(E4B8A5)相差了一個A5,那麼就是因為A5顯示不出來,驗證如下:



>>>

print

"--%s--"

%

ss


--

?

-




因此,只是碰巧顯示了「涓」而已,事實上ss跟「」涓「」毫無關係


回答第一個問題:str類型到底是什麼


在上上小節,提到了utf-8編碼的str,與gbk編碼的str,感覺有點繞。我們知道,一個漢字『嚴』,可存儲的編碼格式可以是gbk(』xd1xcf』),也可以是utf-8(』xe4xb8xa5』),那麼當我們在終端敲入這個漢字的時候,是哪一種格式呢?取決於

終端默認編碼。


windows上(默認終端編碼為gbk):



>>>

s

=

"嚴"


>>>

s


"??"




Linux上(默認終端編碼為utf-8):





>>>

a

=

"嚴"


>>>

a


"??¥"





同樣一個漢字,同樣都是Python中的str類型,在不同的編碼格式下,其二進位是不一樣的。因此,其長度也是不一樣的,對於str類型,其長度是對應的位元組長度。


也能看出gbk編碼的位元組長度一般小於utf-8,這也是gbk繼續存在的一個原因。


這裡,

要強調一下,unicode的二進位形式是與終端的編碼格式無關的

!這個也不難理解。


unicode函數


str類型到unicode類型的轉換,出了上面提到的str.decode,還有一個unicode函數。兩個函數的簽名為:



unicode

(

object

[,

encoding

[,

errors

]])


Return

the Unicode

string

version of object

using one of the following

modes

:


 


str

.

decode

([

encoding

[,

errors

]])


Decodes the string

using the codec registered

for

encoding

.

encoding defaults to the default string

encoding

.




二者參數相同,事實上二者是等價的,encoding的默認值也是一樣的,都是sys.getdefaultencoding()的結果。for example:





>>>

s

=

"嚴"


>>>

newuse

=

unicode

(

s

)


Traceback

(

most recent call

last

)

:


File

"<stdin>"

,

line

1

,

in

<

module

>


UnicodeDecodeError

:

"ascii"

codec

can

"t decode byte 0xd1 in position 0: ordinal not in range(128)


 


>>> newuse = unicode(s, "

utf

-

8

")


Traceback (most recent call last):


File "<stdin>", line 1, in <module>


UnicodeDecodeError: "

utf8

" codec can"

t

decode

byte

0xd1

in

position

0

:

invalid continuation

byte


>>>

newuse

=

unicode

(

s

,

"gbk"

)


>>>

newuse


u

"嚴"




第一個UnicodeDecodeError,就是因為系統默認的編碼是asill嗎;第二個UnicodeDecodeError,是因為,s(str類型的實例)的編碼取決於終端默認編碼(即windows下的gbk),為了能列印出來,也就必須用gbk編碼來表示這個str,因此只能查詢gbk與unicode的映射表將s轉換成unicode類型。


為啥調用sys.setdefaultencoding


在諸多Python代碼中,都會看到這麼一段:



import sys


reload

(

sys

)


sys

.

setdefaultencoding

(

"utf-8"

)




不難猜想,setdefaultencodinggetdefaultencoding是配對的,為啥要將系統的默認編碼設置成utf-8,其實就是解決str到unicode的轉換問題。


上一小節已經提到過,使用unicode函數將str類型轉換成unicode類型時,要考慮兩個因素:第一,str本身是什麼編碼的;第二,如果沒有傳入encoding參數,默認使用sys.getdefaultencoding。encoding參數必須與str本身的編碼對應,否則就是UnicodeDecodeError。


寫python代碼的程序都知道,我們要在py文件第一行寫上:



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


這句話的作用在於,告訴編輯器,該文件裡面的所有str都採用utf-8編碼,且存儲文件的時候也是使用utf-8格式。


然後文件中就會使用下面的這種代碼。



s

=

"中文"


us

=

unicode

(

s

)




使用unicode強制轉換的時候,都不習慣帶參數,為了保證encoding參數必須與str本身的編碼一致,所以使用setdefaultencoding將系統默認編碼設置為utf-8


亂碼與UnicodeError


下面介紹幾種常見的亂碼與異常UnicodeError, 大多數亂碼或者異常的原因在前面已經講過了,同時,對於一些亂碼,也試圖給出可行的解決辦法。


UnicodeError包括UnicodeDecodeError 與UnicodeEncodeError ,前者是decode也就是str轉unicode的時候出了異常,後者則是encode也就是unicode轉str的時候出了異常。


對於一個str,直接列印


例子就是上面反覆提到的例子



>>>

ss

=

us

.

encode

(

"utf-8"

)


>>>

print

ss





如果一個str類型來自網路或者文件讀取,最好先按照對端encode的方式先decode成unicode,然後再輸出(輸出的時候會自動轉換成期望終端支持的編碼格式的str)


編碼範圍無法包括的漢字


直接上例子



>>>

newus

=

u

"囍"


>>>

newus


u

"囍"


>>>

newus

.

encode

(

"gbk"

)


"??"


>>>

newus

.

encode

(

"gb2312"

)


Traceback

(

most recent call

last

)

:


File

"<stdin>"

,

line

1

,

in

<

module

>


UnicodeEncodeError

:

"gb2312"

codec

can

"t encode character u"

u56cd

"

in

position

0

:

illegal multibyte

sequence


>>>




可以看到,『囍』字可以被gbk編碼,但是不能被gb2312編碼。


str轉unicode的時候


在上面講unicode函數的時候已經舉過例子,會爆出UnicodeDecodeError 異常。


這個錯誤比較的原因,更多來自str到unicode的默認轉換,比如一個str與一個unicode相加的時候:



>>>

a

=

"嚴"


>>>

b

=

u

"嚴"


>>>

c

=

a

+

b


Traceback

(

most recent call

last

)

:


File

"<stdin>"

,

line

1

,

in

<

module

>


UnicodeDecodeError

:

"ascii"

codec

can

"

t

decode

byte

0xd1

in

position

0

:

ordinal

not

in

range

(

128

)



unicode 與 str相加,str會轉換為unicode,使用默認的unicode(strobj, encoding = sys.getdefaultencoding())


看起來向unicode編碼的字元串


某些情況下,我們列印出一個str類型,看到結果是』嚴』, 或者』u4e25』,對於這個字元串,是不是很眼熟,不錯, 『嚴『的unicode編碼就是u』u4e25』。仔細一看,只是在引號前面多了一個u(表示是一個unicode類型)。那麼當我們看到一個』u4e25』的時候,怎麼知道對應的漢字是什麼?對於已知的這種格式的str,自然可以手動加一個u,然後在終端輸出,但是如果是一個變數,需要自動轉換成unicode呢,這個時候就可以使用python-specific-encodings中的unicode_escape



>>>

s

=

"嚴"


>>>

s


"嚴"


>>>

us

=

s

.

decode

(

"unicode_escape"

)


>>>

us


u

"嚴"




十六進位格式的字元串


有時候,也會看到類似這樣的str,』??』, 看起來也很熟悉,跟漢字「嚴」的gbk編碼』xd1xcf』很像,區別在於前者多了一個『』, 這樣就無法解釋成一個十六進位了。解決辦法是python-specific-encodings中的string_escape



>>>

s

=

"??"


>>>

s


"??"


>>>

print

s


xd1

xcf


>>>

news

=

s

.

decode

(

"string_escape"

)


>>>

news


"??"


>>>

print

news



給讀者的一個問題


在這裡留下一個問題:



u"嚴" == "嚴"


返回值是True 還是 False呢?當然這裡故意省去了上下文環境,不過明確的說,在不同的編碼環境下,答案是不一樣的,原因都在上文中!


總結與建議


不管怎麼樣解釋,python2.x中的字元編碼還是一件讓人頭疼的事情,即使搞懂了,之後遇到了也可能忘記。對於這個問題,諸多建議如下:


第一:使用python3,就不用再糾結str於unicode了;但是這個很難開發者說了算;


第二:不要使用中文,注釋什麼的都用英文;理想很豐滿,現實很難,只是導致大量的拼音;


第三:對於中文字元串,不要用str表示,而是用unicode表示;現實中也不好實施,大家都不願意多寫一個u


第四:只在傳輸,或者持久化的時候對unicode進行encode,相反的過程時decode


第五:對於網路介面,約定好編解碼格式,強烈建議使用utf-8


第六:看到UnicodeXXXError不要慌,如果XXX是Encode,那麼一定是unicode轉str的時候出了問題;如果是Decode,一定是str轉unicode的時候出了問題。


參考




  • python codecs



  • python-specific-encodings



  • 字元編碼筆記:ASCII,Unicode 和 UTF-8



  • 玩轉Python讓人討厭的編碼問題 





  • 來源:

    xybaby



  • www.cnblogs.com/xybaby/p/7814299.html



  • Python開發整理髮布,轉載請聯繫作者

    獲得授權


【點擊成為Java大神】

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python開發 的精彩文章:

Git 最佳實踐:commit msg
談談 Python 的生成器

TAG:Python開發 |