1. 程式人生 > >關於python編解碼的一些坑(一)

關於python編解碼的一些坑(一)

學過python的都知道,python的encode,decode裡面有一些坑,掉進去後比較難爬出來。正好這段時間想總結一下這些坑,我會寫2-3篇文章來介紹我對這些坑的理解。既然是個人理解,那很可能有些考慮不對的地方。因此如果大家自認為有更準確的理解,也希望能相互交流,共同學習,一起進步!另,文章中引用的文獻,我都做了說明,也都註明了出處,以方便大家查閱。

文章比較長,基本是我一手敲下來的來

一,str 和 unicode
1) str 和 unicode 都是basestring的子類。
str典型編碼型別:gbk,utf8; 表現形式:\xc4\xe3\xba\xc3。若是你看到類似於\xc4\xe3這種型別的編碼,你首先要搞清楚,這個應該是中文編碼經過gbk或utf8或其他型別的編碼(總歸不是unicode編碼),至於到底是gbk還是utf8,不知道!而看到類似於\u4f60,就需要明白,這個是中文的unicode編碼或者說是unicode表示,未經具體的utf8或gbk編碼
2) str是經過編碼的(採用何種編碼取決於你的python編輯器,典型的如gbk, utf8。如筆者在cmd中預設為gbk編碼),典型形式:\xc4\xe3。
3) unicode 沒有經過編碼。這裡可以認為是有一個“統一世界”,在這個世界裡,所有人類世界中的字元(如英文字元,中文字元,阿拉伯字元,希臘字元等等)都有一個統一的代號。典型形式:\u4f60。
4) str是位元組串,表現形式 ” ” 或’ ’ 。是unicode經過encode編碼後的位元組所組成;

>>> s1="你好"
>>> s1
'\xc4\xe3\xba\xc3'
>>> len(s1)
4
>>> s2="hello"           #注意,後面“補充解釋”會講到
>>> s2   
'hello'     
>>> len(s2)
5

而unicode才是真正的字串,表現形式為u” ”

u1=u"你好"
>>> u1
u'\u4f60\u597d'
>>> len(u1)
2
>>> u2=u'hello'
#注意,後面“補充解釋”會講到 >>> u2 u'hello' >>> len(u2) 5

根據上面的四點,這裡要補充解釋幾個方面
str和unicode的典型表形式,為何他們長成這個樣子
先說unicode。“unicode目前普遍採用的是UCS-2標準,它用兩個位元組來表示(這裡說“表示”,而不是“編碼”,因為我覺得這樣更加準確)一個字元,故UCS-2最多隻能表示65536個字元。漢字簡體繁體加起來超過了7萬個,但是UCS-2最多隻能表示65535個字元,故肯定有部分漢字是UCS-2編碼無法表示的,UCS-2的處理辦法是是排除一些幾乎不用的漢字。另外,為了表示所有的漢字,unicode有UCS-4編碼規範,即用四個位元組來表示字串”(——百度百科)。
就拿\u4f60來說,它是中文“你”的UCS-2表示。\u 表示是unicode,4f60是用十六進位制表示的兩個位元組。
因此,今後我們碰到類似“\u—-”這種東東,我們要形成的條件反射是:首先它未經過編碼,它只是“某個字元”在統一世界一個統一程式碼表示,它完全等同於“某個字元”。它是真正的“字串”。

再說具體的編碼形式,如“utf8” 和 “gbk”
無論是utf8和gbk,他們所做的工作都是把unicode字元進行編碼,只是編碼的規則不同。從表象來看,他們都是表現為位元組串的形式。
如中文“你”(unicode為“\u4f60”)的utf編碼為: “\xe4\xbd\xa0”;而gbk的編碼為:“\xc4\xe3”。他們長得差不多,因此如果你看到了這種“\x–\x–\x–\x–\x–\x–”,我們要形成的條件反射是:首先它是某種編碼,它是“位元組串”,我們無法肯定它到底是哪種編碼(python提供了判斷這種位元組串可能是哪種編碼方式的API:chardet(),注意判斷的結果只是可能是某種編碼,並告知我們這種可能性有多大)。

 UTF-8和unicode的關係
utf-8只是unicode的一種具體的實現方式。utf8是針對unicode變長編碼設計的一種字首碼,根據字首可判斷是幾個位元組表示一個字元,如下圖:
這裡寫圖片描述

如果一個位元組的第一位是0,則這個位元組單獨就是一個字元;如果第一位是1,則連續有多少個1,就表示當前字元佔用多少個位元組。
比如”嚴”的unicode是4E25(100111000100101),4E25處在第三行的範圍內(0000 0800-0000 FFFF),因此”嚴”的UTF-8編碼需要三個位元組,即格式是”1110xxxx 10xxxxxx 10xxxxxx”。然後,從”嚴”的最後一個二進位制位開始,依次從後向前填入格式中的x,高位補0,得到”嚴”的UTF-8編碼是”11100100 10111000 10100101”。
可以發現:對於0x00-0x7F之間的字元,十進位制範圍是0–127,UTF-8編碼與ASCII編碼完全相同。可以得到進一步的結論,由於ASCII是計算機發展史上最早的編碼規範,它之後所發展的一些編碼規範,典型如utf-8,gb2312等都完全相容ASCII碼。

解釋第四點4)中的舉例。中文、英文在unicode和gbk的表現形式
先拿例子說話:(在cmd中,編碼格式為gbk)
英文:

>>> u"hello"
u'hello'
>>> "hello"
'hello'
>>> u"hello".encode('gbk')
'hello'

中文

>>> u"你好"
u'\u4f60\u597d'
>>> "你好"
'\xc4\xe3\xba\xc3'
>>> u"你好".encode('gbk')
'\xc4\xe3\xba\xc3'

不知道你看了這幾個例子有什麼想法。怎樣理解例子中發生的一切
我說說我自己的理解。先解釋一下>>> “hello”,發生了什麼,這種實質上是呼叫的是repr(),它告訴我們該字元在python中的存放方式。(區別 >>>print “hello”, print呼叫的是str( ), 它實質上在輸出的時候有個“編碼的動作”,自己查資料吧)
我們發現是英文時,我們無論是否指定編碼方式,它在python內部就是以英文字元存在的,而是中文時,無論它是否有指定某種編碼,它的方式都是某種“程式碼”的形式(\u4f60 或 \xc4\xe3)。
這裡,我從兩個方面來試著解釋上面的例子中所發生的東西。
第一個方面: 對unicode碼的操作

>>> u"hello"   
>>>u'hello' 
>>> u"你好"  
>>>u'\u4f60\u597d'

這一行的兩個句子實質上都沒有經過編碼。只是做了一下字元形式上的變換
表面上看,中文跟英文的unicode不一樣,但實質上是一樣的,還記的我們前面講的嗎“unicode碼就是字元的另外一種表現形式,它沒有經過其他任何編碼”。拿 u”h” 來說,它的unicode碼是u”\u0068”,在cmd中進行驗證

>>> u"\u0068" 
>>>u'h'

那為什麼>>> u”h”不顯示成u“\u0068”呢?我的理解是“內部儲存”和“外部顯示”是不一樣的。內部儲存都是unicode符,而在顯示給外部時,有兩種選擇,unicode符和字元本身。到底選擇哪種方式,有一個我自己總結的原則:哪種方式更友好就顯示哪種。對處在0-127範圍的英文字元來講,顯示字元本身u“h”肯定比干巴巴的u“\u0068 “更友好;在0-127之外的中文字元,只有一種選擇,就是顯示unicode符。實質上,你完全可以把unicode和字元本身等價看待。我們可以進一步進行驗證:英文的unicode是以單個字元顯示的,中文字元是以unicode顯示的,如下:
這裡寫圖片描述

第二個方面:對字串的操作

>>> "hello"  
>>>'hello'           
>>> u"hello".encode('gbk')  
>>>'hello'
>>> "你好"  
>>>'\xc4\xe3\xba\xc3'  
>>> u"你好".encode('gbk')   
>>>'\xc4\xe3\xba\xc3'

我們是要對字串編碼為位元組碼,指定的編碼方式是gbk。
第一行當你敲下字串的時候,實質上,你已經將該字串按照gbk進行了encode(這句話實質上不嚴謹,後面解釋,暫且這麼說),從而變成了位元組碼;它與第一行後部分的顯示地編碼做的是同樣的工作。根據gb2312碼的編碼規則(gbk是gb2312基礎上的擴充套件),無論中文字元還是英文字元,每個字元都被編成兩個位元組。按照這個說法,u“hello”應該是被編成了十個位元組,即>>> u”hello”.encode(‘gbk’)的結果應該是“\x–\x–\x–\x–\x–\x–\x–\x–\x–\x–”這種形式,而非顯示”hello”。實質上,這涉及到了全形和半形的區別(請自行百度全形半形)。
先丟擲幾個結論:全形半形只對英文字元有意義;即便指定的是gbk編碼,gbk也不會對“半形”的英文字元進行編碼,“半形”的英文編碼全部是由ascii編碼完成的,因此一個”半形”英文字元只會被編成一個位元組;gbk編碼只對全形英文字元編碼,每個全形字元被編碼為兩個位元組。如下:
這裡寫圖片描述

因此回過頭再來理解第一行發生的事,實質上是對英文半形字串進行的ascii的編碼,而非gbk的編碼。跟第一個方面的解釋一樣,python內部儲存和外部顯示是不一樣的,在0-127範圍之內顯示都是字元本身,在0-127之外顯示的是編碼後的位元組串。只是因為是英文,在0-127範圍之內,按照原字元進行顯示(見之前解釋)。
同理第二行也是進行了編碼,因為“你好”這兩個中文字元都不在0-127範圍之內,故只能按照編碼之後的位元組碼’\xc4\xe3\xba\xc3’進行顯示。

特別總結一下0-127範圍內的字元:
在0-127範圍內的字元,即ascii 集合內的字元。若沒有經過編碼(即為字元本身或unicode字元),則在python內部是以unicode儲存,以字元本身進行顯示。若該字元經過編碼,無論什麼型別的編碼都與ascii的編碼方式完全一樣,都會按照原字元進行顯示。進一步,無論它有沒有經過編碼,無論它經過何種型別的編碼。0—127範圍內的位元組碼和字元碼的都可以認為是同一個東西,

那我們在日常的編碼過程中,在與外界檔案互動等的過程中,應該要注意哪些方面呢。有以下幾點需要注意:
首先一定一定要規範編碼,統一編碼規範,引用“http://www.mamicode.com/info-detail-308445.html”中的說法,我們可以把python解析器當成一個水池子,有個入口,有個出口。入口處,全部轉成unicode, 池裡全部使用unicode處理;出口處,再轉成目標編碼(當然,有例外,處理邏輯中要用到具體編碼的情況)

在池子內部(即在python編輯器中)
我們要主動設定defaultencoding(預設為ascii),一般設為’utf8’,如下:
import sys
reload(sys)
sys.setdefaultencoding(‘utf-8’)

對於英文字串(準確地說是在0—127範圍內的字元),若沒有經過編碼(即字串前有無u,類似於u”–”),其unicode碼值與ascii碼值是一樣的。若編碼了(類似於“–” 或u“–“.encode(“utf8”)),無論指定的是什麼編碼,它都是按照ascii編碼的,那麼編碼值最終反映的也是ascii碼值。因此,英文字串來講,無所謂是否指定為unicode編碼,也無所謂指定的是什麼編碼,最終反應的都是ascii碼(碼值一樣)。

對於中文字串(準確地說是在0—127之外的字元),就必須要明確我們到底要處理的是字串,還是編碼之後的位元組串。若是字串,我們必須要指定它是unicode的字串,即前面要加上u,如u”你好”;若是我們想要編碼之後的位元組串,就必須要明確指定有效的位元組串,如u”你好”.encoding(“utf8”)。

按照我們前面講的原則,在python內部,我們最好是把所有涉及字串的地方,都在前面加上u,特別是串內出現了中文的情況。除非你確定串內只有英文字元,絕不會出現0—127範圍外的字元。

 在池子外部(即在python編輯器中)
在“輸入”到池子時,全部轉成unicode,一般來說是用“decode()”方法;從池子往外“輸出”時,轉成目標編碼,一般用“encode()”方法。也就是說在池子內部,儘量都保證是unicode字元碼,在池子外部都轉為位元組碼。

輸入:
a) 文字檔案時。實質上讀入的是位元組流,因此在進入“池子”之前,需要把這些位元組流統一解碼為unicode碼。
如:

# 使用codecs直接開unicode通道
file = codecs.open("test", "r", "utf-8")  #位元組碼進python前先按”utf8”解碼成unicode
for i in file:
             print type(i)    # i的型別是unicode的

b) 網頁時。來自於http://python.jobbole.com/81244/ 的例子
拿http客戶端庫中的requests庫來講,其中的Request物件在訪問伺服器後會返回一個Response物件,這個物件將返回的Http響應位元組碼儲存到content屬性中。但是如果你訪問另一個屬性text時,會返回一個unicode物件,亂碼問題就會常常發成在這裡。所以要麼你直接使用content(位元組碼),要麼記得把encoding設定正確,比如我獲取了一段gbk編碼的網頁,就需要以下方法才能得到正確的unicode。

import requests
url = "http://xxx.xxx.xxx"
response = requests.get(url)
response.encoding = 'gbk'
print response.text

輸出:
a) 輸出到shell。在將字串在shell中列印時,實質上是直接將位元組流輸出到shell中的。因此需要將池子中的unicode碼按照shell中編碼格式編碼為shell位元組串。如果編成其他碼,則可能會導致亂碼。
b) Write到檔案時。

# -*- coding: gb2312 -*- 
s = "你好" 
u = s.decode('gbk') 
f = open('text.txt','w') 
f.write(u)  # 出錯! 
f.write(u.encode('gbk')) # 這樣才行 

出錯的原因很簡單,你想輸出的是“字元”,而不是“位元組”。前面說過,“字元”是抽象的,你是沒有辦法把一個抽象的東西寫到檔案裡去的。當然,如果字串內是碼值是0-127範圍內的字元,是可以成功的(見前面的解釋),但是不推薦。最地道的寫法是字串(無論字串碼值是否在0—127範圍內)在寫進檔案之前,統統編碼為位元組串。

二, string_escape 和 unicode_escape
1)string_escape 和 unicode_escape是python特有的兩種編碼。官方文件的說明:
string_escape :Produce[s] a string that is suitable as string literal in Python source code
unicode_escape:Produce[s] a string that is suitable as Unicode literal in Python source code
中文翻譯,在python原始碼中生成一個適合作為unicode字面上的字串。
對於英語渣渣來講:string認識,literal也認識,連一起的string literal就不認識了(unicode literal也是一樣)。
關鍵是這裡的string literal 和 unicode literal該怎麼理解?

>>> "你好".encode('string_escape')
'\\xc4\\xe3\\xba\\xc3'
>>> '\xe6\x88\x91'.encode('string_escape')
'\\xe6\\x88\\x91'
>>> "hello".encode('string_escape')
'hello'

Unicode_escape:

>>> u"你好".encode('unicode-escape')
'\\u4f60\\u597d'
>>> u"\u4f60\u597d".encode('unicode-escape')
'\\u4f60\\u597d'
>>> u'hello'.encode('unicode-escape')
'hello'

我們知道,

>>> u“你好”.encode(‘gbk’)                   
>>>'\xc4\xe3\xba\xc3' 
>>> '\xc4\xe3\xba\xc3'.encode('string_escape')  
>>>'\\xc4\\xe3\\xba\\xc3'

>>>u'\u4f60\u597d'
>>> u'\u4f60\u597d'.encode('unicode_escape') 
>>>'\\u4f60\\u597d'

也就是說string_escape 是把在字串編碼後的每個位元組前面加上轉義字元 ’\’ ,這個轉義字元就是string_escape 的 “escape(轉義)” 的意思。同理unicode_escape也是可以類似理解。總結來說,就是把在python內“儲存”的位元組碼或字元碼(unicode) 顯式地顯示出來,在顯示時,原來位元組串中有一個 ‘\’,再將其轉義之後,就在原來基礎上多加了一個 ‘\’。

這樣做的意義在哪?為什麼要這樣做?下面談談我的理解。
再次強調一遍:在ASCII字符集(0–127)範圍內的字元,可以認為字元和位元組就是同一個東西,它們“融合了”!無論經過怎樣的encode,decode,最後的結果都是ASCII字元本身,所以也可以認為它沒有了encode decode的概念。ASCII字符集內的字元在整個python世界中都是暢通無阻的!!!容易出問題的就是0—127範圍之外的字元!
回到這裡,我們發現,encode之後,是生成了一個全新的字串(也是位元組串)。對python而言,我不去想它代表的是什麼,它就是一個字串而已(’\xc4\xe3\xba\xc3’ 或 ‘\u4f60\u597d’),這個串內的所有字元的碼值都在0–127範圍內。通過這樣的處理,我們驚奇地發現存這樣處理給我們帶來了極大的便利:首先原始字串(無論字元是不是在0-127範圍內)的資訊也沒有丟失,只是換了一種表示方式;同時原始字串被統一轉化為0—127範圍內的字元。還記得我們前面強調過的嗎,在ASCII字符集內的字元,對python世界而言是同一個東西。因此在python與輸入輸出進行互動的時候,就避免了頻繁的decode,encode。不過這樣處理的後果是人可能看不懂,想想print出來的是’\xc4\xe3\xba\xc3’,或者輸出到檔案中的是’\u4f60\u597d’這種鬼東西,雖然資訊沒有丟失,但是我們人看不懂。因此為了人能夠看懂,我們可能要對這些字串還原成它本來的意思,於是就需要對我們就可能就需要各種encode,decode了。
解釋到這裡,你應該明白了,string_escape和unicode_escape的作用就是把原始字串(無論中文還是英文)統一成ASCII字符集的字串,這樣方便在整個python世界中(機器世界)進行處理、傳輸。為了方便我們人類世界看懂,才會最終使用各種encode,decode來將這些ASCII字符集的字串還原成我們它本來的樣子。

三, 那看到一個編碼後的串,我們如何從字面上看它是什麼編碼?

1)沒有辦法準確地判斷一個字串的編碼方式,例如gbk的“\aa”代表甲,utf-8的“\aa”代表乙,如果給定“\aa”怎麼判斷是哪種編碼?它既可以是gbk也可以是utf-8
2)我們能做的是粗略地判斷一個字串的編碼方式,因為上面的例如的情況是很少的,更多的情況是gbk中的’\aa’代表甲,utf-8中是亂碼,例如�,這樣我們就能判斷’\aa’是gbk編碼,因為如果用utf-8編碼去解碼的結果是沒有意義的
3)而我們經常遇到的編碼其實主要的就只有三種:utf-8,gbk,unicode
• unicode一般是 \u 帶頭的,然後後面跟四位數字或字串,例如 \u6d4b\u8bd5 ,一個 \u 對應一個漢字
• utf-8一般是 \x 帶頭的,後面跟兩位字母或數字,例如 \xe6\xb5\x8b\xe8\xaf\x95\xe5\x95\x8a ,三個 \x 代表一個漢字
• gbk一般是 \x 帶頭的,後面跟兩位字母或數字,例如 \xb2\xe2\xca\xd4\xb0\xa1 ,兩個個 \x 代表一個漢字

4)使用chardet模組來判斷

import chardet
raw = u'我是一隻小小鳥'
print chardet.detect(raw.encode('utf-8'))
print chardet.detect(raw.encode('gbk'))

*輸出:
{‘confidence’: 0.99, ‘encoding’: ‘utf-8’}
{‘confidence’: 0.99, ‘encoding’: ‘GB2312’}
chardet模組可以計算這個字串是某個編碼的概率,基本對於99%的應用場景,這個模組都夠用了。*

四,典型例子彙總:
1, 關於encode decode 的常見錯誤(一般都是編碼和解碼不一致導致的)
1) 最常見的一種錯誤,我們對已經編碼過後的位元組串再次使用encode,這種實質上有一個“隱式”的解碼動作,即隱式地將位元組串解碼為unicode字串(解碼型別是sys.getdefaultencoding(),一般為ascii ),然後才再encode。整個過程如下:
這裡寫圖片描述

在python2.x下,最好的解決辦法是保證顯式隱式的編碼型別一致。

1) 筆者在某個專案中有這麼一段資料:
“{\x22provinceName\x22:\x22\xE5\x8C\x97\xE4\xBA\xAC\xE5\xB8\x82\x22}”
我們如何解析它,首先,明面上傳遞”\x–” 這樣的字串,說明是經過“string_escape”編碼過的。“\x22”是英文字元的雙引號,其他 “\x–” 不知道是什麼編碼。我們假設是最常用的utf8編碼,那試著解析一下,解析的過程如下:
這裡寫圖片描述
Bingo! 成功

2) 網上摘取的一段字串(https://www.v2ex.com/t/174215),如下:
“u’\xe6\x97\xa0\xe5\x90\x8d’”
首先字串前的u表示它是unicode字串,但串內又不是 “\u—-”, 而是”\x–”,試著解析一下
這裡寫圖片描述
成功