閱讀302 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Python 編碼的前世今生

為了搞清字符編碼,我們得從計算機的起源開始,計算機中的所有數據,不論是文字、圖片、視頻、還是音頻文件,本質上最終都是按照類似 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

EASCII (ISO/8859-1)

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

cp437

cp437

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

iso8859-1

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 2.x。

現在總算把理論說完了。再來說說 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; see http://www.python.org/ ps/pep-0263.html for details

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


  1. # coding=utf-8

或者是:


  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-

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


  1. basestring
  2. / \
  3. / \
  4. str unicode

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


  1. # windows終端
  2. >>> a = '好'
  3. >>> type(a)
  4. <type 'str'>
  5. >>> a
  6. '\xba\xc3'

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


  1. # linux終端
  2. >>> a='好'
  3. >>> type(a)
  4. <type 'str'>
  5. >>> a
  6. '\xe5\xa5\xbd'
  7. >>> b=u'好'
  8. >>> type(b)
  9. <type 'unicode'>
  10. >>> b
  11. u'\u597d'

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

encode

encode

str 與 unicode 的轉換

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

py-encode

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. '\xe5\xa5\xbd'
  10. >>> d = c.decode('utf-8')
  11. >>> type(d)
  12. <type 'unicode'>
  13. >>> d
  14. u'\u597d'

這個'\xe5\xa5\xbd'就是 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’) 的簡寫。實驗:


  1. >>> s3 = u"你好"
  2. >>> s3
  3. u'\u4f60\u597d'
  4. >>> str(s3)
  5. Traceback (most recent call last):
  6. File "<stdin>", 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 "<stdin>", 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. '\xe5\xa5\xbd'
  5. >>> b=a.decode("utf-8")
  6. >>> b
  7. u'\u597d'
  8. >>> c=b.encode("gbk")
  9. >>> c
  10. '\xba\xc3'
  11. >>> print c
  12. ��

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

decode-encode

decode-encode

其他技巧

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


  1. s = 'id\u003d215903184\u0026index\u003d0\u0026st\u003d52\u0026sid'

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


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

測試:


  1. >>> s = 'id\u003d215903184\u0026index\u003d0\u0026st\u003d52\u0026sid\u003d95000\u0026i'
  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. >>>

原文發布時間為:2016-09-29

本文來自雲棲社區合作夥伴“Linux中國”

最後更新:2017-06-06 16:31:55

  上一篇:go  5 個值得了解的 Linux 服務器發行版
  下一篇:go  人工智能還能當算命師?能預測人類壽命?