Python 編碼的前世今生

轉載自:http://foofish.net/blog/111/python-character-encode作者: _zhijun

本文導航

  • -ASCII

  • -EASCII (ISO/8859-1)

  • -GBK

  • -Unicode

  • -UTF-8

  • -Python 字元編碼

  • -str 與 unicode 的轉換

  • -str(s) 與 unicode(s)

  • -亂碼

  • -其他技巧

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

    一旦走上了編程之路,如果你不把編碼問題搞清楚,那麼它將像幽靈一般糾纏你整個職業生涯,各種靈異事件會接踵而來,揮之不去。只有充分發揮程序員死磕到底的精神你才有可能徹底擺脫編碼問題帶來的煩惱。

    我第一次遇到編碼問題是寫 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-code[2]。

    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。GB2312 的出現,基本滿足了漢字的計算機處理需要,它所收錄的漢字已經覆蓋中國大陸 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 萬個,因此 Unicode 4.0 規範定義了一組附加的字元編碼,UCS-4 就是用 4 個位元組(實際上只用了 31 位,最高位必須為 0)。

    Unicode 理論上完全可以涵蓋一切語言所用的符號。世界上任何一個字元都可以用一個 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 碼填充。

    以漢字「」為例,「」對應的 Unicode 是597D,對應的區間是0000 0800--0000 FFFF,因此它用 UTF-8 表示時需要用 3 個位元組來存儲,597D用二進位表示是:0101100101111101,填充到1110xxxx 10xxxxxx 10xxxxxx得到11100101 10100101 10111101,轉換成 16 進位:E5A5BD,因此「」的 Unicode 「597D」對應的 UTF-8 編碼是「E5A5BD」。

    1. 中文 好

    2. unicode 0101 100101 111101

    3. 編碼規則 1110xxxx 10xxxxxx 10xxxxxx

    4. --------------------------

    5. utf-8 11100101 10100101 10111101

    6. --------------------------

    7. 16進位utf-8 e 5 a 5 b d

    Python 字元編碼

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

    1. >>> import sys

    2. >>> sys.getdefaultencoding()

    3. "ascii"

    所以在 Python 源代碼文件中如果不顯式地指定編碼的話,將出現語法錯誤

    1. #test.py

    2. print "你好"

    上面是 test.py 腳本,運行python test.py就會包如下錯誤:

    1. File 「test.py」, line 1 yntaxError: Non-ASCII character 『xe4′ in file test.py on line 1, but no encoding declared; seehttp://www.python.org/ps/pep-0263.html for details

    為了在源代碼中支持非 ASCII 字元,必須在源文件的第一行或者第二行顯示地指定編碼格式:

    1. # coding=utf-8

    或者是:

    1. #!/usr/bin/python

    2. # -*- coding: utf-8 -*-

    在 Python 中和字元串相關的數據類型,分別是str、unicode兩種,他們都是basestring的子類,可見 str 與 unicode 是兩種不同類型的字元串對象。

    1. basestring

    2. /

    3. /

    4. str unicode

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

    1. # windows終端

    2. >>> a = "好"

    3. >>> type(a)

    4. type "str">

    5. >>> a

    6. "xbaxc3"

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

    1. # linux終端

    2. >>> a="好"

    3. >>> type(a)

    4. type "str">

    5. >>> a

    6. "xe5xa5xbd"

    7. >>> b=u"好"

    8. >>> type(b)

    9. type "unicode">

    10. >>> b

    11. u"u597d"

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

    encode

    str 與 unicode 的轉換

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

    py-encode

    1. #從str類型轉換到unicode

    2. s.decode(encoding) =====> type "str"> to type "unicode">

    3. #從unicode轉換到str

    4. u.encode(encoding) =====> type "unicode"> to type "str">

    5. >>> c = b.encode("utf-8")

    6. >>> type(c)

    7. type "str">

    8. >>> c

    9. "xe5xa5xbd"

    10. >>> d = c.decode("utf-8")

    11. >>> type(d)

    12. type "unicode">

    13. >>> d

    14. u"u597d"

    這個"xe5xa5xbd"就是 Unicodeu"好"通過函數encode編碼得到的 UTF-8 編碼的 str 類型的字元串。反之亦然,str 類型的 c 通過函數decode解碼成 Unicode 字元串 d。

    str(s) 與 unicode(s)

    str(s)unicode(s)是兩個工廠方法,分別返回 str 字元串對象和 Unicode 字元串對象,str(s)s.encode(『ascii』)的簡寫。實驗:

    1. >>> s3 = u"你好"

    2. >>> s3

    3. u"u4f60u597d"

    4. >>> str(s3)

    5. Traceback (most recent call last):

    6. File "", line 1, in module>

    7. UnicodeEncodeError: "ascii" codec can"t encode characters in position 0-1: ordinal not in range(128)

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

    1. >>> s4 = "你好"

    2. >>> unicode(s4)

    3. Traceback (most recent call last):

    4. File "", line 1, in module>

    5. UnicodeDecodeError: "ascii" codec can"t decode byte 0xc4 in position 0: ordinal not in range(128)

    6. >>>

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

    亂碼

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

    1. # encoding: utf-8

    2. >>> a="好"

    3. >>> a

    4. "xe5xa5xbd"

    5. >>> b=a.decode("utf-8")

    6. >>> b

    7. u"u597d"

    8. >>> c=b.encode("gbk")

    9. >>> c

    10. "xbaxc3"

    11. >>> print c

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

    decode-encode

    其他技巧

    對於如 Unicode 形式的字元串(str 類型):

    1. s = "idu003d215903184u0026indexu003d0u0026stu003d52u0026sid"

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

    1. s.decode("unicode-escape")

    測試:

    1. >>> s = "idu003d215903184u0026indexu003d0u0026stu003d52u0026sidu003d95000u0026i"

    2. >>> print(type(s))

    3. type "str">

    4. >>> s = s.decode("unicode-escape")

    5. >>> s

    6. u"id=215903184&index=0&st=52&sid=95000&i"

    7. >>> print(type(s))

    8. type "unicode">

    9. >>>

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

    參考:

  • https://www.python.org/dev/peps/pep-0263/

  • http://www.liaoxuefengcom/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/001386819196283586a37629844456ca7e5a7faa9b94ee8000

  • http://www.fmddlmyy.cn/text6.html

  • 轉載自:http://foofish.net/blog/111/python-character-encode

    轉載自:http://foofish.net/blog/111/python-character-encode作者: _zhijun

    推薦閱讀:

    windows下anaconda 安裝報錯, errno9,怎麼解決?
    昨天看球時,球迷都說了啥——彈幕抓取與分析
    python re.sub 應用
    【Python3網路爬蟲開發實戰】2.3-爬蟲的基本原理

    TAG:Python | 編碼 | 前世今生 |