以前真的未就計算字元編碼有過什麼深入的學習探究,這次學習也是源於客戶的一次投訴。客戶的投訴簡要來說就是:我們的網關在截斷客戶發的長度越限的簡訊內 容時,導致該簡訊在終端上顯示為亂碼。順著這個起因,我花了一些時間概要性的學習了一些關於電腦字元編碼的常識性知識。
字元,這個我們在平時編碼過程中最最常見的元素,其實也有著一段小故事。
計 算機,毫無疑問是一部機器,在最初我們接觸電腦時或者接收電腦教育時,我們就知道:電腦能識別的只有010101的二進位碼。人與電腦互動早期也 是用的是二進位方式,當時人們或通過扳動電腦龐大的面板上無數的開關來向電腦輸入資訊,或使用打孔卡片來向電腦輸入指令和資料。終端和鍵盤組成的字 符人機介面的誕生讓人們大大提高了與電腦的互動效率。這裡提到了'字元',那麼什麼是'字元'?說的通俗些:字元就是人們使用的記號,抽象意義上的一個 符號。比如阿拉伯數字1,這就是一個符號,這個符號的抽象含義:1代表一種數量的概念,關於1這個抽象概念是如何誕生的,有興趣的人可以去翻閱一下類似數 學史之類科普書籍。
人類的記號五花八門,包括國家文字、標點符號、圖形符號、數字等。這些在電腦領域會被統稱為'字元'。而所有字元的 集合就被稱為'字元集'。有了'字元'概念,那麼在電腦中如何表示'字元'呢?前文提到了電腦中都是用二進位bit來交流的,'字元'也只能建築在 bit的基礎上。多少bit表示一個字元合適呢?或者說我們的字元集有多大呢?如果字元集裡只有8個字元,那麼我用3個bit的組合就可以將這些字元都表 示和識別出來。想當年美國人也在考慮這個問題,不過美國人想當然的就認為:所有能用到的有現實意義的字元不超過256個,當時美國人也只用到了128個, 預留128個備用,而256個字元的字元集用8bit就可以表示,這就是舉世聞名的美國標準資訊交換代碼( American Standard Code for Information Interchange, ASCII)。而這8bit恰與電腦中的基本存放裝置資料單元-'位元組'的位個數相同,這樣一個位元組就恰可以表示一個ASCII字元了。如:ASCII字元 'A'的記憶體位元模式:0x41。
這裡提到了一個'編碼'的概念,上面提到的ASCII就是眾多字元編碼規範中的一種,最早的一種,最重要的一種。那麼什麼是字元編碼呢?回顧一下ASCII在制訂的時候都做了哪些事:
1) 規定用8bit即一個位元組來表示一個ASCII字元;
2) 制定了ASCII字元表,即該字元集中的每個字元對應的位元模式。如:ASCII字元'B'的記憶體位元模式:0x42,'1'的記憶體位元模式:0x31。
由此看來一個字元編碼規範要做兩件事:
1) 規定這個字元集中的字元用多少位元組來表示;
2) 制訂該字元編碼集的字元表,即該字元集中每個字元對應的位元模式
1)和2)這兩個規定合在一起就是編碼。
隨 著電腦的普及,世界各國都開始使用電腦,但是對於非英語國家如中、日、韓等來說,ASCII碼是遠遠不能滿足本國人的需要的,我中華文明淵源五千年, 這五千年來積澱下來的文明怎是這256個字元(精確的說是128個字元)所能表達出來的。我們也要制定自己的編碼,同樣日本人、韓國人也都是這麼做的。這 樣一來,世界範圍內就多了諸如GB2312、BIG5、JIS等局限於某個國家或地區使用的本地化編碼通訊協定,這些編碼通訊協定被統稱為:ANSI編碼。這些 ANSI編碼有一些共同的特點:
1) 每種ANSI編碼或者說ANSI字元集只規定自己國家或地區使用的語言所需的'字元';比如中文GB-2312編碼中就不會包含韓國人的文字。
2) ANSI字元集的空間都比ASCII要大很多,一個位元組已經不夠,絕大多數都使用了多位元組的儲存方案。
3) ANSI編碼一般都會相容ASCII碼。
ANSI 的出現讓電腦迅速普及到世界的每個角落,每個國家都利用上了這樣的先進的工具提高了自己的生產力。開啟Windows記事本,"另存新檔"對話方塊的"編碼 "下拉框中有ANSI編碼,在簡體中文系統下,ANSI編碼代表GB2312編碼,在日文作業系統下,ANSI 編碼代表 JIS 編碼。但是隨著互連網的興起,問題出現了。由於ANSI碼的第一個特點:各個國家或地區在編製自己的ANSI碼時並未考慮到其他國家或地區的ANSI碼, 導致編碼空間有重疊,比如:漢字'中'的GB編碼是[0xD6,0xD0],這個編碼在JIS中是什麼呢,我不知道,我也不願意去查那些稀奇古怪的鬼子 文,但我可以肯定的是肯定不是'中'這個字元了,雖然鬼子的語言文字中抄襲了大量的漢文字。這樣一來當在不同ANSI編碼系統之間進行資訊交換和展示的時 候,亂碼就不可避免了。
為了使國際間資訊交流更加方便,Unicode字元集編碼誕生。Unicode是Universal Multiple-Octet Coded Character Set的縮寫,中文含義是"通用多八位編碼字元集"。它是由一個名為 Unicode學術學會(Unicode Consortium)的機構制訂的字元編碼系統,Unicode目標是將世界上絕大多數國家和的確的文字、符號都編入其字元集,它為每種語言中的每個字 符設定了統一併且唯一的二進位編碼(位元模式),以滿足跨語言、跨平台進行文本轉換、處理的要求,以達到支援現今世界各種不同語言的書面文本的交換、處理及 顯示的目的,使世界範圍人們通過電腦進行資訊交換時達到暢通自如而無障礙。說白了Unicode編碼就是先將世界上存在的絕大多數常用字元納入 Unicode字元集,然後進行統一排號。而每個Unicode字元的編碼(位元模式)就是該字元在Unicode字元表中的序號,所以與上面提到的 ANSI編碼不同的是,一個Unicode字元的編碼用的是一個整數表示,而這個整數的長度通常>= 2個位元組。這樣Unicode編碼在不同平台儲存時就要注意其位元組序了。比如:採用標準Unicode編碼的'中'在Windows上的儲存就是 '2D4E',而在SPARC Solaris上的儲存則是'4E2D'。
上面提到了標準Unicode編碼,難道還有其他 Unicode編碼方式,的確,Unicode的出現的確使我們在統一電腦編碼過程中邁出的一大步,但是畢竟Unicode誕生才10幾年,這之前大家 一直使用ASCII碼,一直使用各自的ANSI編碼。要想一次性將全世界的電腦系統都統一改為Unicode編碼,可能性不大。那麼現在越來越多的新系 統都開始支援並使用Unicode,這些新系統與舊系統之間如何交換資料其實是首要難題。於是一個新名詞又誕生了,那就是UTF, Unicode Translation Format,即把Unicode轉做某種格式的意思。為什麼要轉換成某種格式呢?轉換是為了傳輸和交換。一種好的UTF-x方案應該便於在不同的電腦 之間使用網路傳輸不同語言和編碼的文字,使得標準雙位元組的Unicode能夠在現存的處理單位元組的系統上正確傳輸。目前比較常見的UTF方案有三種:
UTF-16:其本身就是標準的Unicode編碼方案,又稱為UCS-2,它固定使用16 bits(兩個位元組)整數來表示一個字元。
UTF-32:又稱為UCS-4,它固定使用32 bits(四個位元組)整數來表示一個字元。
UTF -8:最廣泛的使用的UTF方案,UTF-8使用可變長度位元組來儲存Unicode字元,例如ASCII字母繼續使用1位元組儲存,重音文字、希臘字母或西 裡爾字母等使用2位元組來儲存,而常用的漢字就要使用3位元組。輔助平面字元則使用4位元組。UTF-8更便於在使用Unicode的系統與現存的單位元組的系統 進行資料轉送和交換。與前兩個方案不同:UTF-8以位元組為編碼單元,沒有位元組序的問題。
UTF有三種方案,那麼如何在接收資料和儲存數 據時識別資料和指導識別資料採用的是哪個方案呢?在UTF編碼方案中有一個叫做"ZERO WIDTH NO-BREAK SPACE"的字元,它的編碼是FEFF。而FFFE在UCS中是不存在的字元,所以不應該出現在實際傳輸或儲存中。UCS規範建議我們在傳輸或儲存位元組 流前,先傳輸字元"ZERO WIDTH NO-BREAK SPACE"。這樣根據識別前面的"ZERO WIDTH NO-BREAK SPACE"即可識別編碼方案:
EF BB BF UTF-8
FE FF UTF-16/UCS-2, little endian
FF FE UTF-16/UCS-2, big endian
FF FE 00 00 UTF-32/UCS-4, little endian.
00 00 FE FF UTF-32/UCS-4, big-endian.
以上是簡略的字元編碼的基本知識。下面將編碼與具體的程式設計語言結合起來進行更直觀的學習。這裡還是以C語言舉例。
C 語言定義了兩個字元集(character set):原始碼字元集(source character set)是用於組成C原始碼的字元集合,而運行字元集(execution character set)是可以被執行程式解釋的字元集合。應用程式都有自己的執行字元集,也就說在應用程式執行過程中使用什麼字元集或字元編碼來識別各種資料存放區介質中 的bit流。
[Example1]
/* testwprintf.c , windows xp, mingw gcc-3.4.2 */
int main() {
wchar_t ws[] = L"中文"; --- (1)
wprintf(L"%s/n", ws);
}
編譯該程式gcc編譯器提示:(1)這行:converting to execution character set: Illegal byte sequence
為什麼轉換失敗呢?我們看到程式中使用了寬字元常量。這裡先插入一段C語言的小故事:多位元組字元和寬位元組字元。
C 語言原本是在英文環境中設計的,主要的字元集是ASCII字元。但是國際化軟體必須能夠表示不同的字元,而這些字元數量龐大,無法使用一個位元組編碼,於是 在1994年,"Normative Addendum 1"(基準增補一)的採用,讓ISO C可以標準化兩種表示大型字元集的方法:寬字元(wide character,該字元集內每個字元使用相同的位長)以及多位元組字元(multibyte character,每個字元可以是一到多個位元組不等,而某個位元組序列的字元值由字串或流(stream)所在的環境背景決定)。自從1994 年的增補之後,C不只提供char類型,還提供wchar_t類型(寬字元)。雖然此次C標準仍沒有支援Unicode字元集,但許多實現版本使用 Unicode轉換格式UTF-16和UTF-32來處理寬字元(我遇到的mingw gcc用的是UTF-16, Sun Sparc Gcc用的則是UTF-32),也就是說在大部分標準C實現版本中,預設的一個wchar_t就是一個unicode字元,一個寬字元實際上就是一個 unicode字元,一個寬字元常量字串(L"...")實際上是一個unicode編碼的常量字串。這樣我們來解釋上面的問題。
上 面程式中編譯器在遇到寬字元常量:L"中文"時,試圖將之轉換成unicode碼儲存,mingw gcc試圖使用預設的原始碼符號集->unicode的轉碼方式轉換"中文"這個字面量的二進位位元模式到unicode位元模式,但卻發現"中文"這 個字面量的位元模式不能識別,這就需要我們在外部告知gcc我們的這個"中文"字面量的位元模式是GB2312的,我們使用:gcc -finput-charset=GB2312 testwprintf.c就能解決這一問題了。
好了,編譯完了。我們來執行一下 a.exe,但卻發現在控制台沒有任何輸出,又出現什麼問題了呢?分析一下:目前我們的ws中使用的位元模式是unicode編碼位元模式,哇,原來 wprintf並不支援直接輸出:unicode編碼。類似:printf, wprintf等輸出到控制台或者檔案的庫函數只支援ANSI編碼或多位元組編碼輸出。其實這是符合C語言規範的,因為C標準並未支援Unicode,只是 很多C的實現將寬字元用unicode的位元模式表示吧了。這時我們需要通過setlocale函數來設定如何將unicode編碼的寬字元轉換成一種可以 輸出的編碼。
[Example2]
/* testwprintf.c , windows xp, mingw gcc-3.4.2 */
int main() {
wchar_t ws[] = L"中文";
setlocale(LC_ALL, "chs"); /* 設定gb碼, unix上沒有"chs"這樣的locale,unix上可通過locale -a查 */
wprintf(L"%s/n", ws);
}
setlocale(...)只在運行時起作用,這樣編譯執行後,"中文"二字就會顯示在我們的控制台上了。
當然了我們還可以通過標準庫調用將寬字元手動轉成ANSI字元後再直接輸出。
[Example3]
/* testwprintf.c , windows xp, mingw gcc-3.4.2 */
int main() {
wchar_t ws[] = L"中文";
char ms[12];
memset(&ms, 0, sizeof(ms));
setlocale(LC_ALL, "chs"); /* 設定gb碼, unix上沒有"chs"這樣的locale,unix上可通過locale -a查 */
wcstombs(ms, ws, sizeof(ms));
printf("%s/n", ms);
}
編 譯執行後,"中文"二字同樣躍然紙上。wcstombs是將寬字元串按照setlocale設定的編碼轉成指定的ANSI編碼字串;而mbstowcs 則是按照etlocale設定的編碼將將多位元組字串轉換成unicode編碼儲存在寬字元串中。前者調用setlocale是指導目標編碼的;後者調用 setlocale的作用是指導如何將源字串翻譯成目的unicode字串的。類似的還有字元層級的標準函數:wctomb和mbtowc。
關於字元編碼轉換,其實有很多好用的開源工具包可用,比如著名的iconv,自己平時很少會去實現一個編碼轉換。學習以上知識只是為了讓自己再遇到亂碼問題的時候不再迷糊,而且對電腦字元編碼知識有一個概念上的瞭解是必要的且大有裨益的。
更多請參見:http://bigwhite.blogbus.com/logs/10617585.html