字節問題
“字符串”是個相當簡單的概念:一個字符串是一個字符序列。問題出 在“字符”的定義上。
1.Unicode 標準把字符的標識和具體的字節表述進行了如下的明確區分 字符的標識,即碼位,是 0~1 114 111 的數字(十進制),在Unicode 標準中以 4~6 個十六進制數字表示,而且加前綴“U+”。例 如,字母 A 的碼位是 U+0041
**2.字符的具體表述取決於所用的編碼。編碼是在碼位和字節序列之間轉換時使用的算法。**在 UTF-8 編碼中,A(U+0041)的碼位編碼成 單個字節 \x41,而在 UTF-16LE 編碼中編碼成兩個字節\x41\x00。再舉個例子,歐元符號(U+20AC)在 UTF-8 編碼中是三個字節——\xe2\x82\xac, 而在 UTF-16LE 中編碼成兩個字 節:\xac\x20。
把碼位轉換成字節序列的過程是編碼;把字節序列轉換成碼位的過程是解碼。
#編碼和解碼
>>> s = 'café'
>>> len(s) # ➊
4
>>> b = s.encode('utf8') # ➋
>>> b
b'caf\xc3\xa9' # ➌
>>> len(b) # ➍
5
>>> b.decode('utf8') # ➎
'café'
➊ 'café’字符串有4個Unicode字符。
➋ 使用UTF-8把str對象編碼成bytes對象。
➌ bytes字面量以b開頭。
➍ 字節序列b有5個字節(在UTF-8中,“é”的碼位編碼成兩個字節)。
➎ 使用UTF-8把bytes對象解碼成str對象。
總結:.decode() 和 .encode() 的區別,可以把字節序列想成晦澀難懂的機器磁芯轉儲,把 Unicode 字符串想 成“人類可讀”的文本。那麼,把字節序列變成人類可讀的文本字符串就是解碼,而把字符串變成用於存儲或傳輸的字節序列就是編碼。
字節概要
#包含5個字節的bytes和bytearray對象
>>> cafe = bytes('café', encoding='utf_8') #➊
>>> cafe
b'caf\xc3\xa9'
>>> cafe[0] #➋
99
>>> cafe[:1] #➌
b'c'
>>> cafe_arr = bytearray(cafe)
>>> cafe_arr #➍
bytearray(b'caf\xc3\xa9')
>>> cafe_arr[-1:] #➎
bytearray(b'\xa9')
➊bytes對象可以從str對象使用給定的編碼創建。
➋ 各個元素是range(256)內的整數。
➌ bytes對象的切片還是bytes對象,即使是隻有一個字節的切片。
➍ bytearray對象沒有字面量句法,而是以bytearray()和字節序列字面量參數的形式顯示。
➎ bytearray對象的切片還是bytearray對象。
基本的編解碼器
#使用3個編解碼器編碼字符串“El Niño”,得到的字節序列差異很大
>>> for codec in ['latin_1', 'utf_8', 'utf_16']:
print(codec, 'El Niño'.encode(codec), sep='\t')
latin_1 b'El Ni\xf1o'
utf_8 b'El Ni\xc3\xb1o'
utf_16 b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'
展示不同編解碼器對“A”和高音譜號等字符編碼後得到的字節序列
latin1(即iso8859_1)
一種重要的編碼,是其他編碼的基礎,例如cp1252和Unicode(注意,latin1與cp1252的字節值是一樣的,甚至連碼位也相同)。
cp1252
Microsoft制定的latin1超集,添加了有用的符號,例如彎引號和€(歐元);有些Windows應用把它稱爲“ANSI”,但它並不是ANSI標準。
cp437
IBM PC最初的字符集,包含框圖符號。與後來出現的latin1不兼容。
gb2312
用於編碼簡體中文的陳舊標準;這是亞洲語言中使用較廣泛的多字節編碼之一。
utf-8
目前Web中最常見的8位編碼;與ASCII兼容(純ASCII文本是有效的UTF-8文本)。
utf-16le
UTF-16的16位編碼方案的一種形式;所有UTF-16支持通過轉義序列(稱爲“代理對”,surrogate pair)表示超過U+FFFF的碼位。
編碼問題
雖然有個一般性的UnicodeError異常,但是報告錯誤時幾乎都會指明具體的異常:
UnicodeEncodeError(把字符串轉換成二進制序列時)或UnicodeDecodeError(把二進制序列轉換成字符串時)。
處理UnicodeEncodeError
把文本轉換成字節序列時,如果目標編碼中沒有定義某個字符,那就會拋出UnicodeEncodeError異常。
#編碼成字節序列:成功和錯誤處理
>>> city = 'São Paulo'
>>> city.encode('utf_8') #➊
b'S\xc3\xa3o Paulo'
>>> city.encode('utf_16')
b'\xff\xfeS\x00\xe3\x00o\x00 \x00P\x00a\x00u\x00l\x00o\x00'
>>> city.encode('iso8859_1') #➋
b'S\xe3o Paulo'
>>> city.encode('cp437') #➌
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/.../lib/python3.4/encodings/cp437.py", line 12, in encode
return codecs.charmap_encode(input,errors,encoding_map)
UnicodeEncodeError: 'charmap' codec can't encode character '\xe3' in
position 1: character maps to <undefined>
>>> city.encode('cp437', errors='ignore') #➍
b'So Paulo'
>>> city.encode('cp437', errors='replace') #➎
b'S?o Paulo'
>>> city.encode('cp437', errors='xmlcharrefreplace') #➏
b'São Paulo'
➊ 'utf_?‘編碼能處理任何字符串。
➋ ‘iso8859_1’編碼也能處理字符串’São Paulo’。
➌ ‘cp437’無法編碼’ã’(帶波形符的“a”)。默認的錯誤處理方式’strict’拋出Unicode-
EncodeError。
➍ error=‘ignore’處理方式悄無聲息地跳過無法編碼的字符;這樣做通常很是不妥。
➎ 編碼時指定error=‘replace’,把無法編碼的字符替換成’?’;數據損壞了,但是用戶知
道出了問題。
➏ 'xmlcharrefreplace’把無法編碼的字符替換成XML實體。
處理UnicodeDecodeError
把二進制序列轉換成文本時,如果假設是這兩個編碼中的一個,遇到無法轉換的字節序列時會拋出UnicodeDecodeError。
#把字節序列解碼成字符串:成功和錯誤處理
>>> octets = b'Montr\xe9al' #➊
>>> octets.decode('cp1252') #➋
'Montréal'
>>> octets.decode('iso8859_7') #➌
'Montrιal'
>>> octets.decode('koi8_r') #➍
'MontrИal'
>>> octets.decode('utf_8') #➎
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 5:
invalid continuation byte
>>> octets.decode('utf_8', errors='replace') #➏
'Montr�al'
➊ 這些字節序列是使用latin1編碼的“Montréal”;’\xe9’字節對應“é”。
➋ 可以使用’cp1252’(Windows 1252)解碼,因爲它是latin1的有效超集。
➌ ISO-8859-7用於編碼希臘文,因此無法正確解釋’\xe9’字節,而且沒有拋出錯誤。
➍ KOI8-R用於編碼俄文;這裏,’\xe9’表示西裏爾字母“И”。
➎ 'utf_8’編解碼器檢測到octets不是有效的UTF-8字符串,拋出UnicodeDecodeError。
➏ 使用’replace’錯誤處理方式,\xe9替換成了“�”(碼位是U+FFFD),這是官方指定
的REPLACEMENT CHARACTER(替換字符),表示未知字符。
處理文本文件
處理文本的最佳實踐是“Unicode三明治”
>>> fp = open('cafe.txt', 'w', encoding='utf_8')
>>> fp #➊
<_io.TextIOWrapper name='cafe.txt' mode='w' encoding='utf_8'>
>>> fp.write('café')
4 #➋
>>> fp.close()
>>> import os
>>> os.stat('cafe.txt').st_size
5 #➌
>>> fp2 = open('cafe.txt')
>>> fp2 #➍
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp1252'>
>>> fp2.encoding #➎
'cp1252'
>>> fp2.read()
>'café' #➏
>>> fp3 = open('cafe.txt', encoding='utf_8') #➐
>>> fp3
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='utf_8'>
>>> fp3.read()
'café' #➑
>>> fp4 = open('cafe.txt', 'rb') #➒
>>> fp4
<_io.BufferedReader name='cafe.txt'> #➓
>>> fp4.read() #⑪
b'caf\xc3\xa9'
➊ 默認情況下,open函數採用文本模式,返回一個TextIOWrapper對象。
➋ 在TextIOWrapper對象上調用write方法返回寫入的Unicode字符數。
➌ os.stat報告文件中有5個字節;UTF-8編碼的’é’佔兩個字節,0xc3和0xa9。
➍ 打開文本文件時沒有顯式指定編碼,返回一個TextIOWrapper對象,編碼是區域設置中
的默認值。
➎ TextIOWrapper對象有個encoding屬性;查看它,發現這裏的編碼是cp1252。
➏ 在Windows cp1252編碼中,0xc3字節是“Ô(帶波形符的A),0xa9字節是版權符號。
➐ 使用正確的編碼打開那個文件。
➑ 結果符合預期:得到的是四個Unicode字符’café’。
➒ 'rb’標誌指明在二進制模式中讀取文件。
➓ 返回的是BufferedReader對象,而不是TextIOWrapper對象。
⑪讀取返回的字節序列,結果與預期相符。