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
另外一種被廣泛使用的 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” 。

中文 好
unicode 0101 100101 111101
編碼規則 1110xxxx 10xxxxxx 10xxxxxx
————————–
utf-8 11100101 10100101 10111101
————————–
16 進位制 utf-8 e 5 a 5 b d

Python 字元編碼
注:以下程式碼和概念都是基於 Python 2.x 。
現在總算把理論說完了。再來說說 Python 中的編碼問題。 Python 的誕生時間比 Unicode 要早很多,Python 的預設編碼是 ASCII 。

>>> import sys
>>> sys.getdefaultencoding()
‘ascii’

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

#test.py
print “ 你好”

上面是 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 字元,必須在原始檔的第一行或者第二行顯示地指定編碼格式:

# coding=utf-8

或者是:

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

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

basestring
/
/
str unicode

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

# windows 終端
>>> a = ‘ 好’
>>> type(a)

>>> a
‘xbaxc3′

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

# linux 終端
>>> a=’ 好’
>>> type(a)

>>> a
‘xe5xa5xbd’

>>> b=u’ 好’
>>> type(b)

>>> b
u’u597d’

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

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

py-encode

#從 str 型別轉換到 unicode
s.decode(encoding) =====> to
#從 unicode 轉換到 str
u.encode(encoding) =====> to

>>> c = b.encode(‘utf-8’)
>>> type(c)

>>> c
‘xe5xa5xbd’

>>> d = c.decode(‘utf-8’)
>>> type(d)

>>> d
u’u597d’

這個’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 = u” 你好”
>>> s3
u’u4f60u597d’
>>> str(s3)
Traceback (most recent call last):
File ““, line 1, in
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 有同樣的錯誤:

>>> s4 = “ 你好”
>>> unicode(s4)
Traceback (most recent call last):
File ““, line 1, in
UnicodeDecodeError: ‘ascii’ codec can’t decode byte 0xc4 in position 0: ordinal not in range(128)
>>>

unicode(s4) 等效於 s4.decode(‘ascii’),因此要正確的轉換就要正確指定其編碼 s4.decode(‘gbk’) 或者 s4.decode(‘utf-8′) 。
亂碼
所有出現亂碼的原因都可以歸結為字元經過不同編碼解碼在編碼的過程中使用的編碼格式不一致,比如:

# encoding: utf-8

>>> a=’ 好’
>>> a
‘xe5xa5xbd’
>>> b=a.decode(“utf-8”)
>>> b
u’u597d’
>>> c=b.encode(“gbk”)
>>> c
‘xbaxc3’
>>> print c
��

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

decode-encode
其他技巧
對於如 Unicode 形式的字串(str 型別):

s = ‘idu003d215903184u0026indexu003d0u0026stu003d52u0026sid’

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

s.decode(‘unicode-escape’)

測試:

>>> s = ‘idu003d215903184u0026indexu003d0u0026stu003d52u0026sidu003d95000u0026i’
>>> print(type(s))

>>> s = s.decode(‘unicode-escape’)
>>> s
u’id=215903184&index=0&st=52&sid=95000&i’
>>> print(type(s))

>>>