Python 編碼錯誤的本質原因

發表於2017-06-15

不論你是有著多年經驗的 Python 老司機還是剛入門 Python 不久的新貴,你一定遇到過UnicodeEncodeError、UnicodeDecodeError 錯誤,每當遇到錯誤我們就拿著 encode、decode 函式翻來覆去的轉換,有時試著試著問題就解決了,有時候怎麼試都沒轍,只有借用 Google 大神幫忙,但似乎很少去關心問題的本質是什麼,下次遇到類似的問題重蹈覆轍,那麼你有沒有想過一次性徹底把 Python 字元編碼給搞懂呢?

完全理解字元編碼 與 Python 的淵源前,我們有必要把一些基礎概念弄清楚,雖然有些概念我們每天都在接觸甚至在使用它,但並不一定真正理解它。比如:位元組、字元、字符集、字元碼、字元編碼。

位元組

位元組(Byte)是計算機中資料儲存的基本單元,一位元組等於一個8位的位元,計算機中的所有資料,不論是儲存在磁碟檔案上的還是網路上傳輸的資料(文字、圖片、視訊、音訊檔案)都是由位元組組成的。

字元

你正在閱讀的這篇文章就是由很多個字元(Character)構成的,字元一個資訊單位,它是各種文字和符號的統稱,比如一個英文字母是一個字元,一個漢字是一個字元,一個標點符號也是一個字元。

字符集

字符集(Character Set)就是某個範圍內字元的集合,不同的字符集規定了字元的個數,比如 ASCII 字符集總共有128個字元,包含了英文字母、阿拉伯數字、標點符號和控制符。而 GB2312 字符集定義了7445個字元,包含了絕大部分漢字字元。

字元碼

字元碼(Code Point)指的是字符集中每個字元的數字編號,例如 ASCII 字符集用 0-127 連續的128個數字分別表示128個字元,例如 “A” 的字元碼編號就是65。

字元編碼

字元編碼(Character Encoding)是將字符集中的字元碼對映為位元組流的一種具體實現方案,常見的字元編碼有 ASCII 編碼、UTF-8 編碼、GBK 編碼等。某種意義上來說,字符集與字元編碼有種對應關係,例如 ASCII 字符集對應 有 ASCII 編碼。ASCII 字元編碼規定使用單位元組中低位的7個位元去編碼所有的字元。例如”A” 的編號是65,用單位元組表示就是0×41,因此寫入儲存裝置的時候就是b’01000001’。

編碼、解碼

編碼的過程是將字元轉換成位元組流,解碼的過程是將位元組流解析為字元。


理解了這些基本的術語概念後,我們就可以開始討論計算機的字元編碼的演進過程了。

從 ASCII 碼說起

說到字元編碼,要從計算機的誕生開始講起,計算機發明於美國,在英語世界裡,常用字元非常有限,26個字母(大小寫)、10個數字、標點符號、控制符,這些字元在計算機中用一個位元組的儲存空間來表示綽綽有餘,因為一個位元組相當於8個位元位,8個位元位可以表示256個符號。於是美國國家標準協會ANSI制定了一套字元編碼的標準叫 ASCII(American Standard Code for Information Interchange),每個字元都對應唯一的一個數字,比如字元 “A” 對應數字是65,”B” 對應 66,以此類推。最早 ASCII 只定義了128個字元編碼,包括96個文字和32個控制符號,一共128個字元只需要一個位元組的7位就能表示所有的字元,因此 ASCII 只使用了一個位元組的後7位,剩下最高位1位元被用作一些通訊系統的奇偶校驗。下圖就是 ASCII 碼字元編碼的十進位制、二進位制和字元的對應關係表

ascii

擴充套件的 ASCII:EASCII(ISO/8859-1)

然而計算機慢慢地普及到其他西歐地區時,發現還有很多西歐字元是 ASCII 字符集中沒有的,顯然 ASCII 已經沒法滿足人們的需求了,好在 ASCII 字元只用了位元組的7位 0×00~0x7F 共128個字元,於是他們在 ASCII 的基礎上把原來的7位擴充到8位,把0×80-0xFF這後面的128個數字利用起來,叫 EASCII ,它完全相容ASCII,擴充套件出來的符號包括表格符號、計算符號、希臘字母和特殊的拉丁符號。然而 EASCII 時代是一個混亂的時代,各個廠家都有自己的想法,大家沒有統一標準,他們各自把最高位按照自己的標準實現了自己的一套字元編碼標準,比較著名的就有 CP437, CP437 是 始祖IBM PC、MS-DOS使用的字元編碼,如下圖:

cp437

眾多的 ASCII 擴充字符集之間互不相容,這樣導致人們無法正常交流,例如200在CP437字符集表示的字元是 È ,在 ISO/8859-1 字符集裡面顯示的就是 ╚,於是國際標準化組織(ISO)及國際電工委員會(IEC)聯合制定的一系列8位字符集的標準ISO/8859-1(Latin-1),它繼承了 CP437 字元編碼的128-159之間的字元,所以它是從160開始定義的,ISO-8859-1在 CP437 的基礎上重新定義了 160~255之間的字元。iso8859-1

多位元組字元編碼 GBK

ASCII 字元編碼是單位元組編碼,計算機進入中國後面臨的一個問題是如何處理漢字,對於拉丁語系國家來說通過擴充套件最高位,單位元組表示所有的字元已經綽綽有餘,但是對於亞洲國家來說一個位元組就顯得捉襟見肘了。於是中國人自己弄了一套叫 GB2312 的雙位元組字元編碼,又稱GB0,1981 由中國國家標準總局釋出。GB2312 編碼共收錄了6763個漢字,同時他還相容 ASCII,GB 2312的出現,基本滿足了漢字的計算機處理需要,它所收錄的漢字已經覆蓋中國大陸99.75%的使用頻率,不過 GB2312 還是不能100%滿足中國漢字的需求,對一些罕見的字和繁體字 GB2312 沒法處理,後來就在GB2312的基礎上建立了一種叫 GBK 的編碼,GBK 不僅收錄了27484個漢字,同時還收錄了藏文、蒙文、維吾爾文等主要的少數民族文字。同樣 GBK 也是相容 ASCII 編碼的,對於英文字元用1個位元組來表示,漢字用兩個位元組來標識。

Unicode 的問世

GBK僅僅只是解決了我們自己的問題,但是計算機不止是美國人和中國人用啊,還有歐洲、亞洲其他國家的文字諸如日文、韓文全世界各地的文字加起來估計也有好幾十萬,這已經大大超出了ASCII 碼甚至GBK 所能表示的範圍了,雖然各個國家可以制定自己的編碼方案,但是資料在不同國家傳輸就會出現各種各樣的亂碼問題。如果只用一種字元編碼就能表示地球甚至火星上任何一個字元時,問題就迎刃而解了。是它,是它,就是它,我們的小英雄,統一聯盟國際組織提出了Unicode 編碼,Unicode 的學名是”Universal Multiple-Octet Coded Character Set”,簡稱為UCS。它為世界上每一種語言的每一個字元定義了一個唯一的字元碼,Unicode 標準使用十六進位制數字表示,數字前面加上字首 U+,比如字母『A』的Unicode編碼是 U+0041,漢字『中』的Unicode 編碼是U+4E2D

Unicode有兩種格式:UCS-2和UCS-4。UCS-2就是用兩個位元組編碼,一共16個位元位,這樣理論上最多可以表示65536個字元,不過要表示全世界所有的字元顯示65536個數字還遠遠不過,因為光漢字就有近10萬個,因此Unicode4.0規範定義了一組附加的字元編碼,UCS-4就是用4個位元組(實際上只用了31位,最高位必須為0)。理論上完全可以涵蓋一切語言所用的符號。

Unicode 的侷限

但是 Unicode 有一定的侷限性,一個 Unicode 字元在網路上傳輸或者最終儲存起來的時候,並不見得每個字元都需要兩個位元組,比如字元“A“,用一個位元組就可以表示的字元,偏偏還要用兩個位元組,顯然太浪費空間了。

第二問題是,一個 Unicode 字元儲存到計算機裡面時就是一串01數字,那麼計算機怎麼知道一個2位元組的Unicode字元是表示一個2位元組的字元呢,例如“漢”字的 Unicode 編碼是 U+6C49,我可以用4個ascii數字來傳輸、儲存這個字元;也可以用utf-8編碼的3個連續的位元組E6 B1 89來表示它。關鍵在於通訊雙方都要認可。因此Unicode編碼有不同的實現方式,比如:UTF-8、UTF-16等等。Unicode就像英語一樣,做為國與國之間交流世界通用的標準,每個國家有自己的語言,他們把標準的英文文件翻譯成自己國家的文字,這是實現方式,就像utf-8。

具體實現:UTF-8

UTF-8(Unicode Transformation Format)作為 Unicode 的一種實現方式,廣泛應用於網際網路,它是一種變長的字元編碼,可以根據具體情況用1-4個位元組來表示一個字元。比如英文字元這些原本就可以用 ASCII 碼錶示的字元用UTF-8表示時就只需要一個位元組的空間,和 ASCII 是一樣的。對於多位元組(n個位元組)的字元,第一個位元組的前n為都設為1,第n+1位設為0,後面位元組的前兩位都設為10。剩下的二進位制位全部用該字元的unicode碼填充。

code

以『好』為例,『好』對應的 Unicode 是597D,對應的區間是 0000 0800–0000 FFFF,因此它用 UTF-8 表示時需要用3個位元組來儲存,597D用二進位制表示是: 0101100101111101,填充到 1110xxxx 10xxxxxx 10xxxxxx 得到 11100101 10100101 10111101,轉換成16進位制是 e5a5bd,因此『好』的 Unicode 碼 U+597D 對應的 UTF-8 編碼是 “E5A5BD”。你可以用 Python 程式碼來驗證:


現在總算把理論說完了。再來說說 Python 中的編碼問題。Python 的誕生時間比 Unicode 要早很多,Python2 的預設編碼是ASCII,正因為如此,才導致很多的編碼問題。

所以在 Python2 中,原始碼檔案必須顯示地指定編碼型別,否則但凡程式碼中出現有中文就會報語法錯誤

Python2 字元型別

在 python2 中和字串相關的資料型別有 str 和 unicode 兩種型別,它們繼承自 basestring,而 str 型別的字串的編碼格式可以是 ascii、utf-8、gbk等任何一種型別。

python-str.png

圖片來源:http://funhacks.net/2016/11/25/character_encoding/

對於漢字『好』,用 str 表示時,它對應的 utf-8 編碼 是’\xe5\xa5\xbd’,對應的 gbk 編碼是 ‘\xba\xc3’,而用 unicode 表示時,他對應的符號就是u’\u597d’,與u”好” 是等同的。

str 與 unicode 的轉換

在 Python 中 str 和 unicode 之間是如何轉換的呢?這兩種型別的字串之間的轉換就是靠decode 和 encode 這兩個函式。encode 負責將unicode 編碼成指定的字元編碼,用於儲存到磁碟或傳輸到網路中。而 decode 方法是根據指定的編碼方式解碼後在應用程式中使用。

UnicodeXXXError 錯誤的原因

在字元編碼轉換操作時,遇到最多的問題就是 UnicodeEncodeError 和 UnicodeDecodeError 錯誤了,這些錯誤的根本原因在於 Python2 預設是使用 ascii 編碼進行 decode 和 encode 操作,例如:

CASE 1

當把 s 轉換成 unicode 型別的字串時,decode 方法預設使用 ascii 編碼進行解碼,而 ascii 字符集中根本就沒有中文字元『你好』,所以就出現了 UnicodeDecodeError,正確的方式是顯示地指定 UTF-8 字元編碼。

同樣地道理,對於 encode 操作,把 unicode字串轉換成 str型別的字串時,預設也是使用 ascii 編碼進行編碼轉換的,而 ascii 字符集找不到中文字元『你好』,於是就出現了UnicodeEncodeError 錯誤。

CASE 2

str 型別與 unicode 型別的字串混合使用時,str 型別的字串會隱式地將 str 轉換成 unicode字串,如果 str字串是中文字元,那麼就會出現UnicodeDecodeError 錯誤,因為 python2 預設會使用 ascii 編碼來進行 decode 操作。

正確地方式是顯示地指定 UTF-8 字元編碼進行解碼

亂碼

所有出現亂碼的原因都可以歸結為字元經過不同編碼解碼在編碼的過程中使用的編碼格式不一致,比如:

utf-8編碼的字元‘好’佔用3個位元組,解碼成Unicode後,如果再用gbk來解碼後,只有2個位元組的長度了,最後出現了亂碼的問題,因此防止亂碼的最好方式就是始終堅持使用同一種編碼格式對字元進行編碼和解碼操作。

decode-encode

相關文章