我曾經寫過一篇《談談Unicode編碼,簡要解釋UCS、UTF、BMP、BOM等名詞》(以下簡稱《談談Unicode編碼》),在網上流傳較廣,我也收到不少朋友的反饋。本文探討《談談Unicode編碼》中未介紹或介紹較少的代碼頁、Surrogates等問題,補充一些Unicode資料,順帶介紹一下我最近編寫的一個Unicode工具:UniToy。本文雖然是前文的補充,但在寫作上盡量做到獨立成篇。
標題中的“淺談”是對自己的要求,我希望文字能盡量淺顯易懂。但本文還是假設讀者知道字節、16進制,了解《談談Unicode編碼》中介紹過的字節序和Unicode的基本概念。
0 UniToy
UniToy是我編寫的一個小工具。通過UniToy,我們可以全方位、多角度地查看Unicode,了解Unicode和語言、代碼頁的關系,完成一些文字編碼的相關工作。本文的一些內容是通過UniToy演示的。大家可以從我的網站(www.fmddlmyy.cn)下載UniToy的演示版本。1 文字的顯示
1.1 發生了什么?
我們首先以Windows為例來看看文字顯示過程中發生了什么。用記事本打開一個文本文件,可以看到文件包含的文字:

如果我們用UltraEdit或Hex Workshop查看這個文件的16進制數據,可以看到:

我們看到:文件“例子GBK.txt”有10個字節,依次是“D7 D6 B7 FB BA CD B1 E0 C2 EB”,這就是記事本從文件中讀到的內容。記事本是用來打開文本文件的,所以它會調用Windows的文本顯示函數將讀到的數據作為文本顯示。Windows首先將文本數據轉換到它內部使用的編碼格式:Unicode,然后按照文本的Unicode去字體文件中查找字體圖像,最后將圖像顯示到窗口上。 總結一下前面的分析,文字的顯示應該是這樣的:
- 步驟1:文字首先以某種編碼保存在文件中。
- 步驟2:Windows將文件中的文字編碼映射到Unicode。
- 步驟3:Windows按照Unicode在字體文件中查找字體圖像,畫到窗口上。
所謂編碼就是用數字表示字符,例如用D7D6表示“字”。當然,編碼還意味著約定,即大家都認可。從《談談Unicode編碼》中,我們知道Unicode也是一種文字編碼,它的特殊性在于它是由國際組織設計,可以容納全世界所有語言文字。而我們平常使用的文字編碼通常是針對一個區域的語言、文字設計,只支持特定的語言文字。例如:在上面的例子中,文件“例子GBK.txt”采用的就是GBK編碼。
如果上述3個步驟中任何一步發生了錯誤,文字就不能被正確顯示,例如:
在Unicode被廣泛使用前,有多少種語言、文字,就可能有多少種文字編碼方案。一種文字也可能有多種編碼方案。那么我們怎么確定文本數據采用了什么編碼?
1.2 采用了哪種編碼?
按照慣例,文本文件中的數據都是文本編碼,那么它怎么表明自己的編碼格式?在記事本的“打開”對話框上:

我們可以看到記事本支持4種編碼格式:ANSI、Unicode、Unicode big endian、UTF-8。如果讀者看過《談談Unicode編碼》,對Unicode、Unicode big endian、UTF-8應該不會陌生,其實它們更準確的名稱應該是UTF-16LE(Little Endian)、UTF-16BE(Big Endian)和UTF-8,它們是基于Unicode的不同編碼方案。
在《談談Unicode編碼》中介紹過,Windows通過在文本文件開頭增加一些特殊字節(BOM)來區分上述3種編碼,并將沒有BOM的文本數據按照ANSI代碼頁處理。那么什么是代碼頁,什么是ANSI代碼頁?
2 代碼頁和字符集
2.1 Windows的代碼頁
2.1.1 代碼頁
代碼頁(Code Page)是個古老的專業術語,據說是IBM公司首先使用的。代碼頁和字符集的含義基本相同,代碼頁規定了適用于特定地區的字符集合,和這些字符的編碼。可以將代碼頁理解為字符和字節數據的映射表。
Windows為自己支持的代碼頁都編了一個號碼。例如代碼頁936就是簡體中文 GBK,代碼頁950就是繁體中文 Big5。代碼頁的概念比較簡單,就是一個字符編碼方案。但要說清楚Windows的ANSI代碼頁,就要從Windows的區域(Locale)說起了。
2.1.2 區域和ANSI代碼頁
微軟為了適應世界上不同地區用戶的文化背景和生活習慣,在Windows中設計了區域(Locale)設置的功能。Local是指特定于某個國家或地區的一組設定,包括代碼頁,數字、貨幣、時間和日期的格式等。在Windows內部,其實有兩個Locale設置:系統Locale和用戶Locale。系統Locale決定代碼頁,用戶Locale決定數字、貨幣、時間和日期的格式。我們可以在控制面板的“區域和語言選項”中設置系統Locale和用戶Locale:

每個Locale都有一個對應的代碼頁。Locale和代碼頁的對應關系,大家可以參閱我的另一篇文章《談談Windows程序中的字符編碼》的附錄1。系統Locale對應的代碼頁被作為Windows的默認代碼頁。在沒有文本編碼信息時,Windows按照默認代碼頁的編碼方案解釋文本數據。這個默認代碼頁通常被稱作ANSI代碼頁(ACP)。
ANSI代碼頁還有一層意思,就是微軟自己定義的代碼頁。在歷史上,IBM的個人計算機和微軟公司的操作系統曾經是PC的標準配置。微軟公司將IBM公司定義的代碼頁稱作OEM代碼頁,在IBM公司的代碼頁基礎上作了些增補后,作為自己的代碼頁,并冠以ANSI的字樣。我們在“區域和語言選項”高級頁面的代碼頁轉換表中看到的包含ANSI字樣的代碼頁都是微軟自己定義的代碼頁。例如:
- 874 (ANSI/OEM - 泰文)
- 932 (ANSI/OEM - 日文 Shift-JIS)
- 936 (ANSI/OEM - 簡體中文 GBK)
- 949 (ANSI/OEM - 韓文)
- 950 (ANSI/OEM - 繁體中文 Big5)
- 1250 (ANSI - 中歐)
- 1251 (ANSI - 西里爾文)
- 1252 (ANSI - 拉丁文 I)
- 1253 (ANSI - 希臘文)
- 1254 (ANSI - 土耳其文)
- 1255 (ANSI - 希伯來文)
- 1256 (ANSI - 阿拉伯文)
- 1257 (ANSI - 波羅的海文)
- 1258 (ANSI/OEM - 越南)
在UniToy中,我們可以按照代碼頁編碼順序查看這些代碼頁的字符和編碼:

我們不能直接設置ANSI代碼頁,只能通過選擇系統Locale,間接改變當前的ANSI代碼頁。微軟定義的Locale只使用自己定義的代碼頁。所以,我們雖然可以通過“區域和語言選項”中的代碼頁轉換表安裝很多代碼頁,但只能將微軟的代碼頁作為系統默認代碼頁。
2.1.3 代碼頁轉換表
在Windows 2000以后,Windows統一采用UTF-16作為內部字符編碼。現在,安裝一個代碼頁就是安裝一張代碼頁轉換表。通過代碼頁轉換表,Windows既可以將代碼頁的編碼轉換到UTF-16,也可以將UTF-16轉換到代碼頁的編碼。代碼頁轉換表的具體實現可以是一個以nls為后綴的數據文件,也可以是一個提供轉換函數的動態鏈接庫。有的代碼頁是不需要安裝的。例如:Windows將UTF-7和UTF-8分別作為代碼頁65000和代碼頁65001。UTF-7、UTF-8和UTF-16都是基于Unicode的編碼方案。它們之間可以通過簡單的算法直接轉換,不需要安裝代碼頁轉換表。
在安裝過一個代碼頁后,Windows就知道怎樣將該代碼頁的文本轉換到Unicode文本,也知道怎樣將Unicode文本轉換成該代碼頁的文本。例如:UniToy有導入和導出功能。所謂導入功能就是將任一代碼頁的文本文件轉換到Unicode文本;導出功能就是將Unicode文本轉換到任一指定的代碼頁。這里所說的代碼頁就是指系統已安裝的代碼頁:

其實,如果全世界人民在計算機剛發明時就統一采用Unicode作為字符編碼,那么代碼頁就沒有存在的必要了。可惜在Unicode被發明前,世界各國人民都發明并使用了各種字符編碼方案。所以,Windows必須通過代碼頁支持已經被廣泛使用的字符編碼。從這種意義看,代碼頁主要是為了兼容現有的數據、程序和習慣而存在的。
2.1.4 SBCS、DBCS和MBCS
SBCS、DBCS和MBCS分別是單字節字符集、雙字節字符集和多字節字符集的縮寫。SBCS、DBCS和MBCS的最大編碼長度分別是1字節、兩字節和大于兩字節(例如4或5字節)。例如:代碼頁1252 (ANSI-拉丁文 I)是單字節字符集;代碼頁936 (ANSI/OEM-簡體中文 GBK)是雙字節字符集;代碼頁54936 (GB18030 簡體中文)是多字節字符集。
單字節字符集中的字符都用一個字節表示。顯然,SBCS最多只能容納256個字符。
雙字節字符集的字符用一個或兩個字節表示。那么我們從文本數據中讀到一個字節時,怎么判斷它是單字節字符,還是雙字節字符的首字符?答案是通過字節所處范圍來判斷。例如:在GBK編碼中,單字節字符的范圍是0x00-0x80,雙字節字符首字節的范圍是0x81到0xFE。我們順序讀取字節數據,如果讀到的字節在0x81到0xFE內,那么這個字節就是雙字節字符的首字節。GBK定義雙字節字符的尾字節范圍是0x40到0x7E和0x80到0xFE。
GB18030是多字節字符集,它的字符可以用一個、兩個或四個字節表示。這時我們又如何判斷一個字節是屬于單字節字符,雙字節字符,還是四字節字符?GB18030與GBK是兼容的,它利用了GBK雙字節字符尾字節的未使用碼位。GB18030的四字節字符的第一字節的范圍也是0x81到0xFE,第二字節的范圍是0x30-0x39。通過第二字節所處范圍就可以區分雙字節字符和四字節字符。GB18030定義四字節字符的第三字節范圍是0x81到0xFE,第四字節范圍是0x30-0x39。
2.2 代碼頁實例
2.2.1 實例一:GB18030代碼頁
1.1節的“錯誤2”中演示了一個全被顯示成'?'的文件。這個文件的數據是:

其實,這是一個包含了6個四字節字符的GB18030編碼的文件。記事本按照GBK顯示這些數據,而GB18030的四字節字符編碼在GBK中是未定義的。Windows根據首字節范圍判斷出12個雙字節字符,然后因為找不到匹配的轉換而將其映射到默認字符'?'。使用UniToy按照GB18030代碼頁導入這個文件,就可以看到:

這個GB18030編碼的文件是用UniToy創建的,編輯Unicode文本,然后導出到GB18030編碼格式。
2.2.2 實例二:GBK和Big5的轉換
綜合使用UniToy的導入、導出功能就可以在任意兩個代碼頁之間轉換文本。其實,由于各代碼頁支持的字符范圍不同,我們一般不會直接在代碼頁間轉換文本。例如將以下GBK編碼的文本:

直接轉換到Big5編碼,就會看到:

變成'?'的字符都是Big5編碼不支持的簡化字。在從Unicode轉換到Big5編碼時,由于Big5編碼不支持這些字符,Windows就用默認字符'?'代替。在UniToy中,我們可以先將簡體字轉換到繁體字,然后再導出到Big5編碼,就可以正常顯示:

同理,將Big5編碼的文本轉換到GBK編碼的步驟應該是:
- 將Big5編碼的文本導入到Unicode文本;
- 將繁體的Unicode文本轉換簡體的Unicode文本;
- 將簡體的Unicode文本導出到GBK文本。
2.3 互聯網的字符集
2.3.1 字符集
互聯網上的信息繽紛多彩,但文本依然是最重要的信息載體。html文件通過標記表明自己使用的字符集。例如:
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
或者:
<meta http-equiv="charset" content="iso-8859-1">
那么我們可以使用哪些字符集(charset)呢?在IETF(互聯網工程任務組)的網頁上維護著一份可以在互聯網上使用的字符集的清單:CHARACTER SETS。如果有新的字符集被登記,IETF會更新這份文檔。
簡單瀏覽一下,2006年12月7日的版本列出了253個字符集。其中也包括微軟的CP1250 ~ CP1258,在這里它們不會被稱作什么ANSI代碼頁,而是被簡單地稱作windows-1250、windows-1251等。其實在Unicode被廣泛使用前,除了中日韓等大字符集,世界上,特別是西方使用最廣泛的字符集應該是ISO 8859系列字符集。
2.3.2 ISO 8859系列字符集
ISO 8859系列字符集是歐洲計算機制造商協會(ECMA)在上世紀80年代中期設計,并被國際標準化(ISO)組織采納為國際標準。ISO 8859系列字符集目前有15個字符集,包括:
- ISO 8859-1 大部分的西歐語系,例如英文、法文、西班牙文和德文等(Latin-1)
- ISO 8859-2 大部分的中歐和東歐語系,例如捷克文、波蘭文和匈牙利文等(Latin-2)
- ISO 8859-3 歐洲東南部和其它各種文字(Latin-3)
- ISO 8859-4 斯堪的那維亞和波羅的海語系(Latin-4)
- ISO 8859-5 拉丁文與斯拉夫文(俄文、保加利亞文等)
- ISO 8859-6 拉丁文與阿拉伯文
- ISO 8859-7 拉丁文與希臘文
- ISO 8859-8 拉丁文與希伯來文
- ISO 8859-9 為土耳其文修正的Latin-1(Latin-5)
- ISO 8859-10 拉普人、北歐與愛斯基摩人的文字(Latin-6)
- ISO 8859-11 拉丁文與泰文
- ISO 8859-13 波羅的海周邊語系,例如拉脫維亞文等(Latin-7)
- ISO 8859-14 凱爾特文,例如蓋爾文、威爾士文等(Latin-8)
- ISO 8859-15 改進的Latin-1,增加遺漏的法文、芬蘭文字符和歐元符號(Latin-9)
- ISO 8859-16 羅馬尼亞文(Latin-10)
其中缺少的編號12據說是為了預留給天城體梵文字母(Deva-nagari)的。印地文和尼泊爾文都使用了這種在七世紀形成的字母表。由于印度定義了自己的編碼ISCII(Indian Script Code for Information Interchange),所以這個編號就未被使用。ISO 8859系列字符集都是單字節字符集,即只使用0x00-0xFF對字符編碼。
大家都知道ASCII吧,那么大家知道ANSI X3.4和ISO 646嗎?在1968年發布的ANSI X3.4和1972年發布的ISO 646就是ASCII編碼,只不過是不同組織發布的。絕大多數字符集都與ASCII編碼保持兼容,ISO 8859系列字符集也不例外,它們的0x00-0x7f都與ASCII碼保持一致,各字符集的不同之處在于如何利用0x80-0xff的碼位。使用UniToy可以查看ISO 8859系列所有字符集的編碼,例如:

通過這些演示,大家是不是覺得代碼頁和字符集都是很簡單、樸實的東西呢?好,在進入Unicode的話題前,讓我們先看一個很深奧的概念。