字元編碼ASCII,Unicode和UTF-8
字元編碼介紹
文字,他們通常指顯示在螢幕上的字元或者其他的記號;但是計算機不能直接處理這些字元和標記;它們只認識位(bit)和位元組(byte)。實際上,從螢幕上的每一塊文字都是以某種字元編碼(character encoding)的方式儲存的。粗略地說就是,字元編碼提供一種對映,使螢幕上顯示的內容和記憶體、磁碟記憶體儲的內容對應起來。有許多種不同的字元編碼,有一些是為特定的語言,比如俄語、中文或者英語,設計、優化的,另外一些則可以用於多種語言的編碼。
在實際操作中則會比上邊描述的更復雜一些。許多字元在幾種編碼裡是共用的,但是在實際的記憶體或者磁碟上,不同的編碼方式可能會使用不同的位元組序列來儲存他們。所以,你可以把字元編碼當做一種解碼金鑰。當有人給你一個位元組序列 -- 檔案,網頁,或者別的什麼 -- 並且告訴你它們是文字時,就需要知道他們使用了何種編碼方式,然後才能將這些位元組序列解碼成字元。如果他們給的是錯誤的金鑰或者根本沒有給你金鑰,那就得自己來破解這段編碼,這可是一個艱難的任務。有可能你使用了錯誤的解碼方式,然後出現一些莫名其妙的結果。
你肯定見過這樣的網頁,在撇號("
)該出現的地方被奇怪的像問號的字元替代了。這種情況通常意味著頁面的作者沒有正確的宣告其使用的編碼方式,瀏覽器只能自己來猜測,結果就是一些正確的和意料之外的字元的混合體。如果原文是英語,那只是不方便閱讀而已;在其他的語言環境下,結果可能是完全不可讀的。
現有的字元編碼各類給世界上每種主要的語言都提供了編碼方案。由於每種語言的各不相同,而且在以前記憶體和硬碟都很昂貴,所以每種字元編碼都為特定的語言做了優化。上邊這句話的意思是,每種編碼都使用數字(0–255)來代表這種語言的字元。比如,你也許熟悉ASCII編碼,它將英語中的字元都當做從0–127的數字來儲存。(65表示大寫的A,97表示小寫的a,&
西歐的一些語言,比如法語,西班牙語和德語等,比英語有更多的字母。或者,更準確的說,這些語言含有與變音符號(diacritical marks)組合起來的字母,像西班牙語裡的ñ
。這些語言最常用的編碼方式是CP-1252,又叫做windows-1252,因為它在微軟的視窗作業系統上被廣泛使用。CP-1252和ASCII在0–127這個範圍內的字元是一樣的,但是CP-1252為ñ
(n-with-a-tilde-over-it, 241),Ü
然而,像中文,日語和韓語等語言,他們的字元如此之多而不得不需要多位元組編碼的字符集。即,使用兩個位元組的數字(0–255)代表每個字元。但是就跟不同的單位元組編碼方式一樣,多位元組編碼方式之間也有同樣的問題,即他們使用的數字是相同的,但是表達的內容卻不同。相對於單位元組編碼方式它們只是使用的數字範圍更廣一些,因為有更多的字元需要表示。
在沒有網路的時代,文字由自己輸入,偶爾才會打印出來,大多數情況下使用以上的編碼方案是可行的。那時沒有太多的純文字。原始碼使用ASCII編碼,其他人也都使用字處理器,這些字處理器定義了他們自己的格式(非文字的),這些格式會連同字元編碼資訊和風格樣式一起記錄其中,&c。人們使用與原作者相同的字處理軟體讀取這些文件,所以或多或少地能夠使用。
現在,我們考慮一下像email和web這樣的全球網路的出現。大量的“純文字”檔案在全球範圍內流轉,它們在一臺電腦上被撰寫出來,通過第二臺電腦進行傳輸,最後在另外一臺電腦上顯示。計算機只能識別數字,但是這些數字可能表達的是其他的東西。Oh no! 怎麼辦呢。。好吧,那麼系統必須被設計成在每一段“純文字”上都搭載編碼資訊。記住,編碼方式是將計算機可讀的數字對映成人類可讀的字元的解碼金鑰。失去解碼金鑰則意味著混亂不清的,莫名其妙的資訊,或者更糟。
現在我們考慮嘗試把多段文字儲存在同一個地方,比如放置所有收到郵件的資料庫。這仍然需要對每段文字儲存其相關的字元編碼資訊,只有這樣才能正確地顯示它們。這很困難嗎?試試搜尋你的email資料庫,這意味著需要在執行時進行編碼之間的轉換。很有趣是吧…
現在我們來分析另外一種可能性,即多語言文件,同一篇文件裡來自幾種不同語言的字元混在一起。(提示:處理這樣文件的程式通常使用轉義符在不同的模式(modes)之間切換。噗!現在是俄語 koi8-r 模式,所以241代表 Я;噗噗!現在到了Mac Greek模式,所以241代表 ώ。)當然,你也會想要搜尋這些文件。根本就沒有所謂的純文字。
ASCII碼
ASCII碼一共規定了128個字元的編碼,比如空格"SPACE"是32(二進位制00100000),大寫的字母A是65(二進位制01000001)。這128個符號(包括32個不能打印出來的控制符號),只佔用了一個位元組的後面7位,最前面的1位統一規定為0。
非ASCII編碼
英語用128個符號編碼就夠了,但是用來表示其他語言,128個符號是不夠的。比如,在法語中,字母上方有注音符號,它就無法用ASCII碼錶示。於是,一些歐洲國家就決定,利用位元組中閒置的最高位編入新的符號。比如,法語中的é的編碼為130(二進位制10000010)。這樣一來,這些歐洲國家使用的編碼體系,可以表示最多256個符號。
然而不同的國家有不同的字母,即使都使用256個符號的編碼方式,代表的字母卻不一樣。比如,130在法語編碼中代表了é,在希伯來語編碼中卻代表了字母Gimel (ג),在俄語編碼中又會代表另一個符號。但是不管怎樣,所有這些編碼方式中,0--127表示的符號是一樣的,不一樣的只是128--255的這一段。
至於亞洲國家的文字,使用的符號就更多了,漢字就多達10萬左右。一個位元組只能表示256種符號,肯定是不夠的,就必須使用多個位元組表達一個符號。比如,簡體中文常見的編碼方式是GB2312,使用兩個位元組表示一個漢字,所以理論上最多可以表示256x256=65536個符號。
雖然都是用多個位元組表示一個符號,但是GB類的漢字編碼與後文的Unicode和UTF-8是毫無關係的。
Unicode編碼
Unicode
如上,世界上存在著多種編碼方式,同一個二進位制數字可以被解釋成不同的符號。因此,要想開啟一個文字檔案,就必須知道它的編碼方式,否則用錯誤的編碼方式解讀,就會出現亂碼。為什麼電子郵件常常出現亂碼?就是因為發信人和收信人使用的編碼方式不一樣。
可以想象,如果有一種編碼,將世界上所有的符號都納入其中。每一個符號都給予一個獨一無二的編碼,那麼亂碼問題就會消失。這就是Unicode,就像它的名字都表示的,這是一種所有符號的編碼。
Unicode當然是一個很大的集合,現在的規模可以容納100多萬個符號。每個符號的編碼都不一樣,比如,U+0639表示阿拉伯字母Ain,U+0041表示英語的大寫字母A,U+4E25表示漢字"嚴"。具體的符號對應表,可以查詢unicode.org,或者專門的漢字對應表。
Note:Unicode物件並沒有編碼。它們使用Unicode,一個一致的,通用的字元編碼集。 當你在Python中處理Unicode物件的時候,你可以直接將它們混合使用和互相匹配而不必去考慮編碼細節。
Unicode編碼系統為表達任意語言的任意字元而設計。它使用4位元組的數字來表達每個字母、符號,或者表意文字(ideograph)。每個數字代表唯一的至少在某種語言中使用的符號。(並不是所有的數字都用上了,但是總數已經超過了65535,所以2個位元組的數字是不夠用的。)被幾種語言共用的字元通常使用相同的數字來編碼,除非存在一個在理的語源學(etymological)理由使不這樣做。不考慮這種情況的話,每個字元對應一個數字,每個數字對應一個字元。即不存在二義性。不再需要記錄模式了。U+0041總是代表"A",即使這種語言沒有"A"這個字元。初次面對這個創想,它看起來似乎很偉大。一種編碼方式即可解決所有問題。文件可包含多種語言。不再需要在各種編碼方式之間進行模式轉換。但是很快,一個明顯的問題跳到我們面前。4個位元組?只為了單獨一個字元 這似乎太浪費了,特別是對像英語和西語這樣的語言,他們只需要不到1個位元組即可以表達所需的字元。事實上,對於以象形為基礎的語言(比如中文)這種方法也有浪費,因為這些語言的字元也從來不需要超過2個位元組即可表達。
有一種Unicode編碼方式每1個字元使用4個位元組。它叫做UTF-32,因為32位 = 4位元組。UTF-32是一種直觀的編碼方式;它收錄每一個Unicode字元(4位元組數字)然後就以那個數字代表該字元。這種方法有其優點,最重要的一點就是可以在常數時間內定位字串裡的第N個字元,因為第N個字元從第4×Nth個位元組開始。另外,它也有其缺點,最明顯的就是它使用4個詭異的位元組來儲存每個詭異的字元…
儘管有Unicode字元非常多,但是實際上大多數人不會用到超過前65535個以外的字元。因此,就有了另外一種Unicode編碼方式,叫做UTF-16(因為16位 = 2位元組)。UTF-16將0–65535範圍內的字元編碼成2個位元組,如果真的需要表達那些很少使用的星芒層(astral plane)內超過這65535範圍的Unicode字元,則需要使用一些詭異的技巧來實現。UTF-16編碼最明顯的優點是它在空間效率上比UTF-32高兩倍,因為每個字元只需要2個位元組來儲存(除去65535範圍以外的),而不是UTF-32中的4個位元組。並且,如果我們假設某個字串不包含任何星芒層中的字元,那麼我們依然可以在常數時間內找到其中的第N個字元,直到它不成立為止這總是一個不錯的推斷…
但是對於UTF-32和UTF-16編碼方式還有一些其他不明顯的缺點。不同的計算機系統會以不同的順序儲存位元組。這意味著字元U+4E2D
在UTF-16編碼方式下可能被儲存為4E 2D
或者2D 4E
,這取決於該系統使用的是大尾端(big-endian)還是小尾端(little-endian)。(對於UTF-32編碼方式,則有更多種可能的位元組排列。)只要文件沒有離開你的計算機,它還是安全的 -- 同一臺電腦上的不同程式使用相同的位元組順序(byte order)。但是當我們需要在系統之間傳輸這個文件的時候,也許在全球資訊網中,我們就需要一種方法來指示當前我們的位元組是怎樣儲存的。不然的話,接收文件的計算機就無法知道這兩個位元組4E 2D
表達的到底是U+4E2D
還是U+2D4E
。
為了解決這個問題,多位元組的Unicode編碼方式定義了一個位元組順序標記(Byte Order Mark),它是一個特殊的非列印字元,你可以把它包含在文件的開頭來指示你所使用的位元組順序。對於UTF-16,位元組順序標記是U+FEFF
。如果收到一個以位元組FF FE
開頭的UTF-16編碼的文件,你就能確定它的位元組順序是單向的(one way)的了;如果它以FE FF
開頭,則可以確定位元組順序反向了。
不過,UTF-16還不夠完美,特別是要處理許多ASCII字元時。如果仔細想想的話,甚至一箇中文網頁也會包含許多的ASCII字元 -- 所有包圍在可列印中文字元周圍的元素(element)和屬性(attribute)。能夠在常數時間內找到第Nth個字元當然非常好,但是依然存在著糾纏不休的星芒層字元的問題,這意味著你不能保證每個字元都是2個位元組長,所以,除非你維護著另外一個索引,不然就不能真正意義上的在常數時間內定位第N個字元。另外,朋友,世界上肯定還存在很多的ASCII文字。
Unicode的問題
需要注意的是,Unicode只是一個符號集,它只規定了符號的二進位制程式碼,卻沒有規定這個二進位制程式碼應該如何儲存。
比如,漢字"嚴"的unicode是十六進位制數4E25,轉換成二進位制數足足有15位(100111000100101),也就是說這個符號的表示至少需要2個位元組。表示其他更大的符號,可能需要3個位元組或者4個位元組,甚至更多。
這裡就有兩個嚴重的問題
第一個問題是,如何才能區別Unicode和ASCII?計算機怎麼知道三個位元組表示一個符號,而不是分別表示三個符號呢?
第二個問題是,我們已經知道,英文字母只用一個位元組表示就夠了,如果Unicode統一規定,每個符號用三個或四個位元組表示,那麼每個英文字母前都必然有二到三個位元組是0,這對於儲存來說是極大的浪費,文字檔案的大小會因此大出二三倍,這是無法接受的。
它們造成的結果是:
1)出現了Unicode的多種儲存方式,也就是說有許多種不同的二進位制格式,可以用來表示Unicode。
2)Unicode在很長一段時間內無法推廣,直到網際網路的出現。
為了更好的理解,一般你可以認為unicode是沒有編碼的,只是一種表示;unicode進行encode編碼後才成為utf-8, gbk等等,同時utf-8, gbk等等解碼後就變成unicode。
另外一些人琢磨著這些問題,他們找到了一種解決方法:
UTF-8
UTF-8是一種為Unicode設計的變長(variable-length)編碼系統。即,不同的字元可使用不同數量的位元組編碼。對於ASCII字元(A-Z,&c.)UTF-8僅使用1個位元組來編碼。事實上,UTF-8中前128個字元(0–127)使用的是跟ASCII一樣的編碼方式。像ñ和ö這樣的擴充套件拉丁字元(Extended Latin)則使用2個位元組來編碼。(這裡的位元組並不是像UTF-16中那樣簡單的Unicode編碼點(unicode code point);它使用了一些位變換(bit-twiddling)。)中文字元比如則佔用了3個位元組。很少使用的星芒層字元則佔用4個位元組。
缺點:因為每個字元使用不同數量的位元組編碼,所以尋找串中第N個字元是一個O(N)複雜度的操作 -- 即,串越長,則需要更多的時間來定位特定的字元。同時,還需要位變換來把字元編碼成位元組,把位元組解碼成字元。
優點:在處理經常會用到的ASCII字元方面非常有效。在處理擴充套件的拉丁字符集方面也不比UTF-16差。對於中文字元來說,比UTF-32要好。同時,(在這一條上你得相信我,因為我不打算給你展示它的數學原理。)由位操作的天性使然,使用UTF-8不再存在位元組順序的問題了。一份以UTF-8編碼的文件在不同的計算機之間是一樣的位元流。
網際網路的普及,強烈要求出現一種統一的編碼方式。UTF-8就是在網際網路上使用最廣的一種Unicode的實現方式。
其他實現方式還包括UTF-16(字元用兩個位元組或四個位元組表示)和UTF-32(字元用四個位元組表示),不過在網際網路上基本不用。UTF-8是Unicode的實現方式之一。
UTF-8最大的一個特點,就是它是一種變長的編碼方式。它可以使用1~4個位元組表示一個符號,根據不同的符號而變化位元組長度。
UTF-8的編碼規則
1)對於單位元組的符號,位元組的第一位設為0,後面7位為這個符號的unicode碼。因此對於英語字母,UTF-8編碼和ASCII碼是相同的。
2)對於n位元組的符號(n>1),第一個位元組的前n位都設為1,第n+1位設為0,後面位元組的前兩位一律設為10。剩下的沒有提及的二進位制位,全部為這個符號的unicode碼。
下表總結了編碼規則,字母x表示可用編碼的位。
Unicode符號範圍 | UTF-8編碼方式
(十六進位制) | (二進位制)
--------------------+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
跟據上表,解讀UTF-8編碼非常簡單。如果一個位元組的第一位是0,則這個位元組單獨就是一個字元;如果第一位是1,則連續有多少個1,就表示當前字元佔用多少個位元組。
下面,還是以漢字"嚴"為例,演示如何實現UTF-8編碼。
已知"嚴"的unicode是4E25(100111000100101),根據上表,可以發現4E25處在第三行的範圍內(0000 0800-0000 FFFF),因此"嚴"的UTF-8編碼需要三個位元組,即格式是"1110xxxx 10xxxxxx 10xxxxxx"。然後,從"嚴"的最後一個二進位制位開始,依次從後向前填入格式中的x,多出的位補0。這樣就得到了,"嚴"的UTF-8編碼是"11100100 10111000 10100101",轉換成十六進位制就是E4B8A5。
Unicode與UTF-8之間的轉換
通過上一節的例子,可以看到"嚴"的Unicode碼是4E25{unicode表示是這麼表示,對於全域性可能不是最優的吧?},UTF-8編碼是E4B8A5{unicode的一種比較優化的表示?},兩者是不一樣的。它們之間的轉換可以通過程式實現。
在Windows平臺下,有一個最簡單的轉化方法,就是使用內建的記事本小程式Notepad.exe。開啟檔案後,點選"檔案"選單中的"另存為"命令,會跳出一個對話方塊,在最底部有一個"編碼"的下拉條。
裡面有四個選項:ANSI,Unicode,Unicode big endian 和 UTF-8。
1)ANSI是預設的編碼方式。對於英文檔案是ASCII編碼,對於簡體中文檔案是GB2312編碼(只針對Windows簡體中文版,如果是繁體中文版會採用Big5碼)。
2)Unicode編碼指的是UCS-2編碼方式,即直接用兩個位元組存入字元的Unicode碼。這個選項用的little endian格式。
3)Unicode big endian編碼與上一個選項相對應。我在下一節會解釋little endian和big endian的涵義。
4)UTF-8編碼,也就是上一節談到的編碼方法。
選擇完"編碼方式"後,點選"儲存"按鈕,檔案的編碼方式就立刻轉換好了。
當然使用notepad++等更高階的文字編輯器轉換更簡單,自己可以試試。
中文字元編碼標準
GB2312
1980年,中國製定了GB2312-80,一共收錄了 7445 個字元,包括 6763 個漢字和 682 個其它符號。
GB2312-80,簡稱為GB2312。
在 Windows 中的內碼表(Code Page)是 CP936。
GBK
微軟,對GB2312-80的擴充套件,即利用GB 2312-80未使用的編碼空間,收錄所有的GB 13000.1-93和Unicode 1.1之中的漢字全部字元,制定了GBK編碼。
GBK 收錄了 21886 個符號,它分為漢字區和圖形符號區。漢字區包括 21003 個字元。
GBK 作為對 GB2312 的擴充套件,在現在的 Windows 系統中仍然使用內碼表 CP936 表示,但是同樣的 936 的內碼表跟一開始的 936 的內碼表只支援 GB2312 編碼不同,現在的 936 內碼表支援 GBK 的編碼,GBK 同時也向下相容GB2312 編碼。
所以,技術編碼上,GBK相容舊的GB2312,但是編碼方式和GB13000不同,不相容GB13000,但是所包含文字上,算是和GB13000相同。
技術編碼方面上,演化順序為:ASCII ⇒ GB2312 ⇒ GBK ⇒ GB18030
中文字元相關編碼標準
編碼標準 | 別名 | 標準所屬 | 包含字元 |
---|---|---|---|
ASCII | 國際通用 | ||
GB2312 | 微軟Windows中以前的CP936 | 中國大陸 | 6763 個漢字和 682 個其它符號 |
Unicode 1.1 | 國際通用 | 20,902個字元 | |
GB13000 | 中國大陸 | 20,902個字元 | |
GBK | 微軟Windows中現在的CP936 | 微軟 | 21886 個符號 |
GB18030 | 微軟Windows中的CP54936 | 中國大陸 | 27484 個漢字+其他少數民族字元 |
附
字元編碼筆記