PyTips 0x09 - Python 中 Unicode 的正確用法

項目地址:https://git.io/pytipshtml

0x070x08 分別介紹了 Python 中的字符串類型(str)和字節類型(byte),以及 Python 編碼中最多見也是最頑固的兩個錯誤:python

UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)git

UnicodeDecodeError: 'utf-8' codec can't decode bytes in position 0-1: invalid continuation bytegithub

這一期就從這兩個錯誤入手,分析 Python 中 Unicode 的正確用法。這篇短文並不能保證你能夠永遠杜絕上面兩個錯誤,可是但願在下次遇到這個錯誤的時候知道錯在哪裏、應該從哪裏入手。網絡

編碼與解碼

上面的兩個錯誤分別是 UnicodeEncodeErrorUnicodeDecodeError,也就是說分別在 Unicode 編碼(Encode)和解碼(Decode)過程當中出現了錯誤,那麼編碼和解碼究竟分別意味着什麼?根據維基百科字符編碼的定義:編碼

字符編碼(英語:Character encoding)、字集碼是把字符集中的字符編碼爲指定集合中某一對象(例如:比特模式、天然數序列、8位組或者電脈衝),以便文本在計算機中存儲和經過通訊網絡的傳遞。spa

簡單來講就是把人類通用的語言符號翻譯成計算機通用的對象,而反向的翻譯過程天然就是解碼了。Python 中的字符串類型表明人類通用的語言符號,所以字符串類型有encode()方法;而字節類型表明計算機通用的對象(二進制數據),所以字節類型有decode()方法。翻譯

print("??".encode())
b'\xf0\x9f\x8c\x8e\xf0\x9f\x8c\x8f'
print(b'\xf0\x9f\x8c\x8e\xf0\x9f\x8c\x8f'.decode())
??

既然說編碼和解碼都是翻譯的過程,那麼就須要一本字典將人類和計算機的語言一一對應起來,這本字典的名字叫作字符集,從最先的 ASCII 到如今最通用的 Unicode,它們的本質是同樣的,只是兩本字典的厚度不一樣而已。ASCII 只包含了26個基本拉丁字母、阿拉伯數目字和英式標點符號一共128個字符,所以只須要(不佔滿)一個字節就能夠存儲,而 Unicode 則涵蓋的數據除了視覺上的字形、編碼方法、標準的字符編碼外,還包含了字符特性,如大小寫字母,共可包含 1.1M 個字符,而到如今只填充了其中的 110K 個位置。code

字符集中字符所存儲的位置(或者說對應的計算機通用的數字)稱之爲碼位(code point),例如在 ASCII 中字符 '$' 的碼位就是:htm

print(ord('$'))
36

ASCII 只須要一個字節就能存下全部碼位,而 Unicode 則須要幾個字節才能容納,可是對於具體採用什麼樣的方案來實現 Unicode 的這種映射關係,也有不少不一樣的方案(或規則),例如最多見(也是 Python 中默認的)UTF-8,還有 UTF-1六、UTF-32 等,對於它們規則上的不一樣這裏就不深刻展開了。固然,在 ASCII 與 Unicode 之間還有不少其餘的字符集與編碼方案,例如中文編碼的 GB23十二、繁體字的 Big5 等等,這並不影響咱們對編碼與解碼過程的理解。

Unicode*Error

明白了字符串與字節,編碼與解碼以後,讓咱們手動製造上面兩個 Unicode*Error 試試,首先是編碼錯誤:

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 進行編碼
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 說明你用錯了字典,要翻譯的字符沒辦法正確翻譯成碼位!

再來看解碼錯誤:

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
$
$
$
'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 進行解碼(編碼)的,通常也能夠用其它方法;對於不是不存在子集關係的編碼,強行解碼有可能會致使錯誤或亂碼!

實踐中的策略

清楚了上面介紹的全部原理以後,在時間操做中應該怎樣規避錯誤或亂碼呢?

  1. 記清楚編碼與解碼的方向;

  2. 在 Python 中的操做盡可能採用 UTF-8,輸入或輸出的時候再根據需求肯定是否須要編碼成二進制:

# 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"))
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!

Unicode


歡迎關注 PyHub!
PyHub

參考

  1. Pragmatic Unicode

  2. 字符編碼筆記:ASCII,Unicode和UTF-8

相關文章
相關標籤/搜索