0x07 和 0x08 分別介紹了 Python 中的字串型別(str
)和位元組型別(byte
),以及 Python 編碼中最常見也是最頑固的兩個錯誤:
UnicodeEncodeError: ‘ascii’ codec can’t encode characters in position 0-1: ordinal not in range(128)
UnicodeDecodeError: ‘utf-8’ codec can’t decode bytes in position 0-1: invalid continuation byte
這一期就從這兩個錯誤入手,分析 Python 中 Unicode 的正確用法。這篇短文並不能保證你可以永遠杜絕上面兩個錯誤,但是希望在下次遇到這個錯誤的時候知道錯在哪裡、應該從哪裡入手。
編碼與解碼
上面的兩個錯誤分別是 UnicodeEncodeError
和 UnicodeDecodeError
,也就是說分別在 Unicode 編碼(Encode)和解碼(Decode)過程中出現了錯誤,那麼編碼和解碼究竟分別意味著什麼?根據維基百科字元編碼的定義:
字元編碼(英語:Character encoding)、字集碼是把字符集中的字元編碼為指定集合中某一物件(例如:位元模式、自然數序列、8位組或者電脈衝),以便文字在計算機中儲存和通過通訊網路的傳遞。
簡單來說就是把人類通用的語言符號翻譯成計算機通用的物件,而反向的翻譯過程自然就是解碼了。Python 中的字串型別代表人類通用的語言符號,因此字串型別有encode()
方法;而位元組型別代表計算機通用的物件(二進位制資料),因此位元組型別有decode()
方法。
既然說編碼和解碼都是翻譯的過程,那麼就需要一本字典將人類和計算機的語言一一對應起來,這本字典的名字叫做字符集,從最早的 ASCII 到現在最通用的 Unicode,它們的本質是一樣的,只是兩本字典的厚度不同而已。ASCII 只包含了26個基本拉丁字母、阿拉伯數目字和英式標點符號一共128個字元,因此只需要(不佔滿)一個位元組就可以儲存,而 Unicode 則涵蓋的資料除了視覺上的字形、編碼方法、標準的字元編碼外,還包含了字元特性,如大小寫字母,共可包含 1.1M 個字元,而到現在只填充了其中的 110K 個位置。
字符集中字元所儲存的位置(或者說對應的計算機通用的數字)稱之為碼位(code point),例如在 ASCII 中字元 '$'
的碼位就是:
1 |
print(ord('$')) |
1 2 |
36 |
ASCII 只需要一個位元組就能存下所有碼位,而 Unicode 則需要幾個位元組才能容納,但是對於具體採用什麼樣的方案來實現 Unicode 的這種對映關係,也有很多不同的方案(或規則),例如最常見(也是 Python 中預設的)UTF-8,還有 UTF-16、UTF-32 等,對於它們規則上的不同這裡就不深入展開了。當然,在 ASCII 與 Unicode 之間還有很多其他的字符集與編碼方案,例如中文編碼的 GB2312、繁體字的 Big5 等等,這並不影響我們對編碼與解碼過程的理解。
Unicode*Error
明白了字串與位元組,編碼與解碼之後,讓我們手動製造上面兩個 Unicode*Error
試試,首先是編碼錯誤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def tryEncode(s, encoding="utf-8"): try: print(s.encode(encoding)) except UnicodeEncodeError as err: print(err) s = "$" # UTF-8 String tryEncode(s) # 預設用 UTF-8 進行編碼 tryEncode(s, "ascii") # 嘗試用 ASCII 進行編碼 s = "雨" # UTF-8 String tryEncode(s) # 預設用 UTF-8 進行編碼 tryEncode(s, "ascii") # 嘗試用 ASCII 進行編碼 tryEncode(s, "GB2312") # 嘗試用 GB2312 進行編碼 |
1 2 3 4 5 6 |
b'$' b'$' b'\xe9\x9b\xa8' 'ascii' codec can't encode character '\u96e8' in position 0: ordinal not in range(128) b'\xd3\xea' |
由於 UTF-8 對 ASCII 的相容性,"$"
可以用 ASCII 進行編碼;而 "雨"
則無法用 ASCII 進行編碼,因為它已經超出了 ASCII 字符集的 128 個字元,所以引發了 UnicodeEncodeError
;而 "雨"
在 GB2312 中的碼位是 b'\xd3\xea'
,與 UTF-8 不同,但是仍然可以正確編碼。因此如果出現了 UnicodeEncodeError
說明你用錯了字典,要翻譯的字元沒辦法正確翻譯成碼位!
再來看解碼錯誤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
def tryDecode(s, decoding="utf-8"): try: print(s.decode(decoding)) except UnicodeDecodeError as err: print(err) b = b'$' # Bytes tryDecode(b) # 預設用 UTF-8 進行解碼 tryDecode(b, "ascii") # 嘗試用 ASCII 進行解碼 tryDecode(b, "GB2312") # 嘗試用 GB2312 進行解碼 b = b'\xd3\xea' # 上面例子中通過 GB2312 編碼得到的 Bytes tryDecode(b) # 預設用 UTF-8 進行解碼 tryDecode(b, "ascii") # 嘗試用 ASCII 進行解碼 tryDecode(b, "GB2312") # 嘗試用 GB2312 進行解碼 tryDecode(b, "GBK") # 嘗試用 GBK 進行解碼 tryDecode(b, "Big5") # 嘗試用 Big5 進行解碼 tryDecode(b.decode("GB2312").encode()) # Byte-Decode-Unicode-Encode-Byte |
1 2 3 4 5 6 7 8 9 10 |
$ $ $ 'utf-8' codec can't decode byte 0xd3 in position 0: invalid continuation byte 'ascii' codec can't decode byte 0xd3 in position 0: ordinal not in range(128) 雨 雨 迾 雨 |
一般後續出現的字符集都是對 ASCII 相容的,可以認為 ASCII 是他們的一個子集,因此可以用 ASCII 進行解碼(編碼)的,一般也可以用其它方法;對於不是不存在子集關係的編碼,強行解碼有可能會導致錯誤或亂碼!
實踐中的策略
清楚了上面介紹的所有原理之後,在時間操作中應該怎樣規避錯誤或亂碼呢?
- 記清楚編碼與解碼的方向;
- 在 Python 中的操作儘量採用 UTF-8,輸入或輸出的時候再根據需求確定是否需要編碼成二進位制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# cat utf8.txt # 你好,世界! # file utf8.txt # utf8.txt: UTF-8 Unicode text with open("utf8.txt", "rb") as f: content = f.read() print(content) print(content.decode()) with open("utf8.txt", "r") as f: print(f.read()) # cat gb2312.txt # 你好,Unicode! # file gb2312.txt # gb2312.txt: ISO-8859 text with open("gb2312.txt", "r") as f: try: print(f.read()) except: print("Failed to decode file!") with open("gb2312.txt", "rb") as f: print(f.read().decode("gb2312")) |
1 2 3 4 5 6 7 8 |
b'\xe4\xbd\xa0\xe5\xa5\xbd\xef\xbc\x8c\xe4\xb8\x96\xe7\x95\x8c\xef\xbc\x81\n' 你好,世界! 你好,世界! Failed to decode file! 你好,Unicode! |
參考
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!