對於單純作前端或者後端的同窗來講,通常很難接觸到編碼問題,由於在同一個平臺上,通常都是使用同一種編碼方式,天然問題不大。但對於寫爬蟲的同窗來講,編碼極可能是遇到的第一個坑。這是由於字符串沒法直接經過網絡被傳輸(也不能直接被存儲),須要先轉換成二進制格式,再被還原。所以凡是涉及到經過網絡傳輸字符的地方,一般都容易遇到編碼問題。前端
爲了方便解釋,咱們首先來定義一些概念。每一個開發者都知道字符串,它是一些字符的集合, 好比 hello world
就是一個最多見的字符串。相對來講,字符 比較難定義一些。從語義上來說,它是組成字符串的最基本單位,好比這裏的字母、空格,以及標點、特定語言(中文、日文)、emoji 符號等等。python
字符是語言中的概念,可是計算機只認識 0 和 1 這兩個數字。所以要想讓計算機存儲、處理字符串,就必須把字符串用二進制表示出來。在 ASCII 碼中,每一個英文字母都有本身對應的數字。咱們一般把 ASCII 碼稱爲字符集,也就是字符的集合。瞭解 ASCII 碼的同窗應該都知道小寫字母 a 能夠用 97 來表示,97 也被稱爲字符 a
在 ASCII 字符集中的碼位。後端
若是要設計一種密碼,最簡單的方式就是把字母轉換成它在 ASCII 碼中的碼位再發送,接受者則查找 ASCII 碼錶,還原字符。可見把字符轉換成碼位的過程相似於加密(encrypt),咱們稱之爲編碼(encode),反則則相似於解密,咱們稱之爲解碼(decode)網絡
字符轉換成碼位的過程是編碼,這個過程有無數種實現方式。好比 a -> 97
、b -> 98
這種就是 ASCII 編碼,由於 255 = 2 ^ 8,因此全部 ASCII 編碼下的碼位剛好均可以由一個字節表示。ide
ASCII 比較誕生得比較早,隨着愈來愈多的國家開始使用計算機,0-255 這麼點碼位確定不夠用了。好比中國人爲了展現漢字,發明了 GB2312 編碼。GB2312編碼徹底向下兼容 ASCII 編碼,也就是說 全部 ASCII 字符集中的字符,它在 GB2312 編碼下的碼位與 ASCII 編碼下的碼位徹底一致,而中文則由兩個字節表示,這也就是爲何早期咱們通常認爲一箇中文等於兩個英文的緣由。函數
除了中國人本身的編碼方式,各個地區的人也都根據本身的語言拓展了相應的編碼方式。那麼問題就來了, 給你一個碼位 0xEE 0xDD
,它到底表示什麼字符,取決於它是用哪一種編碼方式編碼的。這就比如你拿到了密文,但沒有密碼錶同樣。所以,要想正確顯示一種語言,就必須攜帶這個語言的編碼規範,要想正確顯示世界上全部的語言,看起來就比較困難了。編碼
所以 Unicode 其實是一種統一的字符集規範,每個 Unicode 字符由 6 個十六進制數字表示,所以理論上能夠表示出 16 ^ 6 = 16777216
個字符,顯然是綽綽有餘了。加密
看起來 Unicode 就是一種很棒的編碼方式。誠然,Unicode 能夠表示全部的字符,但過於全面帶來的缺點就是過於龐大。對於字符 a
來講,若是使用 ASCII 編碼,能夠表示爲 0x61,只要一個字節就能存下,但它的 Unicode 碼位是 0x000061,須要三個字節。所以採用 Unicode 編碼的英文內容,會比 ASCII 編碼大三倍。這大大增長了文件本地存儲時佔用的空間以及傳輸時的體積。spa
所以,咱們有了對 Unicode 字符再次編碼的編碼方式,常見的有 utf-8,utf-16 等。UTF 表示 Unicode Transfer Format,所以是針對 Unicode 字符集的一系列編碼方式。utf-8 是一種變長編碼,也就是說不一樣的 Unicode 字符在 utf-8 編碼下的碼位長度可能不一樣,以下表所示:操作系統
Unicode 編碼(16進制) | utf-8 碼位(二進制) |
---|---|
000000-00007F | 0xxxxxxx |
000080-0007FF | 110xxxxx 10xxxxxx |
000800-00FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
010000-1FFFFF | 11110xxx10xxxxxx10xxxxxx10xxxxxx |
這個表有兩點值得注意。一個是 ASCII 字符集中的全部字符,它們的 utf-8 碼位依然佔用一個字節,所以 utf-8 編碼下的英文字符不會向 Unicode 同樣增長大小。另外一個則是全部中文的 utf-8 碼位都佔用 3 個字節,大於 GBK 編碼的 2 字節。所以若是存在明確的業務須要,是能夠用 GBK 編碼取代 utf-8 編碼的。
儘管 utf-8 很是經常使用,但它可變長度的特色不只會致使某些場景下內容過大,也爲索引和隨機讀取帶來了困難。所以在不少操做系統的內存運算中,一般使用 utf-16 編碼來代替。utf-16 的特色是全部碼位的最小單位都是 2 字節,雖然存在冗餘,但易於索引。因爲碼位都是兩個字節,就會存在字節序的問題。所以 utf-16 編碼的字符串,一開頭會有幾個字節的 BOM(Byte order markd)來標記字節序,好比 0xFF 2
(FE0x55,254) 表示 Intel CPU 的小字節序,若是不加 BOM 則默認表示大字節序。須要注意的是,某些應用會給 utf-8 編碼的字節也加上 BOM。
雖然看起來問題變得複雜了,爲了存儲/傳輸一個字符,居然須要兩次編碼,但別忘了,Unicode 編碼是通用的,所以能夠內置於操做系統內部。因此咱們平時所謂的對字符串進行 utf-8 編碼,其實說的是對字符串的 Unicode 碼位進行 utf-8 編碼。
這一點在 python3 中獲得了充分的體現,字符串由字符組成,每個字符都是一個 Unicode 碼位。
若是把編解碼理解成利用密碼錶進行加解密,那麼就容易理解,爲何編碼和解碼過程都是易錯的。
若是被編碼的 Unicode 字符,在某種編碼中根本沒有列出編碼方式,這個字符就沒法被編碼:
city = 'São Paulo'
b_city = city.encode('cp437')
# UnicodeEncodeError: 'charmap' codec can't encode character '\xe3' in position 1: character maps to <undefined>
b_city = city.encode('cp437', errors='ignore')
# b'So Paulo'
b_city = city.encode('cp437', errors='replace')
# b'S?o Paulo'複製代碼
同理,若是被解碼的碼位,在編碼表中找不到與之對應的字符,這個碼位就沒法被解碼:
octets = b'Montr\xe9al'
s_octest1 = octets.decode('utf8')
# UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 5: invalid continuation byte
s_octest1 = octets.decode('cp1252')
# Montréal
s_octest1 = octets.decode('iso8859_7')
# Montrιal
s_octest1 = octets.decode('utf8', errors='replace')
# Montr�al複製代碼
python 的解決方案是,encode
和 decode
函數都有一個參數 errors
能夠控制如何處理沒法被編、解碼的內容。它的值能夠是 ignore
(忽略這個錯誤並繼續執行),也能夠是 replace
(用系統的佔位符填充)。
通常來講,沒法從碼位推斷出編碼方式,就像你不可能從密文推斷出加密方式同樣。可是某些編碼方式會留下很是顯著的特徵,一旦這些特徵頻繁出現,基本就能夠判定編碼方式。Python 提供了一個名爲 Chardet
的包,能夠幫助開發者推斷出編碼方式,而且會給出相應的置信度。置信度越高,說明是這種編碼方式的可能性越大。
octets = b'Montr\xe9al'
chardet.detect(octets)
# {'encoding': 'ISO-8859-1', 'confidence': 0.73, 'language': ''}
octets.decode('ISO-8859-1')
# Montréal複製代碼