PYTHON編碼的前世今生

劉志軍發表於2016-10-09

這是我在知乎上回答的一個問題:Python 編碼為什麼那麼蛋疼?,期間收到了不少贊,不過發現我的回答還存在一些誤導,於是通過查詢資料重新整理了一篇,希望能解答你對編碼的困惑。

一旦走上了程式設計之路,如果你不把編碼問題搞清楚,那麼它將像幽靈一般糾纏你整個職業生涯,各種靈異事件會接踵而來,揮之不去。只有充分發揮程式設計師死磕到底的精神你才有可能徹底擺脫編碼問題帶來的煩惱,我第一次遇到編碼問題是寫 JavaWeb 相關的專案,一串字元從瀏覽器遊離到應用程式程式碼中,翻江倒海沉浸到資料庫中,隨時隨地都有可能踩到編碼的地雷。第二次遇到編碼問題就是學 Python 的時候,在爬取網頁資料時,編碼問題又出現了,當時我的心情是奔潰的,用時下最ing的一句話就是:“我當時就懵逼了”。為了搞清字元編碼,我們得從計算機的起源開始,計算機中的所有資料,不論是文字、圖片、視訊、還是音訊檔案,本質上最終都是按照類似 01010101 的數字形式儲存的。我們是幸運的,我們也是不幸的,幸運的是時代賦予了我們都有機會接觸計算機,不幸的是,計算機不是我們國人發明的,所以計算機的標準得按美帝國人的習慣來設計,那麼最開始計算機是通過什麼樣的方式來表現字元的呢?這要從計算機編碼的發展史說起。

ASCII

每個做 JavaWeb 開發的新手都會遇到亂碼問題,每個做 Python 爬蟲的新手都會遇到編碼問題,為什麼編碼問題那麼蛋疼呢?這個問題要從1992年 Guido van Rossum 創造 Python 這門語言說起,那時的 Guido 絕對沒想到的是 Python 這門語言在今天會如此受大家歡迎,也不會想到計算機發展速度會如此驚人,儘管 Guido 在當初設計這門語言時是不需要關心編碼的,因為在英語世界裡,字元的個數非常有限,26個字母(大小寫)、10個數字、標點符號、控制符,也就是鍵盤上所有的鍵所對應的字元加起來也不過是一百多個字元而已,這在計算機中用一個位元組的儲存空間來表示一個字元是綽綽有餘的,因為一個位元組相當於8個位元位,8個位元位可以表示256個符號。於是聰明的美國人就制定了一套字元編碼的標準叫ASCII(American Standard Code for Information Interchange),每個字元都對應唯一的一個數字,比如字元A對應的二進位制數值是01000001,對應的十進位制就是65。最開始ASCII只定義了128個字元編碼,包括96個文字和32個控制符號,一共128個字元只需要一個位元組的7位就能表示所有的字元,因此 ASCII 只使用了一個位元組的後7位,最高位都為0。每個字元與ASCII碼的對應關係可檢視網站ascii-codeascii

EASCII(ISO/8859-1)

然而計算機慢慢地普及到其他西歐地區時,他們發現還有很多西歐所特有的字元是 ASCII 編碼表中沒有的,於是後來出現了可擴充套件的 ASCII 叫 EASCII ,顧名思義,它是在ASCII的基礎上擴充套件而來,把原來的7位擴充到8位,它完全相容ASCII,擴充套件出來的符號包括表格符號、計算符號、希臘字母和特殊的拉丁符號。然而 EASCII 時代是一個混亂的時代,大家沒有統一標準,他們各自把最高位按照自己的標準實現了自己的一套字元編碼標準,比較著名的就有 CP437, CP437 是 Windows 系統中使用的字元編碼,如下圖:
cp437

另外一種被廣泛使用的 EASCII 還有 ISO/8859-1(Latin-1),它是國際標準化組織(ISO)及國際電工委員會(IEC)聯合制定的一系列8位元字符集的標準,ISO/8859-1 只繼承了 CP437 字元編碼的128-159之間的字元,所以它是從160開始定義的,不幸的是這些眾多的 ASCII 擴充字集之間互不相容。 iso8859-1

GBK

隨著時代的進步,計算機開始普及到千家萬戶,比爾蓋茲讓每個人桌面都有一臺電腦的夢想得以實現。但是計算機進入中國不得不面臨的一個問題就是字元編碼,雖然我們們國家的漢字是人類使用頻率最多的文字,漢字博大精深,常見的漢字就有成千上萬,這已經大大超出了 ASCII 編碼所能表示的字元範圍了,即使是 EASCII 也顯得杯水車薪,於是聰明的中國人自己弄了一套編碼叫 GB2312,又稱GB0,1981由中國國家標準總局釋出。GB2312 編碼共收錄了6763個漢字,同時他還相容 ASCII,GB 2312的出現,基本滿足了漢字的計算機處理需要,它所收錄的漢字已經覆蓋中國大陸99.75%的使用頻率,不過 GB2312 還是不能100%滿足中國漢字的需求,對一些罕見的字和繁體字 GB2312 沒法處理,後來就在GB2312的基礎上建立了一種叫 GBK 的編碼,GBK 不僅收錄了27484個漢字,同時還收錄了藏文、蒙文、維吾爾文等主要的少數民族文字。同樣 GBK 也是相容 ASCII 編碼的,對於英文字元用1個位元組來表示,漢字用兩個位元組來標識。

Unicode

對於如何處理中國人自己的文字我們可以另立山頭,按照我們自己的需求制定一套編碼規範,但是計算機不止是美國人和中國人用啊,還有歐洲、亞洲其他國家的文字諸如日文、韓文全世界各地的文字加起來估計也有好幾十萬,這已經大大超出了ASCII碼甚至GBK所能表示的範圍了,況且人家為什麼用採用你GBK標準呢?如此龐大的字元庫究竟用什麼方式來表示好呢?於是統一聯盟國際組織提出了Unicode編碼,Unicode的學名是”Universal Multiple-Octet Coded Character Set”,簡稱為UCS。Unicode有兩種格式:UCS-2和UCS-4。UCS-2就是用兩個位元組編碼,一共16個位元位,這樣理論上最多可以表示65536個字元,不過要表示全世界所有的字元顯示65536個數字還遠遠不過,因為光漢字就有近10萬個,因此Unicode4.0規範定義了一組附加的字元編碼,UCS-4就是用4個位元組(實際上只用了31位,最高位必須為0)。理論上完全可以涵蓋一切語言所用的符號。世界上任何一個字元都可以用一個Unicode編碼來表示,一旦字元的Unicode編碼確定下來後,就不會再改變了。但是Unicode有一定的侷限性,一個Unicode字元在網路上傳輸或者最終儲存起來的時候,並不見得每個字元都需要兩個位元組,比如一字元“A“,用一個位元組就可以表示的字元,偏偏還要用兩個位元組,顯然太浪費空間了。第二問題是,一個Unicode字元儲存到計算機裡面時就是一串01數字,那麼計算機怎麼知道一個2位元組的Unicode字元是表示一個2位元組的字元呢,還是表示兩個1位元組的字元呢,如果你不事先告訴計算機,那麼計算機也會懵逼了。Unicode只是規定如何編碼,並沒有規定如何傳輸、儲存這個編碼。例如“漢”字的Unicode編碼是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”597D”對應的UTF-8編碼是”E5A5BD”

PYTHON字元編碼

現在總算把理論說完了。再來說說Python中的編碼問題。Python的誕生時間比Unicode要早很多,Python的預設編碼是ASCII

所以在Python原始碼檔案中如果不顯示地指定編碼的話,將出現語法錯誤

上面是test.py指令碼,執行 python test.py 就會包如下錯誤:

File “test.py”, line 1 yntaxError: Non-ASCII character ‘xe4′ in file test.py on line 1, but no encoding declared; see http://www.python.org/ ps/pep-0263.html for details

為了在原始碼中支援非ASCII字元,必須在原始檔的第一行或者第二行顯示地指定編碼格式:

或者是:

在python中和字串相關的資料型別,分別是strunicode兩種,他們都是basestring的子類,可見str與unicode是兩種不同型別的字串物件。

對於同一個漢字“好”,用str表示時,它對應的就是utf-8編碼的’xe5xa5xbd’,而用unicode表示時,他對應的符號就是u’u597d’,與u"好"是等同的。需要補充一點的是,str型別的字元其具體的編碼格式是UTF-8還是GBK,還是其他格式,根據作業系統相關。比如在Windows系統中,cmd命令列中顯示的:

而在Linux系統的命令列中顯示的是:

不論是Python3x、Java還是其他程式語言,Unicode編碼都成為語言的預設編碼格式,而資料最後儲存到介質中的時候,不同的介質可有用不同的方式,有些人喜歡用UTF-8,有些人喜歡用GBK,這都無所謂,只要平臺統一的編碼規範,具體怎麼實現並不關心。

encode

str與unicode的轉換

那麼在Python中str和unicode之間是如何轉換的呢?這兩種型別的字串型別之間的轉換就是靠這兩個方法decodeencode

py-encode

這個’xe5xa5xbd’就是unicode u’好’通過函式encode編碼得到的UTF-8編碼的str型別的字串。反之亦然,str型別的c通過函式decode解碼成unicode字串d。

str(s)與unicode(s)

str(s)和unicode(s)是兩個工廠方法,分別返回str字串物件和unicode字串物件,str(s)是s.encode(‘ascii’)的簡寫。實驗:

上面s3是unicode型別的字串,str(s3)相當於是執行s3.encode(‘ascii’)因為“你好”兩個漢字不能用ascii碼來表示,所以就報錯了,指定正確的編碼:s3.encode(‘gbk’)或者s3.encode(“utf-8”)就不會出現這個問題了。類似的unicode有同樣的錯誤:

unicode(s4)等效於s4.decode(‘ascii’),因此要正確的轉換就要正確指定其編碼s4.decode(‘gbk’)或者s4.decode(“utf-8”)。

亂碼

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

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

decode-encode

其他技巧

對於如unicode形式的字串(str型別):

轉換成真正的unicode需要使用:

測試:

以上程式碼和概念都是基於Python2.x。

參考:

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

PYTHON編碼的前世今生

相關文章