導讀 | 一旦走上了程式設計之路,如果你不把編碼問題搞清楚,那麼它將像幽靈一般糾纏你整個職業生涯,各種靈異事件會接踵而來,揮之不去。只有充分發揮程式設計師死磕到底的精神你才有可能徹底擺脫編碼問題帶來的煩惱。 |
我第一次遇到編碼問題是寫 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 叫EASCII ,顧名思義,它是在ASCII的基礎上擴展而來,把原來的7 位擴充到8 位,它完全相容於ASCII,擴展出來的符號包括表格符號、計算符號、希臘字母和特殊的拉丁符號。然而EASCII 時代是一個混亂的時代,大家沒有統一標準,他們各自把最高位元按照自己的標準實現了自己的一套字元編碼標準,比較著名的就有 CP437, CP437 是Windows 系統中使用的字元編碼,如下圖:
另外一種被廣泛使用的EASCII 還有 ISO/8859-1(Latin-1),它是國際標準化組織(ISO)及國際電工委員會(IEC)共同製定的一系列8位字符集的標準,ISO/8859-1 只繼承了CP437 字符編碼的128-159 之間的字符,所以它是從160 開始定義的,不幸的是這些眾多的ASCII 擴充字集之間互不相容。
隨著時代的進步,電腦開始普及到千家萬戶,比爾蓋茲讓每個人桌面都有一台電腦的夢想得以實現。但是電腦進入中國不得不面臨的一個問題就是字元編碼,雖然咱們國家的漢字是人類使用頻率最多的文字,漢字博大精深,常見的漢字就有成千上萬,這已經大大超出了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(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 ,因此「好編碼」的對應程式的「Uni」50A4m
現在總算把理論說完了。再來談談 Python 中的編碼問題。 Python 的出生時間比 Unicode 早很多,Python 的預設編碼是ASCII。
>>> import sys
#
>>> sys.getdefaultencoding()
'ascii'
#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;為了在原始程式碼中支援非 ASCII 字符,必須在原始檔案的第一行或第二行顯示地指定編碼格式:
# coding=utf-8或是:
#!/usr/bin/python # -*- coding: utf-8 -*-在Python 中和字串相關的資料類型,分別是 str、unicode 兩種,他們都是basestring 的子類,可見str 與unicode 是兩種不同類型的字串物件。
basestring / / / / str unicode對於同一個漢字“好”,用str 表示時,它對應的就是UTF-8 編碼'/xe5/xa5/xbd',而用 Unicode 表示時,它對應的符號就是u'/u597d',與u "好"是等同的。需要補充一點的是,str 類型的字元其具體的編碼格式是 UTF-8 還是 GBK,還是其它格式,根據作業系統相關。例如在 Windows 系統中,cmd 命令列中顯示的:
# windows终端 >>> a = '好' >>> type(a) <type 'str'> >>> a '/xba/xc3'而在 Linux 系統的命令列中顯示的是:
# linux终端 >>> a='好' >>> type(a) <type 'str'> >>> a '/xe5/xa5/xbd' >>> b=u'好' >>> type(b) <type 'unicode'> >>> b u'/u597d'不論是Python3x、Java 還是其他程式語言,Unicode 編碼都成為了語言的預設編碼格式,而資料最後儲存到媒體的時候,不同的媒體可有用不同的方式,有些人喜歡用UTF-8 ,有些人喜歡用GBK,這都無所謂,只要平台統一的編碼規範,具體怎麼實現並不關心。
#str 與 unicode 的轉換
那麼在 Python 中 str 和 unicode 之間是如何轉換的呢?這兩種類型的字串類型之間的轉換就是靠這兩個方法:decode 和encode。
#
#从str类型转换到unicode s.decode(encoding) =====> <type 'str'> to <type 'unicode'> #从unicode转换到str u.encode(encoding) =====> <type 'unicode'> to <type 'str'> >>> c = b.encode('utf-8') >>> type(c) <type 'str'> >>> c '/xe5/xa5/xbd' >>> d = c.decode('utf-8') >>> type(d) <type 'unicode'> >>> d 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’) 的簡寫。實驗:
>>> s3 = u"你好" >>> s3 u'/u4f60/u597d' >>> str(s3) Traceback (most recent call last): File "<stdin>", line 1, in <module> 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 "<stdin>", line 1, in <module> UnicodeDecodeError: 'ascii' codec can't decode byte 0xc4 in position 0: ordinal not in range(128) >>>unicode(s4)等效於s4.decode('ascii')
所有出現亂碼的原因都可以歸結為字元經過不同編碼解碼在編碼的過程中使用的編碼格式不一致,例如:
# encoding: utf-8 >>> a='好' >>> a '/xe5/xa5/xbd' >>> b=a.decode("utf-8") >>> b u'/u597d' >>> c=b.encode("gbk") >>> c '/xba/xc3' >>> print cUTF-8 編碼的字元'好'佔用3 個位元組,解碼成Unicode 後,如果再用 GBK 來解碼後,只有2 個位元組的長度了,最後出現了亂碼的問題,因此防止亂碼的最好方法就是始終堅持使用同一種編碼格式對字元進行編碼和解碼操作。
#其他技巧
對於如 Unicode 形式的字串(str 類型):
s = 'id/u003d215903184/u0026index/u003d0/u0026st/u003d52/u0026sid'轉換成真正的 Unicode 需要使用:
s.decode('unicode-escape')測試:
>>> s = 'id/u003d215903184/u0026index/u003d0/u0026st/u003d52/u0026sid/u003d95000/u0026i' >>> print(type(s)) <type 'str'> >>> s = s.decode('unicode-escape') >>> s u'id=215903184&index=0&st=52&sid=95000&i' >>> print(type(s)) <type 'unicode'> >>>以上程式碼和概念都是基於 Python2.x。 ###
以上是Python 程式設計的歷史演變的詳細內容。更多資訊請關注PHP中文網其他相關文章!