1. 程式人生 > >Python深入:編碼問題總結

Python深入:編碼問題總結

一:字元編碼簡介

         1:ASCII

最初的計算機的使用是在美國,所用到的字元也就是現在鍵盤上的一些符號和少數兒個特殊的符號,一個位元組所就能足以容納所有的這些字元,實際上表示這些字元的位元組最高位都為0,也就是說這些位元組都在0到127之間,如字元a對應數字97。這套編碼規則被稱為ASCII(美國標準資訊交換碼)

2:GBK、GB2312

隨著計算機的應用和普及,許多國家都把本地的字符集引入了計算機,大大擴充套件了計算機中字元的範圍。以中文為例,一個位元組是不能容納所有的中文漢字的,因此大陸將每一箇中文字元都用兩個位元組的數字來表示,原有的ASCII字元的編碼保持不變,仍用一個位元組表示,為了將一箇中文字元與兩個ASCII碼字元相區別,中文字元的每個位元組的最高位都為1,這套編碼規則稱為GBK(

國標碼),後來,又在GBK的基礎上對更多的中文字元(包括繁體)進行了編碼,新的編碼系統就是GB2312,可見GBK是GB2312的子集。

3:Unicode

每個國家和地區都制定了一套自己的編碼,那麼同樣的一個位元組,在不同的國家和地區就代表了不同的字元。比如,130在法語編碼中代表了é,在希伯來語編碼中卻代表了字母Gimel (ג),在俄語編碼中又會代表另一個符號。

為了解決各個國家和地區使用本地化字元編碼帶來的不利影響,將全世界所有的符號進行了統一編碼,稱之為Unicode編碼。這是一種所有符號的編碼。如 “中”這個符號,在全世界的任何角落始終對應的都是一個十六進位制的數字4e2d。

最初的Unicode標準UCS-2使用兩個位元組表示一個字元,所以你常常可以聽到Unicode使用兩個位元組表示一個字元的說法。但過了不久有人覺得256*256太少了,還是不夠用,於是出現了UCS-4標準,它使用4個位元組表示一個字元,不過我們用的最多的仍然是UCS-2。

4:UTF

注意,UCS(Unicode Character Set)還僅僅是字元對應碼位的一張表而已,比如"中"這個字的碼位是4E2D。字元具體如何傳輸和儲存則是由UTF(UCS Transformation Format)來負責。

一開始這事很簡單,直接使用UCS的碼位來儲存,這就是UTF-16,比如,"漢"直接使用\x4E\x2D儲存(UTF-16-BE),或是倒過來使用\x2D\x4E儲存(UTF-16-LE)。

這裡就有兩個嚴重的問題,第一個問題是,如何才能區別Unicode和ASCII?計算機怎麼知道兩個位元組表示一個符號,而不是分別表示兩個符號呢?第二個問題是,英文字母只用一個位元組表示就夠了,如果Unicode統一規定,每個符號用兩個位元組表示,這對於儲存來說是極大的浪費。於是UTF-8橫空出世。

5:UTF-8

UTF-8是使用最廣的一種Unicode的實現方式。其他實現方式還包括UTF-16(字元用兩個位元組或四個位元組表示)和UTF-32(字元用四個位元組表示),不過在網際網路上基本不用。重複一遍,這裡的關係是,UTF-8Unicode的實現方式之一

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編碼方式

00 00 00 00   -   00 00 00 7F

 0xxxxxxx

00 00 00 80   -   00 00 07 FF

 110xxxxx 10xxxxxx

00 00 08 00   -   00 00 FF FF

 1110xxxx 10xxxxxx 10xxxxxx

00 01 00 00   -   00 10 FF FF

 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

跟據上表,解讀UTF-8編碼非常簡單。如果一個位元組的第一位是0,則這個位元組單獨就是一個字元;如果第一位是1,則連續有多少個1,就表示當前字元佔用多少個位元組。

下面以漢字"嚴"為例,演示如何實現UTF-8編碼。

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

6:BOM

BOM(Byte Order Mark)。UTF引入了BOM來表示自身編碼,如果一開始讀入的幾個位元組是其中之一,則代表接下來要讀取的文字使用的編碼是相應的編碼:

BOM_UTF8             '\xef\xbb\xbf' 

BOM_UTF16_LE    '\xff\xfe' 

BOM_UTF16_BE    '\xfe\xff'

如果一個文字檔案的頭兩個位元組是FE FF,就表示該檔案採用大頭方式;如果頭兩個位元組是FF FE,就表示該檔案採用小頭方式。以漢字"嚴"為例,Unicode碼是4E25,需要用兩個位元組儲存,一個位元組是4E,另一個位元組是25。儲存的時候,4E在前,25在後,就是Big endian方式;25在前,4E在後,就是Little endian方式。

         一般情況下,不建議在Linux中使用BOM。

7:Unicode與UTF-8之間的轉換

"嚴"的Unicode碼是4E25,UTF-8編碼是E4B8A5,以該漢字為例,利用Ultraedit檢視各種編碼的具體值,一般的編輯器都支援以不同的編碼方式儲存文字,編碼方式如下:

1)ANSI是預設的編碼方式。對於英文檔案是ASCII編碼,對於簡體中文檔案是GB2312編碼(只針對Windows簡體中文版,如果是繁體中文版會採用Big5碼)。這種方式編碼的檔案,就是兩個位元組"D1 CF",這正是"嚴"的GB2312編碼:

2)UTF16編碼指的是UCS-2編碼方式,即直接用兩個位元組存入字元的Unicode碼。編碼是四個位元組"FF FE 25 4E",其中"FF FE"表明是小頭方式儲存:

3)UTF16-NOBOM編碼與上一個選項相對應,只不過沒有BOM編碼,也就是檔案中只儲存UTF16編碼"25 4E":

4)UTF-8編碼,檔案中共6個位元組:"EF BB BF E4 B8 A5",其中前三個為BOM編碼,後三個為UTF8編碼:

6)UTF8-NOBOM編碼與上一個選項相對應,只不過沒有BOM編碼,也就是檔案中只儲存UTF8編碼" E4 B8 A5":

二:Python程式碼檔案的編碼

         Python直譯器會使用某種編碼方式來解釋Python原始碼檔案,預設情況下,這種編碼方式就是ASCII。

         Python2.1版本,在Python原始碼檔案中,只能以以基於Latin-1的“轉義unicode”的方式來書寫Unicode字元,這對於亞洲的程式設計師是很不友好的。解決該問題的方法是,在原始碼檔案的頂部,使用某種特殊的註釋方式來表明原始碼檔案的編碼。

         為了表明原始碼檔案的編碼,這種特殊的註釋必須位於原始碼檔案的第一行或第二行,類似於:

#coding=<encodingname>  

或:

#!/usr/bin/python
# -*- coding:<encoding name> -*-

或:

#!/usr/bin/python
# vim: setfileencoding=<encoding name> :
這種方式的本質是:檔案的第一行或第二行必須能匹配正則表示式:

” coding[:=]\s*([-\w.]+)”,該表示式中的group1就會被解釋為編碼名稱,如果Python無法識別該編碼,則在編譯時就會報錯。

         像Windows這樣的平臺,會在Unicode檔案的最開始加上BOM位元組碼,UTF-8檔案的位元組碼是:”xef\xbb\xbf”。為了相容這種方式,包含這種位元組碼的檔案,即使沒有位元組編碼註釋,也會被解釋為”utf-8”。如果一個原始碼檔案,既有編碼註釋,又有UTF-8BOM位元組碼,則在編碼註釋中的編碼名稱只能是”utf-8””utf8”都不行),否則會報錯:

“SyntaxError: encodingproblem: utf-8”

下面是一些使用編碼註釋的例子:

1. Emacs風格的檔案編碼註釋:

         #!/usr/bin/python
         # -*- coding: latin-1 -*-
         import os, sys
         ...

         #!/usr/bin/python
         # -*- coding: iso-8859-15 -*-
         import os, sys
         ...


         #!/usr/bin/python
         # -*- coding: ascii -*-
         import os, sys
         ...

2. 使用純文字方式的註釋:

         # This Python file uses the following encoding:utf-8
         import os, sys
         ...

3. 沒有編碼註釋,則Python直譯器預設使用ASCII。

如果沒宣告編碼,但是檔案中又包含非ASCII編碼的字元的話,python解析器去解析的python檔案,就會報錯。

         #!/usr/local/bin/python
         import os, sys
         ...

4. 下面這些語法不起作用

沒有加”coding:”字首:

         #!/usr/local/bin/python
         # latin-1
         import os, sys
         ...

編碼方式不在第一行或第二行:

         #!/usr/local/bin/python
         #
         # -*- coding: latin-1 -*-
         import os, sys
         ...

不支援的編碼方式:

         #!/usr/local/bin/python
         # -*- coding: utf-42 -*-
         import os, sys
         ...

5:注意:

         允許的編碼方式包括ASCII相容編碼以及某些多位元組編碼,比如SHIFT_JIS。但不包括為所有字元都是有雙位元組或者更多位元組的編碼,比如UTF-16(注:也就是通常說的Unicode,但SHIFT_JIS也好,GBK也好,因為相容ASCII編碼,所以都可以在Python原始檔裡使用)。

         如果宣告的編碼與實際不符(就是說,檔案實際上是以另外的編碼儲存的),出錯的可能性很大。

         檔案的編碼格式決定了在該原始檔中宣告的字串的編碼,str = '哈哈'

print repr(str)

a.如果檔案格式為utf-8,則str的值為:'\xe5\x93\x88\xe5\x93\x88'(哈哈的utf-8編碼)

b.如果檔案格式為gbk,則str的值為:'\xb9\xfe\xb9\xfe'(哈哈的gbk編碼)

三:str和unicode

str和unicode都是basestring的子類。編碼是指unicode-->str,解碼是指str-->unicode。

str是一個位元組陣列,這個位元組陣列表示的是對unicode物件編碼(可以是utf-8、gbk、cp936、GB2312)後的儲存的格式。這裡它僅僅是一個位元組流,沒有其它的含義,如果你想使這個位元組流顯示的內容有意義,就必須用正確的編碼格式,解碼顯示。 對UTF-8編碼的str'哈哈'使用len()函式時,結果是6,因為實際上,UTF-8編碼的'哈哈' == '\x e5\x93\x88\xe5\x93\x88'。

unicode才是真正意義上的字串,對位元組串str使用正確的字元編碼進行解碼後獲得,例如'哈哈'的unicode物件為 u'\u54c8\u54c8' ,len(u”哈哈”) == 2

字串在Python內部的表示是unicode編碼,因此,在做編碼轉換時,通常需要以unicode作為中間編碼,即先將其他編碼的字串解碼(decode)成unicode,再從unicode編碼(encode)成另一種編碼。 

decode的作用是將其他編碼的字串轉換成unicode編碼,如str1.decode('gb2312'),表示將gb2312編碼的字串str1轉換成unicode編碼。

encode的作用是將unicode編碼轉換成其他編碼的字串,如str2.encode('gb2312'),表示將unicode編碼的字串str2轉換成gb2312編碼。 

因此,轉碼的時候一定要先搞明白,字串str是什麼編碼,然後decode成unicode,然後再encode成其他編碼

str.decode([encoding[, errors]])

         使用encoding指示的編碼,對str進行解碼,返回一個unicode物件。預設情況下encoding是“字串預設編碼”,比如ascii。

errors指示如何處理解碼錯誤,預設情況下是”strict”,也就是遇到解碼錯誤時,直接丟擲UnicodeError異常。其他的errors值可以有”ignore”,”replace”等。

unicode(object[, encoding[, errors]])

         str.decode作用相同,但是該方法要快一些:http://stackoverflow.com/questions/440320/unicode-vs-str-decode-for-a-utf8-encoded-byte-string-python-2-x

str.encode([encoding[, errors]])

         返回一個經encoding編碼後的str物件,預設的encoding是”預設字串編碼”。

errors指示如何處理解碼錯誤,預設情況下是”strict”,也就是遇到解碼錯誤時,直接丟擲UnicodeError異常。其他的errors值可以有”ignore”,”replace”,'xmlcharrefreplace', 'backslashreplace'等。

一般情況下,對Unicode物件進行編碼encode(),返回str物件。對str物件呼叫解碼decode(),返回unicode物件。

但是對str呼叫encode也不會報錯,str.encode()實際上就等價於str.decode(sys.defaultencoding).encode().而sys.defaultencoding一般是ascii。

同樣的,對unicode物件進行解碼unicode.decode實際上就等價於unicode.encode(sys.defaultencoding).decode()

四:示例

         英文字元的ASCII、UTF-8、GBK等編碼都是一樣的,因此下面的示例僅討論漢字的情況。並且保證原始碼檔案的實際編碼方式,和編碼註釋是一樣的。

1:檔案為預設編碼(ASCII),無編碼註釋

astr ="哈哈"

執行檔案,報錯:SyntaxError: Non-ASCII character '\xb9' in file 2.py on line 2, butno encoding declared; seehttp://www.python.org/peps/pep-0263.htmlfor details

原因:預設的檔案編碼為ASCII,這種編碼無法處理漢字,將檔案儲存為GBK或者UTF-8格式,並相應的註釋宣告。

2:檔案編碼為UTF-8

# -*- coding:UTF-8 -*-
def toHexString(s):
    return ":".join("{0:x}".format(ord(c))for c in s)
 
ustr =u"哈哈"
print repr(ustr)
print ustr, "len is ",len(ustr)
print
 
astr ="哈哈"
ustr =astr.decode("UTF-8")
print repr(ustr)
print ustr, "len is ",len(ustr)

         執行檔案,結果為:

u'\u54c8\u54c8'

哈哈 len is  2

u'\u54c8\u54c8'

哈哈 len is  2

         可見,上下兩種方式,其實是等價的,在原始碼檔案中直接使用u”...”編寫Unicode字串,使用檔案的註釋編碼,將該str解碼為Unicode

Python supports writing Unicode literals in anyencoding, but you have to declare the encoding being used. This is done byincluding a special comment as either the first or second line of the sourcefile。

(https://docs.python.org/2/howto/unicode.html#the-unicode-type)

3:檔案為UTF-8編碼,

# -*- coding:utf-8 -*-
 
astr ="哈哈"
print repr(astr)
print astr, "len is ",len(astr)
print
 
ustr =astr.decode("gbk")
print repr(ustr)
print ustr, "len is ",len(ustr)

 

執行檔案,結果如下:

'\xe5\x93\x88\xe5\x93\x88'

鍝堝搱 len is  6

(第一行輸出:因為檔案為UTF-8編碼,所以輸出”哈哈”的UTF-8編碼)

(第二行輸出:因為print語句它的實現是將要輸出的內容傳送給終端,終端會根據預設的編碼(GBK)對輸入的位元組流進行解釋,因為 '\xe5\x93\x88\xe5\x93\x88'用GBK去解釋,其顯示的出來就是“鍝堝搱”,而且,該str的長度就是編碼的長度,為6)

u'\u935d\u581d\u6431'

鍝堝搱 len is  3

(第一行輸出:因為檔案是UTF-8編碼,但是卻用GBK對“哈哈”,也就是'\xe5\x93\x88\xe5\x93\x88'進行解碼,所以輸出錯誤)

(第二行輸出:Python在向控制檯輸出unicode物件的時候會自動根據輸出環境的編碼(GBK)進行轉換,而如果輸出的不是unicode物件而是普通的str字串,則會直接按照該字串的編碼輸出字串(http://noalgo.info/578.html)。所以,printustr就等價於print astr了。如果將ustr = astr.decode("gbk") 替換成 ustr =astr.decode("UTF-8"),則輸出:

u'\u54c8\u54c8'

哈哈 len is  2

54c8:54c8

這是因為將astr按照檔案編碼註釋宣告的編碼方式進行解碼,解碼成Unicode之後,print時又自動進行GBK編碼,所以會輸出正確的字元)

4:開啟具有漢字的檔案:

檔名可以包含Unicode字元,作業系統在處理這樣的檔案時,將Unicode字元根據某種編碼進行處理,比如Max OS X使用UTF-8編碼,而Windows上的編碼是可配置的,用”mbcs”來表示當前的編碼。

         sys.getfilesystemencoding()函式可以得到當前檔案系統使用的編碼,但是其實可以不用管這些,當需要開啟一個包含漢字的檔案時,直接使用Unicode字元即可,這樣它會自動轉換為正確的編碼字串:

# -*- coding:UTF-8 -*-
 
#astr = "哈哈.txt".decode("UTF-8")
#astr = u"哈哈.txt"
astr ="哈哈.txt".decode("UTF-8").encode("GBK")
try:
    with open(astr) asfobj:
        print fobj.read()
except Exception, e:
    print "error is ",e

         上面這三種方式,都可以正確開啟該檔案。

參考:

http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html

http://www.jb51.net/article/17560.htm