Python編碼和Unicode

jobbole發表於2013-11-25

  我確定有很多關於Unicode和Python的說明,但為了方便自己的理解使用,我還是打算再寫一些關於它們的東西。

  位元組流 vs Unicode物件

  我們先來用Python定義一個字串。當你使用string型別時,實際上會儲存一個位元組串。

[  a ][  b ][  c ] = "abc"
[ 97 ][ 98 ][ 99 ] = "abc"

  在這個例子裡,abc這個字串是一個位元組串。97.,98,,99是ASCII碼。在Python 2.x裡定義就是將所有的字串當做ASCII來對待。不幸的是,ASCII在拉丁式字符集裡是最不常見的標準。

  ASCII是用前127個數字來做字元對映。像windows-1252和UTF-8這樣的字元對映有相同的前127個字元。在你的字串裡每個位元組的值低於127的時候是安全的混合字串編碼。然而作這個假設是件很危險的事情,下面還將會提到。

  當你的字串裡有位元組的值大於126的時候就會有問題冒出來。我們來看一個用windows-1252編碼的字串。Windows-1252裡的字元對映是8位的字元對映,那麼總共就會有256個字元。前127個跟ASCII是一樣的,接下來的127個是由windows-1252定義的其他字元。

A windows-1252 encoded string looks like this:
[ 97 ] [ 98 ] [ 99 ] [ 150 ] = "abc–"

  Windows-1252仍然是一個位元組串,但你有沒有看到最後一個位元組的值是大於126的。如果Python試著用預設的ASCII標準來解碼這個位元組流,它就會報錯。我們來看當Python解碼這個字串的時候會發生什麼:

>>> x = "abc" + chr(150)
>>> print repr(x)
'abc\x96'
>>> u"Hello" + x
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
UnicodeDecodeError: 'ASCII' codec can't decode byte 0x96 in position 3: ordinal not in range(128)

  我們來用UTF-8來編碼另一個字串:

A UTF-8 encoded string looks like this:
[ 97 ] [ 98 ] [ 99 ] [ 226 ] [ 128 ] [ 147 ] = "abc–"
[0x61] [0x62] [0x63] [0xe2]  [ 0x80] [ 0x93] = "abc-"

  如果你拿起看你熟悉的Unicode編碼表,你會發現英文的破折號對應的Unicode編碼點為8211(0×2013)。這個值大於ASCII最大值127。大於一個位元組能夠儲存的值。因為8211(0×2013)是兩個位元組,UTF-8必須利用一些技巧告訴系統儲存一個字元需要三個位元組。我們再來看當Python準備用預設的ASCII來編碼一個裡面有字元的值大於126的UTF-8編碼字串。

>>> x = "abc\xe2\x80\x93"
>>> print repr(x)
'abc\xe2\x80\x93'
>>> u"Hello" + x
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
UnicodeDecodeError: 'ASCII' codec can't decode byte 0xe2 in position 3: ordinal not in range(128)

  你可以看到,Python一直是預設使用ASCII編碼。當它處理第4個字元的時候,因為它的值為226大於126,所以Python丟擲了錯誤。這就是混合編碼所帶來的問題。

  解碼位元組流

  在一開始學習Python Unicode 的時候,解碼這個術語可能會讓人很疑惑。你可以把位元組流解碼成一個Unicode物件,把一個Unicode 物件編碼為位元組流。

  Python需要知道如何將位元組流解碼為Unicode物件。當你拿到一個位元組流,你呼叫它的“解碼方法來從它建立出一個Unicode物件。

  你最好是儘早的將位元組流解碼為Unicode。

>>> x = "abc\xe2\x80\x93"
>>> x = x.decode("utf-8")
>>> print type(x)
<type 'unicode'>
>>> y = "abc" + chr(150)
>>> y = y.decode("windows-1252")
>>> print type(y)
>>> print x + y
abc–abc–

  將Unicode編碼為位元組流

  Unicode物件是一個文字的編碼不可知論的代表。你不能簡單地輸出一個Unicode物件。它必須在輸出前被變成一個位元組串。Python會很適合做這樣的工作,儘管Python將Unicode編碼為位元組流時預設是適用ASCII,這個預設的行為會成為很多讓人頭疼的問題的原因。

>>> u = u"abc\u2013"
>>> print u
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character u'\u2013' in position 3: ordinal not in range(128)
>>> print u.encode("utf-8")
abc–

  使用codecs模組

  codecs模組能在處理位元組流的時候提供很大幫助。你可以用定義的編碼來開啟檔案並且你從檔案裡讀取的內容會被自動轉化為Unicode物件。

  試試這個:

>>> import codecs
>>> fh = codecs.open("/tmp/utf-8.txt", "w", "utf-8")
>>> fh.write(u"\u2013")
>>> fh.close()

  它所做的就是拿到一個Unicode物件然後將它以utf-8編碼寫入到檔案。你也可以在其他的情況下這麼使用它。

  試試這個:

  當從一個檔案讀取資料的時候,codecs.open 會建立一個檔案物件能夠自動將utf-8編碼檔案轉化為一個Unicode物件。

  我們接著上面的例子,這次使用urllib流。

>>> stream = urllib.urlopen("http://www.google.com")
>>> Reader = codecs.getreader("utf-8")
>>> fh = Reader(stream)
>>> type(fh.read(1))
<type 'unicode'>
>>> Reader
<class encodings.utf_8.StreamReader at 0xa6f890>

  單行版本:

>>> fh = codecs.getreader("utf-8")(urllib.urlopen("http://www.google.com"))
>>> type(fh.read(1))

  你必須對codecs模組十分小心。你傳進去的東西必須是一個Unicode物件,否則它會自動將位元組流作為ASCII進行解碼。

>>> x = "abc\xe2\x80\x93" # our "abc-" utf-8 string
>>> fh = codecs.open("/tmp/foo.txt", "w", "utf-8")
>>> fh.write(x)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python2.5/codecs.py", line 638, in write
  return self.writer.write(data)
File "/usr/lib/python2.5/codecs.py", line 303, in write
  data, consumed = self.encode(object, self.errors)
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe2 in position 3: ordinal not in range(128)

  哎呦我去,Python又開始用ASCII來解碼一切了。

  將UTF-8位元組流切片的問題

  因為一個UTF-8編碼串是一個位元組列表,len( )和切片操作無法正常工作。首先用我們之前用的字串。

[ 97 ] [ 98 ] [ 99 ] [ 226 ] [ 128 ] [ 147 ] = "abc–"

  接下來做以下的:

>>> my_utf8 = "abc–"
>>> print len(my_utf8)
6

  神馬?它看起來是4個字元,但是len的結果說是6。因為len計算的是位元組數而不是字元數。

>>> print repr(my_utf8)
'abc\xe2\x80\x93'

  現在我們來切分這個字串。

>>> my_utf8[-1] # Get the last char
'\x93'

  我去,切分結果是最後一位元組,不是最後一個字元。

  為了正確的切分UTF-8,你最好是解碼位元組流建立一個Unicode物件。然後就能安全的操作和計數了。

>>> my_unicode = my_utf8.decode("utf-8")
>>> print repr(my_unicode)
u'abc\u2013'
>>> print len(my_unicode)
4
>>> print my_unicode[-1]
–

  當Python自動地編碼/解碼

  在一些情況下,當Python自動地使用ASCII進行編碼/解碼的時候會丟擲錯誤。

  第一個案例是當它試著將Unicode和位元組串合併在一起的時候。

>>> u"" + u"\u2019".encode("utf-8")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe2 in position 0:   ordinal not in range(128)

  在合併列表的時候會發生同樣的情況。Python在列表裡有string和Unicode物件的時候會自動地將位元組串解碼為Unicode。

>>> ",".join([u"This string\u2019s unicode", u"This string\u2019s utf-8".encode("utf-8")])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe2 in position 11:  ordinal not in range(128)

  或者當試著格式化一個位元組串的時候:

>>> "%s\n%s" % (u"This string\u2019s unicode", u"This string\u2019s  utf-8".encode("utf-8"),)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe2 in position 11: ordinal not in range(128)

  基本上當你把Unicode和位元組串混在一起用的時候,就會導致出錯。

  在這個例子裡面,你建立一個utf-8檔案,然後往裡面新增一些Unicode物件的文字。就會報UnicodeDecodeError錯誤。

>>> buffer = []
>>> fh = open("utf-8-sample.txt")
>>> buffer.append(fh.read())
>>> fh.close()
>>> buffer.append(u"This string\u2019s unicode")
>>> print repr(buffer)
['This file\xe2\x80\x99s got utf-8 in it\n', u'This string\u2019s unicode']
>>> print "\n".join(buffer)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe2 in position 9: ordinal not in range(128)

  你可以使用codecs模組把檔案作為Unicode載入來解決這個問題。

>>> import codecs
>>> buffer = []
>>> fh = open("utf-8-sample.txt", "r", "utf-8")
>>> buffer.append(fh.read())
>>> fh.close()
>>> print repr(buffer)
[u'This file\u2019s got utf-8 in it\n', u'This string\u2019s unicode']
>>> buffer.append(u"This string\u2019s unicode")
>>> print "\n".join(buffer)
This file’s got utf-8 in it

This string’s unicode

  正如你看到的,由codecs.open 建立的流在當資料被讀取的時候自動地將位元串轉化為Unicode。

  最佳實踐

  1.最先解碼,最後編碼

  2.預設使用utf-8編碼

  3.使用codecs和Unicode物件來簡化處理

  最先解碼意味著無論何時有位元組流輸入,需要儘早將輸入解碼為Unicode。這會防止出現len( )和切分utf-8位元組流發生問題。

  最後編碼意味著只有在準備輸入的時候才進行編碼。這個輸出可能是一個檔案,一個資料庫,一個socket等等。只有在處理完成之後才編碼unicode物件。最後編碼也意味著,不要讓Python為你編碼Unicode物件。Python將會使用ASCII編碼,你的程式會崩潰。

  預設使用UTF-8編碼意味著:因為UTF-8可以處理任何Unicode字元,所以你最好用它來替代windows-1252和ASCII。

  codecs模組能夠讓我們在處理諸如檔案或socket這樣的流的時候能少踩一些坑。如果沒有codecs提供的這個工具,你就必須將檔案內容讀取為位元組流,然後將這個位元組流解碼為Unicode物件。

  codecs模組能夠讓你快速的將位元組流轉化為Unicode物件,省去很多麻煩。

  解釋UTF-8

  最後的部分是讓你能入門UTF-8,如果你是個超級極客可以無視這一段。

  利用UTF-8,任何在127和255之間的位元組是特別的。這些位元組告訴系統這些位元組是多位元組序列的一部分。

Our UTF-8 encoded string looks like this:
[ 97 ] [ 98 ] [ 99 ] [ 226 ] [ 128 ] [ 147 ] = "abc–"

  最後3位元組是一個UTF-8多位元組序列。如果你把這三個位元組裡的第一個轉化為2進位制可以看到以下的結果:

11100010

  前3位元告訴系統它開始了一個3位元組序列226,128,147。

  那麼完整的位元組序列。

11100010 10000000 10010011

  然後你運用三位元組序列的下面的掩碼。

1110xxxx 10xxxxxx 10xxxxxx
XXXX0010 XX000000 XX010011 Remove the X's
0010       000000   010011 Collapse the numbers
00100000 00010011          Get Unicode number 0x2013, 8211 The "–"

  這是基本的UTF-8入門,如果想知道更多的細節,可以去看UTF-8的維基頁面。

  原文連結: ERIC MORITZ   翻譯: 伯樂線上 - 賤聖OMG

相關文章