文字編碼和Unicode

談談Unicode編碼,簡要解釋UCS、UTF、BMP、BOM等名詞

這是一篇程序員寫給程序員的趣味讀物。所謂趣味是指可以比較輕鬆地瞭解一些原來不清楚的概念,增進知識,類似於打RPG遊戲的升級。整理這篇文章的動機是兩個問題:

問題一:

使用Windows記事本的“另存爲”,可以在GBK、Unicode、Unicode big endian和UTF-8這幾種編碼方式間相互轉換。同樣是txt文件,Windows是怎樣識別編碼方式的呢?

我很早前就發現Unicode、Unicode big endian和UTF-8編碼的txt文件的開頭會多出幾個字節,分別是FF、FE(Unicode),FE、FF(Unicode big endian),EF、BB、BF(UTF-8)。但這些標記是基於什麼標準呢?

問題二:
最近在網上看到一個ConvertUTF.c,實現了UTF-32、UTF-16和UTF-8這三種編碼方式的相互轉換。對於Unicode(UCS2)、GBK、UTF-8這些編碼方式,我原來就瞭解。但這個程序讓我有些糊塗,想不起來UTF-16和UCS2有什麼關係。

查了查相關資料,總算將這些問題弄清楚了,順帶也瞭解了一些Unicode的細節。寫成一篇文章,送給有過類似疑問的朋友。本文在寫作時儘量做到通俗易懂,但要求讀者知道什麼是字節,什麼是十六進制。

0、big endian和little endian

big endian和little endian是CPU處理多字節數的不同方式。例如“漢”字的Unicode編碼是6C49。那麼寫到文件裏時,究竟是將6C寫在前面,還是將49寫在前面?如果將6C寫在前面,就是big endian。如果將49寫在前面,就是little endian。

“endian”這個詞出自《格列佛遊記》。小人國的內戰就源於吃雞蛋時是究竟從大頭(Big-Endian)敲開還是從小頭(Little-Endian)敲開,由此曾發生過六次叛亂,一個皇帝送了命,另一個丟了王位。

我們一般將endian翻譯成“字節序”,將big endian和little endian稱作“大尾”和“小尾”。

1、字符編碼、內碼,順帶介紹漢字編碼

字符必須編碼後才能被計算機處理。計算機使用的缺省編碼方式就是計算機的內碼。早期的計算機使用7位的ASCII編碼,爲了處理漢字,程序員設計了用於簡體中文的GB2312和用於繁體中文的big5。

GB2312(1980年)一共收錄了7445個字符,包括6763個漢字和682個其它符號。漢字區的內碼範圍高字節從B0-F7,低字節從A1-FE,佔用的碼位是72*94=6768。其中有5個空位是D7FA-D7FE。

GB2312支持的漢字太少。1995年的漢字擴展規範GBK1.0收錄了21886個符號,它分爲漢字區和圖形符號區。漢字區包括21003個字符。

ASCII、GB2312到GBK這些編碼方法是向下兼容的,即同一個字符在這些方案中總是有相同的編碼,後面的標準支持更多的字符。在這些編碼中,英文和中文可以統一地處理。區分中文編碼的方法是高字節的最高位不爲0。按照程序員的稱呼,GB2312、GBK都屬於雙字節字符集 (DBCS)。

2000年的GB18030是取代GBK1.0的正式國家標準。該標準收錄了27484個漢字,同時還收錄了藏文、蒙文、維吾爾文等主要的少數民族文字。從漢字字彙上說,GB18030在GB13000.1的20902個漢字的基礎上增加了CJK擴展A的6582個漢字(Unicode碼0x3400-0x4db5),一共收錄了27484個漢字。

CJK就是中日韓的意思。Unicode爲了節省碼位,將中日韓三國語言中的文字統一編碼。GB13000.1就是ISO/IEC 10646-1的中文版,相當於Unicode 1.1。

GB18030的編碼採用單字節、雙字節和4字節方案。其中單字節、雙字節和GBK是完全兼容的。4字節編碼的碼位就是收錄了CJK擴展A的6582個漢字。 例如:UCS的0x3400在GB18030中的編碼應該是8139EF30,UCS的0x3401在GB18030中的編碼應該是8139EF31。

微軟提供了GB18030的升級包,但這個升級包只是提供了一套支持CJK擴展A的6582個漢字的新字體:新宋體-18030,並不改變內碼。Windows 的內碼仍然是GBK。

這裏還有一些細節:

  • GB2312的原文還是區位碼,從區位碼到內碼,需要在高字節和低字節上分別加上A0。

  • 對於任何字符編碼,編碼單元的順序是由編碼方案指定的,與endian無關。例如GBK的編碼單元是字節,用兩個字節表示一個漢字。 這兩個字節的順序是固定的,不受CPU字節序的影響。UTF-16的編碼單元是word(雙字節),word之間的順序是編碼方案指定的,word內部的字節排列纔會受到endian的影響。後面還會介紹UTF-16。

  • GB2312的兩個字節的最高位都是1。但符合這個條件的碼位只有128*128=16384個。所以GBK和GB18030的低字節最高位都可能不是1。不過這不影響DBCS字符流的解析:在讀取DBCS字符流時,只要遇到高位爲1的字節,就可以將下兩個字節作爲一個雙字節編碼,而不用管低字節的高位是什麼。

2、Unicode、UCS和UTF

前面提到從ASCII、GB2312、GBK到GB18030的編碼方法是向下兼容的。而Unicode只與ASCII兼容(更準確地說,是與ISO-8859-1兼容),與GB碼不兼容。例如“漢”字的Unicode編碼是6C49,而GB碼是BABA。

Unicode也是一種字符編碼方法,不過它是由國際組織設計,可以容納全世界所有語言文字的編碼方案。Unicode的學名是"Universal Multiple-Octet Coded Character Set",簡稱爲UCS。UCS可以看作是"Unicode Character Set"的縮寫。

根據維基百科全書(http://zh.wikipedia.org/wiki/)的記載:歷史上存在兩個試圖獨立設計Unicode的組織,即國際標準化組織(ISO)和一個軟件製造商的協會(unicode.org)。ISO開發了ISO 10646項目,Unicode協會開發了Unicode項目。

在1991年前後,雙方都認識到世界不需要兩個不兼容的字符集。於是它們開始合併雙方的工作成果,併爲創立一個單一編碼表而協同工作。從Unicode2.0開始,Unicode項目採用了與ISO 10646-1相同的字庫和字碼。

目前兩個項目仍都存在,並獨立地公佈各自的標準。Unicode協會現在的最新版本是2005年的Unicode 4.1.0。ISO的最新標準是ISO 10646-3:2003。

UCS只是規定如何編碼,並沒有規定如何傳輸、保存這個編碼。例如“漢”字的UCS編碼是6C49,我可以用4個ascii數字來傳輸、保存這個編碼;也可以用utf-8編碼:3個連續的字節E6 B1 89來表示它。關鍵在於通信雙方都要認可。UTF-8、UTF-7、UTF-16都是被廣泛接受的方案。UTF-8的一個特別的好處是它與ISO-8859-1完全兼容。UTF是“UCS Transformation Format”的縮寫。

IETF的RFC2781和RFC3629以RFC的一貫風格,清晰、明快又不失嚴謹地描述了UTF-16和UTF-8的編碼方法。我總是記不得IETF是Internet Engineering Task Force的縮寫。但IETF負責維護的RFC是Internet上一切規範的基礎。

2.1、內碼和code page

目前Windows的內核已經支持Unicode字符集,這樣在內核上可以支持全世界所有的語言文字。但是由於現有的大量程序和文檔都採用了某種特定語言的編碼,例如GBK,Windows不可能不支持現有的編碼,而全部改用Unicode。

Windows使用代碼頁(code page)來適應各個國家和地區。code page可以被理解爲前面提到的內碼。GBK對應的code page是CP936。微軟也爲GB18030定義了code page:CP54936。

3、UCS-2、UCS-4、BMP

UCS有兩種格式:UCS-2和UCS-4。顧名思義,UCS-2就是用兩個字節編碼,UCS-4就是用4個字節(實際上只用了31位,最高位必須爲0)編碼。下面讓我們做一些簡單的數學遊戲:

UCS-2有2^16=65536個碼位,UCS-4有2^31=2147483648個碼位。

UCS-4根據最高位爲0的最高字節分成2^7=128個group。每個group再根據次高字節分爲256個plane。每個plane根據第3個字節分爲256行 (rows),每行包含256個cells。當然同一行的cells只是最後一個字節不同,其餘都相同。

group 0的plane 0被稱作Basic Multilingual Plane, 即BMP。或者說UCS-4中,高兩個字節爲0的碼位被稱作BMP。

將UCS-4的BMP去掉前面的兩個零字節就得到了UCS-2。在UCS-2的兩個字節前加上兩個零字節,就得到了UCS-4的BMP。而目前的UCS-4規範中還沒有任何字符被分配在BMP之外。

4、UTF編碼

UTF-8就是以8位爲單元對UCS進行編碼。從UCS-2到UTF-8的編碼方式如下:

UCS-2編碼(16進制) UTF-8 字節流(二進制)
0000 - 007F 0xxxxxxx
0080 - 07FF 110xxxxx 10xxxxxx
0800 - FFFF 1110xxxx 10xxxxxx 10xxxxxx

例如“漢”字的Unicode編碼是6C49。6C49在0800-FFFF之間,所以肯定要用3字節模板了:1110xxxx10xxxxxx10xxxxxx。將6C49寫成二進制是:0110 110001 001001, 用這個比特流依次代替模板中的x,得到:111001101011000110001001,即E6 B1 89

讀者可以用記事本測試一下我們的編碼是否正確。需要注意,UltraEdit在打開utf-8編碼的文本文件時會自動轉換爲UTF-16,可能產生混淆。你可以在設置中關掉這個選項。更好的工具是Hex Workshop。

UTF-16以16位爲單元對UCS進行編碼。對於小於0x10000的UCS碼,UTF-16編碼就等於UCS碼對應的16位無符號整數。對於不小於0x10000的UCS碼,定義了一個算法。不過由於實際使用的UCS2,或者UCS4的BMP必然小於0x10000,所以就目前而言,可以認爲UTF-16和UCS-2基本相同。但UCS-2只是一個編碼方案,UTF-16卻要用於實際的傳輸,所以就不得不考慮字節序的問題。

5、UTF的字節序和BOM

UTF-8以字節爲編碼單元,沒有字節序的問題。UTF-16以兩個字節爲編碼單元,在解釋一個UTF-16文本前,首先要弄清楚每個編碼單元的字節序。例如“奎”的Unicode編碼是594E,“乙”的Unicode編碼是4E59。如果我們收到UTF-16字節流“594E”,那麼這是“奎”還是“乙”?

Unicode規範中推薦的標記字節順序的方法是BOM。BOM不是“Bill Of Material”的BOM表,而是Byte Order Mark。BOM是一個有點小聰明的想法:

在UCS編碼中有一個叫做"ZERO WIDTH NO-BREAK SPACE"的字符,它的編碼是FEFF。而FFFE在UCS中是不存在的字符,所以不應該出現在實際傳輸中。UCS規範建議我們在傳輸字節流前,先傳輸字符"ZERO WIDTH NO-BREAK SPACE"。

樣如果接收者收到FEFF,就表明這個字節流是Big-Endian的;如果收到FFFE,就表明這個字節流是Little-Endian的。因此字符"ZERO WIDTH NO-BREAK SPACE"又被稱作BOM。

UTF-8不需要BOM來表明字節順序,但可以用BOM來表明編碼方式。字符"ZERO WIDTH NO-BREAK SPACE"的UTF-8編碼是EF BB BF(讀者可以用我們前面介紹的編碼方法驗證一下)。所以如果接收者收到以EF BB BF開頭的字節流,就知道這是UTF-8編碼了。

Windows就是使用BOM來標記文本文件的編碼方式的。

6、進一步的參考資料

本文主要參考的資料是 "Short overview of ISO-IEC 10646 and Unicode" (http://www.nada.kth.se/i18n/ucs/unicode-iso10646-oview.html)。

我還找了兩篇看上去不錯的資料,不過因爲我開始的疑問都找到了答案,所以就沒有看:

  1. "Understanding Unicode A general introduction to the Unicode Standard" (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&item_id=IWS-Chapter04a)
  2. "Character set encoding basics Understanding character set encodings and legacy encodings" (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&item_id=IWS-Chapter03)

我寫過UTF-8、UCS-2、GBK相互轉換的軟件包,包括使用Windows API和不使用Windows API的版本。以後有時間的話,我會整理一下放到我的個人主頁上(http://www.fmddlmyy.cn)。

我是想清楚所有問題後纔開始寫這篇文章的,原以爲一會兒就能寫好。沒想到考慮措辭和查證細節花費了很長時間,竟然從下午1:30寫到9:00。希望有讀者能從中受益。

附錄1 再說說區位碼、GB2312、內碼和代碼頁

有的朋友對文章中這句話還有疑問:
“GB2312的原文還是區位碼,從區位碼到內碼,需要在高字節和低字節上分別加上A0。”

我再詳細解釋一下:

“GB2312的原文”是指國家1980年的一個標準《中華人民共和國國家標準 信息交換用漢字編碼字符集 基本集 GB 2312-80》。這個標準用兩個數來編碼漢字和中文符號。第一個數稱爲“區”,第二個數稱爲“位”。所以也稱爲區位碼。1-9區是中文符號,16-55區是一級漢字,56-87區是二級漢字。現在Windows也還有區位輸入法,例如輸入1601得到“啊”。(這個區位輸入法可以自動識別16進制的GB2312和10進制的區位碼,也就是說輸入B0A1同樣會得到“啊”。)

內碼是指操作系統內部的字符編碼。早期操作系統的內碼是與語言相關的。現在的Windows在系統內部支持Unicode,然後用代碼頁適應各種語言,“內碼”的概念就比較模糊了。微軟一般將缺省代碼頁指定的編碼說成是內碼。

內碼這個詞彙,並沒有什麼官方的定義,代碼頁也只是微軟這個公司的叫法。作爲程序員,我們只要知道它們是什麼東西,沒有必要過多地考證這些名詞。

Windows中有缺省代碼頁的概念,即缺省用什麼編碼來解釋字符。例如Windows的記事本打開了一個文本文件,裏面的內容是字節流:BA、BA、D7、D6。Windows應該去怎麼解釋它呢?

是按照Unicode編碼解釋、還是按照GBK解釋、還是按照BIG5解釋,還是按照ISO8859-1去解釋?如果按GBK去解釋,就會得到“漢字”兩個字。按照其它編碼解釋,可能找不到對應的字符,也可能找到錯誤的字符。所謂“錯誤”是指與文本作者的本意不符,這時就產生了亂碼。

答案是Windows按照當前的缺省代碼頁去解釋文本文件裏的字節流。缺省代碼頁可以通過控制面板的區域選項設置。記事本的另存爲中有一項ANSI,其實就是按照缺省代碼頁的編碼方法保存。

Windows的內碼是Unicode,它在技術上可以同時支持多個代碼頁。只要文件能說明自己使用什麼編碼,用戶又安裝了對應的代碼頁,Windows就能正確顯示,例如在HTML文件中就可以指定charset。

有的HTML文件作者,特別是英文作者,認爲世界上所有人都使用英文,在文件中不指定charset。如果他使用了0x80-0xff之間的字符,中文Windows又按照缺省的GBK去解釋,就會出現亂碼。這時只要在這個html文件中加上指定charset的語句,例如:
<meta http-equiv="Content-Type" content="text/html; charset=ISO8859-1">
如果原作者使用的代碼頁和ISO8859-1兼容,就不會出現亂碼了。

再說區位碼,啊的區位碼是1601,寫成16進制是0x10,0x01。這和計算機廣泛使用的ASCII編碼衝突。爲了兼容00-7f的ASCII編碼,我們在區位碼的高、低字節上分別加上A0。這樣“啊”的編碼就成爲B0A1。我們將加過兩個A0的編碼也稱爲GB2312編碼,雖然GB2312的原文根本沒提到這一點。

 Windows程序中的字符編碼 :

0 Where is Win32 API

Windows程序有用戶態和核心態的說法。在32位地址空間中,用戶態只能訪問0x80000000以下空間(其實只是0x00010000-0x7FFEFFFF),核心態代碼可以訪問0x80000000以上空間。所有硬件管理都在覈心態。用戶態代碼不能直接使用核心態的任何代碼。所謂用戶態、核心態其實只是不同的CPU特權級別。在x86 CPU上,用戶態處於ring 3,核心態處於ring 0。

從用戶態進入核心態的最常用的方法是在寄存器eax填一個功能碼,然後執行int 2e。這有點像DOS時代的DOS和BIOS系統調用。在NT架構中這種機制被稱作system service。

在覈心態提供system service的有兩個傢伙:ntoskrnl.exe和win32k.sys。ntoskrnl.exe是Windows的大腦,它的上層被稱爲Executive,下層被稱作Kernel。Win32k.sys提供與顯示有關的system service。

在用戶態一側,有一個重要的角色叫作ntdll.dll,大多數system service都是它調用的。它封裝這些system service,然後提供一個API接口。這個接口被稱作native API。 native API的用戶是各個子系統(subsystem),包括Win32子系統、OS/2子系統、POSIX子系統。各個子系統爲Win32、OS2、POSIX程序提供了運行平臺。

ntdll.dll由於提供了平臺無關的API接口,所以被看作是NT系統的原生接口,由之得到了“native API”的匪號。其實它的主要工作是將調用傳遞到核心態。

Win32、OS/2、POSIX,聽起來很龐大。其實真正做好的只有Win32子系統。OS2、POSIX都是Console UI,即只有字符界面。提供OS/2子系統,只因爲在1988年,NT的主要設計目標就是與OS/2兼容,後來由於Windows 3.0賣得很好,所以設計目標被變更爲與Windows兼容。提供POSIX子系統,是爲了應付美國政府的一個編號爲FIPS 151-2的標準。

Win32子系統的管理員是一個叫作csrss.exe的弟兄,它的全名是:Client/Server Run-Time Subsystem。它剛上任時,本來要分管所有的子系統,但後來POSIX和OS/2都被分別處理了,所以只管了一個Win32。即使這樣也很了不起,所有的Win32程序的進程、線程們都要向它登記。

不過Win32程序用得最多的還是Win32子系統的DLL們,最核心的DLL包括:kernel32.dll、User32.dll、Gdi32.dll、Advapi32.dll。這些DLL包裝了ntdll.dll的native API。其中Gdi32.dll比較特殊,它與核心態的win32k.sys直接保持聯繫,以提高NT系統的圖形處理能力。Win32子系統的DLL們提供的接口函數在MSDN文檔中被詳細介紹,它們就是Win32 API。

附錄0 Windows的啓動

計算機上電後,從BIOS的ROM開始運行。BIOS在做一些初始化後會將硬盤的第一個扇區的數據讀入內存,然後將控制權交給它,這段數據被稱作Master Boot Record(MBR)。

MBR包含一段啓動代碼和硬盤的主分區表。這段啓動代碼掃描主分區表,找到第一個可以啓動的分區,然後將這個分區的第一個扇區讀入內存並運行。這個扇區被稱作引導扇區(boot sector)。

引導扇區的代碼具備讀文件系統根目錄的能力,顯然不同的文件系統需要不同的代碼。引導扇區會從根目錄中讀出一個叫作ntldr的文件。顧名思義,這個文件是load NT的主要角色。它的業績主要包括將CPU從實模式轉入保護模式,啓動分頁機制,處理boot.ini等。

如果boot.ini中有一句:

C:/bootsect.rh="Red Hat Linux"

bootsect.rh的內容是Linux引導扇區,用戶又選擇了“Red Hat Linux”,ntldr就會將執行Linux的引導扇區,開始Linux的引導。如果用戶選擇繼續使用Windows,ntldr會裝載並運行我們前面提到的ntoskrnl.exe。

ntoskrnl.exe會啓動會話管理器smss.exe。smss.exe啓動csrss.exe和winlogon.exe。smss.exe會永遠等待csrss.exe和winlogon.exe返回。如果兩者之一異常中止,就會導致系統崩潰。所以病毒們經常以打擊csrss.exe爲樂。

winlogon.exe負責用戶登錄,在完成登錄後,它會啓動註冊表HKLM/SOFTWARE/Microsoft/Windows NT/Current Version/Winlogon項下Userinit值指定的程序。該值的缺省數據是userinit.exe。userinit.exe會裝載個人設置,讓硬盤響個不停,並考驗我們的耐性,最後啓動註冊表同一項下Shell值指定的程序。該值的缺省數據是Explorer.exe。Explorer.exe運行後,我們就會看到熟悉的開始菜單和桌面。

1 Win32 API的A/W函數

要了解Win32子系統的DLL們提供了哪些API,最直接的方法就是用Win32dsm直接查看DLL們的導出表。這時我們會發現Win32 API中帶字符串的API一般都有兩個版本,例如CreateFileA和CreateFileW。當然也有例外,例如GetProcAddress函數。

A代表ANSI代碼頁,W是寬字符,即Unicode字符。Windows中的Unicode字符一般指UCS2的UTF16-LE編碼。讓我們通過幾個實例觀察A/W版本間的關係。

例1:用WIn32dsm查看gdi32.dll的彙編代碼,可以看到TextOutA調用GdiGetCodePage獲取當前代碼頁,再調用MultiByteToWideChar轉換輸入的字符串,然後調用一個內部函數。而TextOutW直接調用這個內部函數。

例2:用調試器跟蹤一個使用了CreateFileA的程序,可以看到:CreateFileA在將輸入字符串轉換爲Unicode後,會調用CreateFileW。假設輸入文件名是“測試.txt”,對應的數據就是:“B2 E2 CA D4 2E 74 78 74 00”。
在調試器中可以看到傳給CreateFileW的文件名數據是:“4B 6D D5 8B 2E 00 74 00 78 00 74 00 00 00”。 這是"測試.txt"對應的Unicdoe字符串。CreateFileW會接着調用ntdll.dll中的NtCreateFile。順便看看NtCreateFile的代碼:
mov eax, 00000020
lea edx, dword ptr [esp+04]
int 2E
ret 002C
可見這個native API只是簡單地調用了核心態提供的0x20號system service。

例3:gdi32.dll中的GetGlyphOutline函數可以獲取指定字符的字模。GetGlyphOutlineA和GetGlyphOutlineW函數都會調用同一個內部函數(記作F)。函數F在返回前將通過int 2E調用0x10B1號system service。
GetGlyphOutlineW直接調用函數F。GetGlyphOutlineA在調用函數F前,要依次調用GdiGetCodePage、IsDBCSLeadByteEx和MultiByteToWideChar,將當前代碼頁的字符編碼轉換成Unicode編碼。
如果我們調用GetGlyphOutlineA時傳入“baba”,這是“漢”字的GBK編碼,用調試器可以看到傳給函數F的字符編碼是“6c49”,這是“漢”字的Unicode編碼。

從以上例子可見,A版本總會在某處將輸入的字符串轉換爲Unicode字符串,然後和W版本執行相同的代碼。在由A/W版本API引出MBCS程序和Unicode程序前,讓我們先解釋一下Locale和ANSI代碼頁。

2 Locale和ANSI代碼頁

2.1 Locale和LCID

Locale是指特定於某個國家或地區的一組設定,包括字符集,數字、貨幣、時間和日期的格式等。在Windows中,每個Locale可以用一個32位數字表示,記作LCID。在winnt.h中可以看到LCID的組成。它的高16位表示字符的排序方法,一般爲0。在它的低16位中,低10位是primary language的ID,高4位指定sublanguage。sublanguage被用來區分同一種語言的不同編碼。下面是部分primary language和sublanguage的常數定義:

#define LANG_CHINESE 0x04
#define LANG_ENGLISH 0x09
#define LANG_FRENCH 0x0c
#define LANG_GERMAN 0x07

#define SUBLANG_CHINESE_TRADITIONAL 0x01 // Chinese (Taiwan Region)
#define SUBLANG_CHINESE_SIMPLIFIED 0x02 // Chinese (PR China)
#define SUBLANG_ENGLISH_US 0x01 // English (USA)
#define SUBLANG_ENGLISH_UK 0x02 // English (UK)

好,現在我們可以計算簡體中文的LCID了,將sublanguage的常數左移10位,即乘上1024,再加上primary language的常數:2*1024+4=2052,16進制是0804。美國英語是:1*1024+9=1033,16進制是0409。。繁體中文是1*1024+4=1028,16進制是0404。

2.2 代碼頁

每個Locale都聯繫着很多信息,可以通過GetLocalInfo函數讀取。其中最重要的信息就是字符集了,即Locale對應的語言文字的編碼。Windows將字符集稱作代碼頁。

每個Locale可以對應一個ANSI代碼頁和一個OEM代碼頁。Win32 API使用ANSI代碼頁,底層設備使用OEM代碼頁,兩者可以相互映射。

例如English (US)的ANSI和OEM代碼頁分別爲“1252 (ANSI - Latin I)”和“437 (OEM - United States)”。 Chinese (PRC)的ANSI和OEM代碼頁都是“936 (ANSI/OEM - Simplified Chinese GBK)”。  Chinese (TW)的ANSI和OEM代碼頁都是“950 (ANSI/OEM - Traditional Chinese Big5)”。

附錄1中有一張很長的表。列出了我正在使用的Windows所支持的135個Locale的部分信息,包括 LCID、國家/地區名稱、語言名稱、語言縮寫和對應的ANSI代碼頁。

2.3 系統Locale、用戶Locale,再談ANSI代碼頁

在Windows中,通過控制面板可以爲系統和用戶分別設置Locale。系統Locale決定代碼頁,用戶Locale決定數字、貨幣、時間和日期的格式。這不是一個好的設計,後面會談到它帶來的問題。

使用GetSystemDefaultLCID函數和GetUserDefaultLCID函數分別得到系統和用戶的LCID。有很多材料將這兩個函數和另外兩個函數混淆:GetSystemDefaultUILanguage和GetUserDefaultUILanguage。

GetSystemDefaultUILanguage和GetUserDefaultUILanguage得到的是您當前使用的Windows版本所帶的UI資源的語言。

用戶程序缺省使用的代碼頁是當前系統Locale的ANSI代碼頁,可以稱作ANSI編碼,也就是A版本的Win32 API默認的字符編碼。對於一個未指定編碼方式的文本文件,Windows會按照ANSI編碼解釋。

2.4 AppLocale

如果一個文本文件採用BIG5編碼,系統當前的ANSI代碼頁是GBK。打開這個文件,就會顯示亂碼。例如“中文”在BIG5中的編碼是A4A4、A4E5,這兩個編碼在GBK中對應的字符是“いゅ”。這是日文的兩個平假名。

在Windows XP平臺有一個AppLocale程序,可以以指定的語言運行非Unicode程序。用Win32dsm打開看一看,其實它只是在運行程序前設置了兩個環境變量。我們可以用個批處理文件模仿一下:

@ECHO OFF
SET __COMPAT_LAYER=#ApplicationLocale
SET ApplocaleID=0404
start notepad.exe

在簡體中文平臺,用這個批處理文件啓動的記事本可以正確顯示BIG5編碼的文本文件。用它打開GBK編碼的文本文件會怎麼樣?“中文”會被顯示爲“笢恅”。設置這兩個環境變量會作用於當前進程和其子進程。Windows 2000平臺不支持這個方法。

3 MBCS程序和Unicode程序

3.1 與字符編碼有關的編譯參數

讓我們回到Win32 API。我們在程序中使用的Win32 API沒有A/W後綴,Windows的頭文件會根據編譯參數UNICODE將沒有後綴的函數名替換爲A版本或W版本,例如:

#ifdef UNICODE
#define CreateFile CreateFileW
#else
#define CreateFile CreateFileA
#endif

C RunTime庫(CRT)使用_UNICODE和_MBCS來區分三套字符串處理函數,分別用於SBCS、MBCS和Unicdoe字符串。SBCS和MBCS分別指單字節字符串和多字節字符串。例如_tcsclen的3個版本分別爲strlen、_mbslen和wcslen ,猜猜以下函數返回幾?

strlen("VOIP網關");
_mbslen((unsigned char *)"VOIP網關");
wcslen(L"VOIP網關");

答案是8、6、6。L"ANSI字符串"通知編譯器將ANSI字符串轉換爲Unicode字符串,這是VC++編譯器提供的一個小甜點。不過我們應該用宏:_T("ANSI字符串")。_T宏只在我們定義了_UNICODE時才轉換。這樣同一套代碼既可以編譯MBCS版本,也可以編譯Unicode版本。

MFC用_UNICODE參數區分Unicode版本特有的代碼,決定使用什麼版本的導入庫或靜態庫。

3.2 Unicode程序、MBCS程序和多語言支持

Unicode程序直接使用Unicode版本的CRT和Win32 API。Unicode程序的運行與當前的ANSI代碼頁沒有關係。MBCS程序的運行依賴於ANSI代碼頁。如果設計者和使用者使用不同的代碼頁,就可能出現亂碼。微軟開發的程序大都是Unicode程序,不管我們怎樣變換系統Locale,它們總能正常運行。

使用VCL類庫的Delphi程序都是MBCS程序。VCL框架在程序啓動會調用GetThreadLocale獲取當前用戶的LCID,然後在當前目錄查找對應的資源文件,命名規則是:程序名+'.'+語言縮寫,語言縮寫可以參見附錄1。在找不到時纔會使用EXE文件中的資源。不過如果系統LCID是English(United States),用戶LCID是Chinese(PRC),由VCL產生的程序就會出現亂碼。讀者可以自己分析原因。

爲VCL程序做多語言版本。只要用Delphi自帶的Resource DLL Wizard再做一個特定語言的資源DLL,原來的程序都不用改。不過很多程序員用其它組件做多語言版本,例如TsiLang 。

MBCS程序雖然也可以做成多語言版本,但它無法在同時顯示不同代碼頁特有的字符,這時就必須使用Unicode程序了。

VS.NET文檔中有個多語言資源的例子:SatDLL。它只用Win32 API的例子,卻用了VC7項目。我在學習時將它改成了VC6項目,並糾正了它的兩個問題:
1、用GetUserDefaultUILanguage讀到的是Windows資源版本,不是當前用戶設置的代碼頁。
2、啓動時沒有使用資源DLL裏的菜單。

在我的個人主頁(http://www.fmddlmyy.cn)上可以下載修改過的SatDll。這個程序說明了支持多語言資源的基本思路:將不同語言資源放到不同的DLL中,在程序啓動時根據當前Locale裝載對應的資源DLL。必要時動態切換資源。爲了標記不同語言的資源,可以將它們放到不同的目錄中,以LCID作爲目錄名,例如“2052”、“1033”。當然我們也可以用其它方法聯繫LCID和資源DLL。

MFC程序可以在App類的InitInstance函數中用AfxSetResourceHandle函數設置資源DLL。在Delphi中動態切換資源可以參考Delphi Demo目錄RichEdit項目的ReInit.pas。在讀取當前設定時,建議用GetSystemDefaultLCID函數,因爲系統Locale決定ANSI代碼頁。

3.4 資源和亂碼

通過檢查可執行文件,我們可以確定VC和Delphi的資源編譯器都以Unicode保存字符資源。在VC環境編輯資源時,我們會指定資源的代碼頁。編譯器根據資源的代碼頁,將其轉換到Unicode。

Unicode程序直接使用以Unicode編碼保存的資源。MBCS程序需要將Unicode資源先轉換回當前ANSI代碼頁,然後再使用。如果資源中的Unicode字符串不能映射到當前代碼頁中的字符,就會出現??。

例如Windows的標準對話框也會出現亂碼。假設我們使用簡體中文Windows,當前Locale是Chinese (TW),我們的程序是MBCS的,使用標準的打開文件對話框。因爲在BIG5中沒有“開”這個字,所以“打開”會被顯示成“打?”。將程序編譯成Unicode版本,就可以避免這個問題。

如果字符不是保存在資源中,而是硬編碼在程序中。然後開發者和用戶使用不同的代碼頁,就會導致亂碼。假設開發者的Locale是Chinese (PRC),用戶的Locale是English (US),程序中硬編碼了字符串“文件”。 Chinese (PRC)的ANSI代碼頁是GBK,“文件”的編碼“CE C4 BC FE”。English (US)的ANSI代碼頁是Latin I,用戶按照Latin I編碼去解釋“CE C4 BC FE”,就會看到“Îļþ”。

回答我前面提過的一個問題:Delphi程序根據用戶LCID轉換資源中的字符串。如果用戶LCID是Chinese (PRC),系統LCID是English (US)。那麼資源中的Unicode字符串會被轉換爲GBK編碼,然後按照Latin I顯示,這時我們看到的就是類似“Îļþ”的東東,不是??。

既然資源是以Unicode保存的,MBCS程序如果不將其轉換到ANSI代碼頁,而用W版本的函數直接顯示,就不會產生亂碼。例如MFC程序菜單裏的中文,在English (US)的Locale也可以正常顯示。不過這取決於各部分代碼的具體實現,menu bar控件裏的中文在English (US)的Locale會全部顯示成??。

進一步的參考資料

本文的第0節和附錄0主要參考了《Inside Windows 2000 Third Edition》,國內出過該書的影印版。DDK文檔中有大量Windows內核的信息。用Win32dsm和各種調試器查看Windows系統文件可以獲得更直接的信息。

關於Window程序的字符編碼,最好的參考資料是winnt.h等SDK的包含文件、VCL、MFC、CRT的源文件。我們不需要閱讀它們,只要找到自己感興趣的信息就可以了,用Source Insight可能方便一些。

本文所談的不是什麼萬古不遷的道理,只是別的程序員的一些設定,我們因爲需要使用他們的程序,所以有必要了解一些細節。研究問題的方法和興趣永遠比問題本身重要,如一句拉丁俗語所說:res, non verba,實質勝於文字。

尾聲

“明月雖有圓缺,但畢竟永恆不滅,人生卻如過眼煙雲,一去不回,真不知計較爲何?”

“蛙聲雖是短促,但卻是萬籟中一個活潑的禪機,也可以說萬古如斯,永恆不遷,無奈感受到的,能有幾人?”

這是一本武俠書中的對話。在時間的長河中,人生和蛙聲一樣易逝。說到蛙聲,我的20個月的小寶寶在喝湯後,略加醞釀,就會緊閉着嘴巴,發出很像蛙鳴的聲音。我們會逗他說:“小青蛙又來了”。小傢伙益發得意,不管我的抗議,將連湯帶油的小下巴親熱地貼在我的身上。

 

附錄1 一些關於LCID的信息

使用EnumSystemLocales函數可以枚舉系統支持的LCID。用GetLocaleInfo可以得到ANSI代碼頁的ID,再通過GetCPInfoEx可以獲得代碼頁的全稱。以下是我在中文Windows XP上讀到的內容。

LCID

國家或地區

語言

語言縮寫

ANSI代碼頁

1025

沙特阿拉伯

阿拉伯語(沙特阿拉伯)

ARA

1256  (ANSI - 阿拉伯文)

1026

保加利亞

保加利亞語

BGR

1251  (ANSI - 西里爾文)

1027

西班牙

加泰隆語

CAT

1252  (ANSI - 拉丁文 I)

1028

臺灣

中文(臺灣)

CHT

950   (ANSI/OEM - 繁體中文 Big5)

1029

捷克共和國

捷克語

CSY

1250  (ANSI - 中歐)

1030

丹麥

丹麥語

DAN

1252  (ANSI - 拉丁文 I)

1031

德國

德語(德國)

DEU

1252  (ANSI - 拉丁文 I)

1032

希臘

希臘語

ELL

1253  (ANSI - 希臘文)

1033

美國

英語(美國)

ENU

1252  (ANSI - 拉丁文 I)

1034

西班牙

西班牙語(傳統)

ESP

1252  (ANSI - 拉丁文 I)

1035

芬蘭

芬蘭語

FIN

1252  (ANSI - 拉丁文 I)

1036

法國

法語(法國)

FRA

1252  (ANSI - 拉丁文 I)

1037

以色列

希伯來語

HEB

1255  (ANSI - 希伯來文)

1038

匈牙利

匈牙利語

HUN

1250  (ANSI - 中歐)

1039

冰島

冰島語

ISL

1252  (ANSI - 拉丁文 I)

1040

意大利

意大利語(意大利)

ITA

1252  (ANSI - 拉丁文 I)

1041

日本

日語

JPN

932   (ANSI/OEM - 日文 Shift-JIS)

1042

朝鮮

朝鮮語

KOR

949   (ANSI/OEM - 韓文)

1043

荷蘭

荷蘭語(荷蘭)

NLD

1252  (ANSI - 拉丁文 I)

1044

挪威

挪威語(伯克梅爾)

NOR

1252  (ANSI - 拉丁文 I)

1045

波蘭

波蘭語

PLK

1250  (ANSI - 中歐)

1046

巴西

葡萄牙語(巴西)

PTB

1252  (ANSI - 拉丁文 I)

1048

羅馬尼亞

羅馬尼亞語

ROM

1250  (ANSI - 中歐)

1049

俄羅斯

俄語

RUS

1251  (ANSI - 西里爾文)

1050

克羅地亞

克羅地亞語

HRV

1250  (ANSI - 中歐)

1051

斯洛伐克語

斯洛伐克語

SKY

1250  (ANSI - 中歐)

1052

阿爾巴尼亞

阿爾巴尼亞語

SQI

1250  (ANSI - 中歐)

1053

瑞典

瑞典語

SVE

1252  (ANSI - 拉丁文 I)

1054

泰國

泰語

THA

874   (ANSI/OEM - 泰文)

1055

土耳其

土耳其語

TRK

1254  (ANSI - 土耳其文)

1056

巴基斯坦伊斯蘭共和國

烏都語

URD

1256  (ANSI - 阿拉伯文)

1057

印度尼西亞

印度尼西亞語

IND

1252  (ANSI - 拉丁文 I)

1058

烏克蘭

烏克蘭語

UKR

1251  (ANSI - 西里爾文)

1059

比利時

比利時語

BEL

1251  (ANSI - 西里爾文)

1060

斯洛文尼亞

斯洛文尼亞語

SLV

1250  (ANSI - 中歐)

1061

愛沙尼亞

愛沙尼亞語

ETI

1257  (ANSI - 波羅的海文)

1062

拉脫維亞

拉脫維亞語

LVI

1257  (ANSI - 波羅的海文)

1063

立陶宛

立陶宛語

LTH

1257  (ANSI - 波羅的海文)

1065

伊朗

法斯語

FAR

1256  (ANSI - 阿拉伯文)

1066

越南

越南語

VIT

1258  (ANSI/OEM - 越南)

1067

亞美尼亞

亞美尼亞語

HYE

936   (ANSI/OEM - 簡體中文 GBK)

1068

阿塞拜疆

阿塞拜疆語(拉丁文)

AZE

1254  (ANSI - 土耳其文)

1069

西班牙

巴士克語

EUQ

1252  (ANSI - 拉丁文 I)

1071

前南斯拉夫馬其頓共和國

馬其頓語(FYROM)

MKI

1251  (ANSI - 西里爾文)

1078

南非

南非語

AFK

1252  (ANSI - 拉丁文 I)

1079

格魯吉亞

格魯吉亞語

KAT

936   (ANSI/OEM - 簡體中文 GBK)

1080

法羅羣島

法羅語

FOS

1252  (ANSI - 拉丁文 I)

1081

印度

印地語

HIN

936   (ANSI/OEM - 簡體中文 GBK)

1086

馬來西亞

馬來語(馬來西亞)

MSL

1252  (ANSI - 拉丁文 I)

1087

吉爾吉斯坦

哈薩克語

KKZ

1251  (ANSI - 西里爾文)

1088

吉爾吉斯斯坦

吉爾吉斯語 (西里爾文)

KYR

1251  (ANSI - 西里爾文)

1089

肯尼亞

斯瓦希里語

SWK

1252  (ANSI - 拉丁文 I)

1091

烏茲別克斯坦

烏茲別克語(拉丁文)

UZB

1254  (ANSI - 土耳其文)

1092

韃靼斯坦

韃靼語

TTT

1251  (ANSI - 西里爾文)

1094

印度

旁遮普語

PAN

936   (ANSI/OEM - 簡體中文 GBK)

1095

印度

古吉拉特語

GUJ

936   (ANSI/OEM - 簡體中文 GBK)

1097

印度

泰米爾語

TAM

936   (ANSI/OEM - 簡體中文 GBK)

1098

印度

泰盧固語

TEL

936   (ANSI/OEM - 簡體中文 GBK)

1099

印度

卡納拉語

KAN

936   (ANSI/OEM - 簡體中文 GBK)

1102

印度

馬拉地語

MAR

936   (ANSI/OEM - 簡體中文 GBK)

1103

印度

梵文

SAN

936   (ANSI/OEM - 簡體中文 GBK)

1104

蒙古

蒙古語(西里爾文)

MON

1251  (ANSI - 西里爾文)

1110

西班牙

加里西亞語

GLC

1252  (ANSI - 拉丁文 I)

1111

印度

孔卡尼語

KNK

936   (ANSI/OEM - 簡體中文 GBK)

1114

敘利亞

敘利亞語

SYR

936   (ANSI/OEM - 簡體中文 GBK)

1125

馬爾代夫

第維埃語

DIV

936   (ANSI/OEM - 簡體中文 GBK)

2049

伊拉克

阿拉伯語(伊拉克)

ARI

1256  (ANSI - 阿拉伯文)

2052

中華人民共和國

中文(中國)

CHS

936   (ANSI/OEM - 簡體中文 GBK)

2055

瑞士

德語(瑞士)

DES

1252  (ANSI - 拉丁文 I)

2057

英國

英語(英國)

ENG

1252  (ANSI - 拉丁文 I)

2058

墨西哥

西班牙語(墨西哥)

ESM

1252  (ANSI - 拉丁文 I)

2060

比利時

法語(比利時)

FRB

1252  (ANSI - 拉丁文 I)

2064

瑞士

意大利語(瑞士)

ITS

1252  (ANSI - 拉丁文 I)

2067

比利時

荷蘭語(比利時)

NLB

1252  (ANSI - 拉丁文 I)

2068

挪威

挪威語(尼諾斯克)

NON

1252  (ANSI - 拉丁文 I)

2070

葡萄牙

葡萄牙語(葡萄牙)

PTG

1252  (ANSI - 拉丁文 I)

2074

塞爾維亞

塞爾維亞語(拉丁文)

SRL

1250  (ANSI - 中歐)

2077

芬蘭

瑞典語(芬蘭)

SVF

1252  (ANSI - 拉丁文 I)

2092

阿塞拜疆

阿塞拜疆語(西里爾文)

AZE

1251  (ANSI - 西里爾文)

2110

文萊達魯薩蘭

馬來語(文萊達魯薩蘭)

MSB

1252  (ANSI - 拉丁文 I)

2115

烏茲別克斯坦

烏茲別克語(西里爾文)

UZB

1251  (ANSI - 西里爾文)

3073

埃及

阿拉伯語(埃及)

ARE

1256  (ANSI - 阿拉伯文)

3076

香港特別行政區

中文(香港特別行政區)

ZHH

950   (ANSI/OEM - 繁體中文 Big5)

3079

奧地利

德語(奧地利)

DEA

1252  (ANSI - 拉丁文 I)

3081

澳大利亞

英語(澳大利亞)

ENA

1252  (ANSI - 拉丁文 I)

3082

西班牙

西班牙語(國際)

ESN

1252  (ANSI - 拉丁文 I)

3084

加拿大

法語(加拿大)

FRC

1252  (ANSI - 拉丁文 I)

3098

塞爾維亞

塞爾維亞語(西里爾文)

SRB

1251  (ANSI - 西里爾文)

4097

利比亞

阿拉伯語(利比亞)

ARL

1256  (ANSI - 阿拉伯文)

4100

新加坡

中文(新加坡)

ZHI

936   (ANSI/OEM - 簡體中文 GBK)

4103

盧森堡

德語(盧森堡)

DEL

1252  (ANSI - 拉丁文 I)

4105

加拿大

英語(加拿大)

ENC

1252  (ANSI - 拉丁文 I)

4106

危地馬拉

西班牙語(危地馬拉)

ESG

1252  (ANSI - 拉丁文 I)

4108

瑞士

法語(瑞士)

FRS

1252  (ANSI - 拉丁文 I)

5121

阿爾及利亞

阿拉伯語(阿爾及利亞)

ARG

1256  (ANSI - 阿拉伯文)

5124

澳門特別行政區

中文(澳門特別行政區)

ZHM

950   (ANSI/OEM - 繁體中文 Big5)

5127

列支敦士登

德語(列支敦士登)

DEC

1252  (ANSI - 拉丁文 I)

5129

新西蘭

英語(新西蘭)

ENZ

1252  (ANSI - 拉丁文 I)

5130

哥斯達黎加

西班牙語(哥斯達黎加)

ESC

1252  (ANSI - 拉丁文 I)

5132

盧森堡

法語(盧森堡)

FRL

1252  (ANSI - 拉丁文 I)

6145

摩洛哥

阿拉伯語(摩洛哥)

ARM

1256  (ANSI - 阿拉伯文)

6153

愛爾蘭

英語(愛爾蘭)

ENI

1252  (ANSI - 拉丁文 I)

6154

巴拿馬

西班牙語(巴拿馬)

ESA

1252  (ANSI - 拉丁文 I)

6156

摩納哥公國

法語(摩納哥)

FRM

1252  (ANSI - 拉丁文 I)

7169

突尼斯

阿拉伯語(突尼斯)

ART

1256  (ANSI - 阿拉伯文)

7177

南非

英語(南非)

ENS

1252  (ANSI - 拉丁文 I)

7178

多米尼加共和國

西班牙語(多米尼加共和國)

ESD

1252  (ANSI - 拉丁文 I)

8193

阿曼

阿拉伯語(阿曼)

ARO

1256  (ANSI - 阿拉伯文)

8201

牙買加

英語(牙買加)

ENJ

1252  (ANSI - 拉丁文 I)

8202

委內瑞拉

西班牙語(委內瑞拉)

ESV

1252  (ANSI - 拉丁文 I)

9217

也門

阿拉伯語(也門)

ARY

1256  (ANSI - 阿拉伯文)

9225

加勒比海

英語(加勒比海)

ENB

1252  (ANSI - 拉丁文 I)

9226

哥倫比亞

西班牙語(哥倫比亞)

ESO

1252  (ANSI - 拉丁文 I)

10241

敘利亞

阿拉伯語(敘利亞)

ARS

1256  (ANSI - 阿拉伯文)

10249

伯利茲

英語(伯利茲)

ENL

1252  (ANSI - 拉丁文 I)

10250

祕魯

西班牙語(祕魯)

ESR

1252  (ANSI - 拉丁文 I)

11265

約旦

阿拉伯語(約旦)

ARJ

1256  (ANSI - 阿拉伯文)

11273

特立尼達和多巴哥

英語(特立尼達)

ENT

1252  (ANSI - 拉丁文 I)

11274

阿根廷

西班牙語(阿根廷)

ESS

1252  (ANSI - 拉丁文 I)

12289

黎巴嫩

阿拉伯語(黎巴嫩)

ARB

1256  (ANSI - 阿拉伯文)

12297

津巴布韋

英語(津巴布韋)

ENW

1252  (ANSI - 拉丁文 I)

12298

厄瓜多爾

西班牙語(厄瓜多爾)

ESF

1252  (ANSI - 拉丁文 I)

13313

科威特

阿拉伯語(科威特)

ARK

1256  (ANSI - 阿拉伯文)

13321

菲律賓共和國

英語(菲律賓)

ENP

1252  (ANSI - 拉丁文 I)

13322

智利

西班牙語(智利)

ESL

1252  (ANSI - 拉丁文 I)

14337

阿聯酋

阿拉伯語(阿聯酋)

ARU

1256  (ANSI - 阿拉伯文)

14346

烏拉圭

西班牙語(烏拉圭)

ESY

1252  (ANSI - 拉丁文 I)

15361

巴林

阿拉伯語(巴林)

ARH

1256  (ANSI - 阿拉伯文)

15370

巴拉圭

西班牙語(巴拉圭)

ESZ

1252  (ANSI - 拉丁文 I)

16385

卡塔爾

阿拉伯語(卡塔爾)

ARQ

1256  (ANSI - 阿拉伯文)

16394

玻利維亞

西班牙語(玻利維亞)

ESB

1252  (ANSI - 拉丁文 I)

17418

薩爾瓦多

西班牙語(薩爾瓦多)

ESE

1252  (ANSI - 拉丁文 I)

18442

洪都拉斯

西班牙語(洪都拉斯)

ESH

1252  (ANSI - 拉丁文 I)

19466

尼加拉瓜

西班牙語(尼加拉瓜)

ESI

1252  (ANSI - 拉丁文 I)

20490

波多黎各(美)

西班牙語(波多黎各(美))

ESU

1252  (ANSI - 拉丁文 I)

LCID取決於語言,在表中列出國家名只是爲了增加趣味性。例如可以看到以色列還在使用古老的希伯來語。“希伯來語”的法文是hébreu,這個單詞還有一個意思,就是“不能理解的東西”。


1 文字的顯示

1.1 發生了什麼?

我們首先以Windows爲例來看看文字顯示過程中發生了什麼。用記事本打開一個文本文件,可以看到文件包含的文字:

字符和編碼

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

D7D6 B7FB BACD B1E0 C2EB

我們看到:文件“例子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個步驟中任何一步發生了錯誤,文字就不能被正確顯示,例如:

  • 錯誤1:如果弄錯了編碼,例如將Big5編碼的文字當作GBK編碼,就會出現亂碼。

  • 錯誤2:如果從特定編碼到Unicode的映射發生錯誤,例如文本數據中出現該編碼方案未定義的字符,Windows就會使用缺省字符,通常是?。

  • 如果當前字體不支持要顯示的字符,Windows就會顯示字體文件中的缺省圖像:空白或方格

在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系列所有字符集的編碼,例如:

 

3 字符編碼模型

程序員經常會面對複雜的問題,而降低複雜性的最簡單的方法就是分而治之。Peter Constable在他的文章"Character set encoding basics Understanding character set encodings and legacy encodings"中描述了字符編碼的四層模型。我覺得這種說法確實可以更清晰地展現字符編碼中發生的事情,所以在這裏也介紹一下。

3.1 字符的範圍(Abstract character repertoire)

設計字符編碼的第一層就是確定字符的範圍,即要支持哪些字符。有些編碼方案的字符範圍是固定的,例如ASCII、ISO 8859 系列。有些編碼方案的字符範圍是開放的,例如Unicode的字符範圍就是世界上所有的字符。

3.2 用數字表示字符(Coded character set)

設計字符編碼的第二層是將字符和數字對應起來。可以將這個層次理解成數學家(即從數學角度)看到的字符編碼。數學家看到的字符編碼是一個正整數。例如在Unicode中:漢字“字”對應的數字是23383。漢字“”對應的數字是134192。

在寫html文件時,可以通過輸入"&#23383;"來插入字符“字”。不過在設計字符編碼時,我們還是習慣用16進製表示數字。即將23383寫成0x5BD7,將134192寫成0x20C30。

3.3 用基本數據類型表示字符(Character encoding form)

設計字符編碼的第三層是用編程語言中的基本數據類型來表示字符。可以將這個層次理解成程序員看到的字符編碼。在Unicode中,我們有很多方式將數字23383表示成程序中的數據,包括:UTF-8、UTF-16、UTF-32。UTF是“UCS Transformation Format”的縮寫,可以翻譯成Unicode字符集轉換格式,即怎樣將Unicode定義的數字轉換成程序數據。例如,“漢字”對應的數字是0x6c49和0x5b57,而編碼的程序數據是:

BYTE data_utf8[]={0xE6,0xB1,0x89,0xE5,0xAD,0x97}; // UTF-8編碼
WORD data_utf16[]={0x6c49,0x5b57};                            // UTF-16編碼
DWORD data_utf32[]={0x6c49,0x5b57};                         // UTF-32編碼

這裏用BYTE、WORD、DWORD分別表示無符號8位整數,無符號16位整數和無符號32位整數。UTF-8、UTF-16、UTF-32分別以BYTE、WORD、DWORD作爲編碼單位。

“漢字”的UTF-8編碼需要6個字節。“漢字”的UTF-16編碼需要兩個WORD,大小是4個字節。“漢字”的UTF-32編碼需要兩個DWORD,大小是8個字節。4.2節會介紹將數字映射到UTF編碼的規則。

3.4 作爲字節流的字符(Character encoding scheme)

字符編碼的第四層是計算機看到的字符,即在文件或內存中的字節流。例如,“字”的UTF-32編碼是0x5b57,如果用little endian表示,字節流是“57 5b 00 00”。如果用big endian表示,字節流是“00 00 5b 57”。

字符編碼的第三層規定了一個字符由哪些編碼單位按什麼順序表示。字符編碼的第四層在第三層的基礎上又考慮了編碼單位內部的字節序。UTF-8的編碼單位是字節,不受字節序的影響。UTF-16、UTF-32根據字節序的不同,又衍生出UTF-16LE、UTF-16BE、UTF-32LE、UTF-32BE四種編碼方案。LE和BE分別是Little Endian和Big Endian的縮寫。

3.5 小結

通過四層模型,我們又把字符編碼中發生的這些事情梳理了一遍。其實大多數代碼頁都不需要完整的四層模型,例如GB18030以字節爲編碼單位,直接規定了字節序列和字符的映射關係,跳過了第二層,也不需要第四層。

4 再談Unicode

Unicode是國際組織制定的可以容納世界上所有文字和符號的字符編碼方案。Unicode用數字0-0x10FFFF來映射這些字符,最多可以容納1114112個字符,或者說有1114112個碼位。碼位就是可以分配給字符的數字。UTF-8、UTF-16、UTF-32都是將數字轉換到程序數據的編碼方案。

Unicode字符集可以簡寫爲UCS(Unicode Character Set)。早期的Unicode標準有UCS-2、UCS-4的說法。UCS-2用兩個字節編碼,UCS-4用4個字節編碼。UCS-4根據最高位爲0的最高字節分成2^7=128個group。每個group再根據次高字節分爲256個平面(plane)。每個平面根據第3個字節分爲256行 (row),每行有256個碼位(cell)。group 0的平面0被稱作BMP(Basic Multilingual Plane)。將UCS-4的BMP去掉前面的兩個零字節就得到了UCS-2。

Unicode標準計劃使用group 0 的17個平面: 從BMP(平面0)到平面16,即數字0-0x10FFFF。《談談Unicode編碼》主要介紹了BMP的編碼,本文將介紹完整的Unicode編碼,並從多個角度瀏覽Unicode。本文的介紹基於Unicode 5.0.0版本。

4.1 瀏覽Unicode

先看一些數字:每個平面有2^16=65536個碼位。Unicode計劃使用了17個平面,一共有17*65536=1114112個碼位。其實,現在已定義的碼位只有238605個,分佈在平面0、平面1、平面2、平面14、平面15、平面16。其中平面15和平面16上只是定義了兩個各佔65534個碼位的專用區(Private Use Area),分別是0xF0000-0xFFFFD和0x100000-0x10FFFD。所謂專用區,就是保留給大家放自定義字符的區域,可以簡寫爲PUA。

平面0也有一個專用區:0xE000-0xF8FF,有6400個碼位。平面0的0xD800-0xDFFF,共2048個碼位,是一個被稱作代理區(Surrogate)的特殊區域。它的用途將在4.2節介紹。

238605-65534*2-6400-2408=99089。餘下的99089個已定義碼位分佈在平面0、平面1、平面2和平面14上,它們對應着Unicode目前定義的99089個字符,其中包括71226個漢字。平面0、平面1、平面2和平面14上分別定義了52080、3419、43253和337個字符。平面2的43253個字符都是漢字。平面0上定義了27973個漢字。

在更深入地瞭解Unicode字符前,我們先了解一下UCD。

4.1.1 什麼是UCD

UCD是Unicode字符數據庫(Unicode Character Database)的縮寫。UCD由一些描述Unicode字符屬性和內部關係的純文本或html文件組成。大家可以在Unicode組織的網站看到UCD的最新版本

UCD中的文本文件大都是適合於程序分析的Unicode相關數據。其中的html文件解釋了數據庫的組織,數據的格式和含義。UCD中最龐大的文件無疑就是描述漢字屬性的文件Unihan.txt。在UCD 5.0,0中,Unihan.txt文件大小有28,221K字節。Unihan.txt中包含了很多有參考價值的索引,例如漢字部首、筆劃、拼音、使用頻度、四角號碼排序等。這些索引都是基於一些比較權威的辭典,但大多數索引只能檢索部分漢字。

我介紹UCD的目的主要是爲了使用其中的兩個概念:Block和Script。

4.1.2 Block

UCD中的Blocks.txt將Unicode的碼位分割成一些連續的Block,並描述了每個Block的用途:

開始碼位 結束碼位 Block名稱(英文) Block名稱(中文)
0000 007F Basic Latin 基本拉丁字母
0080 00FF Latin-1 Supplement 拉丁字母補充-1
0100 017F Latin Extended-A 拉丁字母擴充-A
0180 024F Latin Extended-B 拉丁字母擴充-B
0250 02AF IPA Extensions 國際音標擴充
02B0 02FF Spacing Modifier Letters 進格修飾字符
0300 036F Combining Diacritical Marks 組合附加符號
0370 03FF Greek and Coptic 希臘文和哥普特文
0400 04FF Cyrillic 西里爾文
0500 052F Cyrillic Supplement 西里爾文補充
0530 058F Armenian 亞美尼亞文
0590 05FF Hebrew 希伯來文
0600 06FF Arabic 基本阿拉伯文
0700 074F Syriac 敘利亞文
0750 077F Arabic Supplement 阿拉伯文補充
0780 07BF Thaana 塔納文
07C0 07FF NKo N'Ko字母表
0900 097F Devanagari 天成文書(梵文)
0980 09FF Bengali 孟加拉文
0A00 0A7F Gurmukhi 錫克教文
0A80 0AFF Gujarati 古吉拉特文
0B00 0B7F Oriya 奧里亞文
0B80 0BFF Tamil 泰米爾文
0C00 0C7F Telugu 泰盧固文
0C80 0CFF Kannada 卡納達文
0D00 0D7F Malayalam 德拉維族文
0D80 0DFF Sinhala 僧伽羅文
0E00 0E7F Thai 泰文
0E80 0EFF Lao 老撾文
0F00 0FFF Tibetan 藏文
1000 109F Myanmar 緬甸文
10A0 10FF Georgian 格魯吉亞文
1100 11FF Hangul Jamo 朝鮮文
1200 137F Ethiopic 埃塞俄比亞文
1380 139F Ethiopic Supplement 埃塞俄比亞文補充
13A0 13FF Cherokee 切羅基文
1400 167F Unified Canadian Aboriginal Syllabics 加拿大印第安方言
1680 169F Ogham 歐甘文
16A0 16FF Runic 北歐古字
1700 171F Tagalog 塔加路文
1720 173F Hanunoo 哈努諾文
1740 175F Buhid 布迪文
1760 177F Tagbanwa Tagbanwa文
1780 17FF Khmer 高棉文
1800 18AF Mongolian 蒙古文
1900 194F Limbu 林布文
1950 197F Tai Le 德宏傣文
1980 19DF New Tai Lue 新傣文
19E0 19FF Khmer Symbols 高棉文
1A00 1A1F Buginese 布吉文
1B00 1B7F Balinese 巴釐文
1D00 1D7F Phonetic Extensions 拉丁字母音標擴充
1D80 1DBF Phonetic Extensions Supplement 拉丁字母音標擴充增補
1DC0 1DFF Combining Diacritical Marks Supplement 組合附加符號補充
1E00 1EFF Latin Extended Additional 拉丁字母擴充附加
1F00 1FFF Greek Extended 希臘文擴充
2000 206F General Punctuation 一般標點符號
2070 209F Superscripts and Subscripts 上標和下標
20A0 20CF Currency Symbols 貨幣符號
20D0 20FF Combining Diacritical Marks for Symbols 符號用組合附加符號
2100 214F Letterlike Symbols 似字母符號
2150 218F Number Forms 數字形式
2190 21FF Arrows 箭頭符號
2200 22FF Mathematical Operators 數學運算符號
2300 23FF Miscellaneous Technical 零雜技術用符號
2400 243F Control Pictures 控制圖符
2440 245F Optical Character Recognition 光學字符識別
2460 24FF Enclosed Alphanumerics 帶括號的字母數字
2500 257F Box Drawing 製表符
2580 259F Block Elements 方塊元素
25A0 25FF Geometric Shapes 幾何形狀
2600 26FF Miscellaneous Symbols 零雜符號
2700 27BF Dingbats 雜錦字型
27C0 27EF Miscellaneous Mathematical Symbols-A 零雜數學符號-A
27F0 27FF Supplemental Arrows-A 箭頭符號補充-A
2800 28FF Braille Patterns 盲文
2900 297F Supplemental Arrows-B 箭頭符號補充-B
2980 29FF Miscellaneous Mathematical Symbols-B 零雜數學符號-B
2A00 2AFF Supplemental Mathematical Operators 數學運算符號
2B00 2BFF Miscellaneous Symbols and Arrows 零雜符號和箭頭
2C00 2C5F Glagolitic 格拉哥里字母表
2C60 2C7F Latin Extended-C 拉丁字母擴充-C
2C80 2CFF Coptic 科普特文
2D00 2D2F Georgian Supplement 格魯吉亞文補充
2D30 2D7F Tifinagh 提非納字母
2D80 2DDF Ethiopic Extended 埃塞俄比亞文擴充
2E00 2E7F Supplemental Punctuation 標點符號補充
2E80 2EFF CJK Radicals Supplement 中日韓部首補充
2F00 2FDF Kangxi Radicals 康熙字典部首
2FF0 2FFF Ideographic Description Characters 漢字結構描述字符
3000 303F CJK Symbols and Punctuation 中日韓符號和標點
3040 309F Hiragana 平假名
30A0 30FF Katakana 片假名
3100 312F Bopomofo 注音符號
3130 318F Hangul Compatibility Jamo 朝鮮文兼容字母
3190 319F Kanbun 日文的漢字批註
31A0 31BF Bopomofo Extended 注音符號擴充
31C0 31EF CJK Strokes 中日韓筆劃
31F0 31FF Katakana Phonetic Extensions 片假名音標擴充
3200 32FF Enclosed CJK Letters and Months 帶括號的中日韓字母及月份
3300 33FF CJK Compatibility 中日韓兼容字符
3400 4DBF CJK Unified Ideographs Extension A 中日韓統一表意文字擴充A
4DC0 4DFF Yijing Hexagram Symbols 易經六十四卦象
4E00 9FFF CJK Unified Ideographs 中日韓統一表意文字
A000 A48F Yi Syllables 彝文音節
A490 A4CF Yi Radicals 彝文字根
A700 A71F Modifier Tone Letters 聲調修飾字母
A720 A7FF Latin Extended-D 拉丁字母擴充-D
A800 A82F Syloti Nagri Syloti Nagri字母表
A840 A87F Phags-pa Phags-pa字母表
AC00 D7AF Hangul Syllables 朝鮮文音節
D800 DB7F High Surrogates 高位替代
DB80 DBFF High Private Use Surrogates 高位專用替代
DC00 DFFF Low Surrogates 低位替代
E000 F8FF Private Use Area 專用區
F900 FAFF CJK Compatibility Ideographs 中日韓兼容表意文字
FB00 FB4F Alphabetic Presentation Forms 字母變體顯現形式
FB50 FDFF Arabic Presentation Forms-A 阿拉伯文變體顯現形式-A
FE00 FE0F Variation Selectors 字型變換選取器
FE10 FE1F Vertical Forms 豎排標點符號
FE20 FE2F Combining Half Marks 組合半角標示
FE30 FE4F CJK Compatibility Forms 中日韓兼容形式
FE50 FE6F Small Form Variants 小型變體形式
FE70 FEFF Arabic Presentation Forms-B 阿拉伯文變體顯現形式-B
FF00 FFEF Halfwidth and Fullwidth Forms 半角及全角字符
FFF0 FFFF Specials 特殊區域
10000 1007F Linear B Syllabary 線形文字B音節文字
10080 100FF Linear B Ideograms 線形文字B表意文字
10100 1013F Aegean Numbers 愛琴海數字
10140 1018F Ancient Greek Numbers 古希臘數字
10300 1032F Old Italic 古意大利文
10330 1034F Gothic 哥特文
10380 1039F Ugaritic 烏加里特楔形文字
103A0 103DF Old Persian 古波斯文
10400 1044F Deseret 德塞雷特大學音標
10450 1047F Shavian 肅伯納速記符號
10480 104AF Osmanya Osmanya字母表
10800 1083F Cypriot Syllabary 塞浦路斯音節文字
10900 1091F Phoenician 腓尼基文
10A00 10A5F Kharoshthi 迦婁士悌文
12000 123FF Cuneiform 楔形文字
12400 1247F Cuneiform Numbers and Punctuation 楔形文字數字和標點
1D000 1D0FF Byzantine Musical Symbols 東正教音樂符號
1D100 1D1FF Musical Symbols 音樂符號
1D200 1D24F Ancient Greek Musical Notation 古希臘音樂符號
1D300 1D35F Tai Xuan Jing Symbols 太玄經符號
1D360 1D37F Counting Rod Numerals 算籌
1D400 1D7FF Mathematical Alphanumeric Symbols 數學用字母數字符號
20000 2A6DF CJK Unified Ideographs Extension B 中日韓統一表意文字擴充 B
2F800 2FA1F CJK Compatibility Ideographs Supplement 中日韓兼容表意文字補充
E0000 E007F Tags 標籤
E0100 E01EF Variation Selectors Supplement 字型變換選取器補充
F0000 FFFFF Supplementary Private Use Area-A 補充專用區-A
100000 10FFFF Supplementary Private Use Area-B 補充專用區-B

Block是Unicode字符的一個屬性。屬於同一個Block的字符有着相近的用途。Block表中的開始碼位、結束碼位只是用來劃分出一塊區域,在開始碼位和結束碼位之間可能還有很多未定義的碼位。使用UniToy,大家可以按照Block瀏覽Unicode字符,既可以按列表顯示:

也可以顯示每個字符的詳細信息:

4.1.3 Script

Unicode中每個字符都有一個Script屬性,這個屬性表明字符所屬的文字系統。Unicode目前支持以下Script:

Script名稱(英文) Script名稱(中文) Script包含的字符數
Arabic 阿拉伯文 966
Armenian 亞美尼亞文 90
Balinese 巴釐文 121
Bengali 孟加拉文 91
Bopomofo 漢語注音符號 64
Braille 盲文 256
Buginese 布吉文 30
Buhid 布迪文 20
Canadian Aboriginal 加拿大印第安方言 630
Cherokee 切羅基文 85
Common Common 5020
Coptic 科普特文 128
Cuneiform 楔形文字 982
Cypriot 塞浦路斯音節文字 55
Cyrillic 西里爾文 277
Deseret 德塞雷特大學音標 80
Devanagari 天成文書(梵文) 107
Ethiopic 埃塞俄比亞文 461
Georgian 格魯吉亞文 120
Gothic 哥特文 94
Glagolitic 格拉哥里字母表 27
Greek 希臘文 506
Gujarati 古吉拉特文 83
Gurmukhi 錫克教文 77
Han 漢文 71570
Hangul 韓文書寫系統 11619
Hanunoo 哈努諾文 21
Hebrew 希伯來文 133
Hiragana 平假名 89
Inherited Inherited 461
Kannada 卡納達文 86
Katakana 片假名 164
Kharoshthi 迦婁士悌文 65
Khmer 高棉文 146
Lao 老撾文 65
Latin 拉丁文系 1070
Limbu 林布文(尼泊爾東部) 66
Linear B 線形文字B 211
Malayalam 德拉維族文(印度) 78
Mongolian 蒙古文 152
Myanmar 緬甸文 78
New Tai Lue 新傣文 80
Nko N'Ko字母表 59
Ogham 歐甘文字 29
Old Italic 古意大利文 35
Old Persian 古波斯文 50
Oriya 奧里亞文 81
Osmanya Osmanya字母表 40
Phags Pa Phags Pa字母表(蒙古) 56
Phoenician 腓尼基文 27
Runic 古代北歐文 78
Shavian 肅伯納速記符號 48
Sinhala 僧伽羅文 80
Syloti Nagri Syloti Nagri字母表(印度) 44
Syriac 敘利亞文 77
Tagalog 塔加路文(菲律賓) 20
Tagbanwa Tagbanwa文(菲律賓) 18
Tai Le 德宏傣文 35
Tamil 泰米爾文 71
Telugu 泰盧固文(印度) 80
Thaana 馬爾代夫書寫體 50
Thai 泰國文 86
Tibetan 藏文 195
Tifinagh 提非納字母表 55
Ugaritic 烏加里特楔形文字 31
Yi 彝文 1220

其中,有兩個Script值有着特殊的含義:

  • Common:Script屬性爲Common的字符可能在多個文字系統中使用,不是某個文字系統特有的。例如:空格、數字等。
  • Inherited:Script屬性爲Inherited的字符會繼承前一個字符的Script屬性。主要是一些組合用符號,例如:在“組合附加符號”區(0x300-0x36f),字符的Script屬性都是Inherited。

UCD中的Script.txt列出了每個字符的Script屬性。使用UniToy可以按照Script屬性查看字符。例如:

左側Script窗口中,第一層節點是按英文字母順序排列的Script屬性。第二層節點是包含該Script文字的行(row),點擊後顯示該行內屬於這個Script的字符。這樣,就可以集中查看屬於同一文字系統的字符。

4.1.4 Unicode中的漢字

前面提過,在Unicode已定義的99089個字符中,有71226個字符是漢字。它們的分佈如下:

Block名稱 開始碼位 結束碼位 數量
中日韓統一表意文字擴充A 3400 4db5 6582
中日韓統一表意文字 4e00 9fbb 20924
中日韓兼容表意文字 f900 fa2d 302
中日韓兼容表意文字 fa30 fa6a 59
中日韓兼容表意文字 fa70 fad9 106
中日韓統一表意文字擴充B 20000 2a6d6 42711
中日韓兼容表意文字補充 2f800 2fa1d 542

UCD的Unihan.txt中的部首偏旁索引(kRSUnicode)可以檢索全部71226個漢字。kRSUnicode的部首是按照康熙字典定義的,共214個部首。簡體字按照簡體部首對應的繁體部首檢索。UniToy整理了康熙字典部首對應的簡體部首,提供了按照部首檢索漢字的功能:

4.2 UTF編碼

在字符編碼的四個層次中,第一層的範圍和第二層的編碼在4.1節已經詳細討論過了。本節討論第三層的UTF編碼和第四層的字節序,主要談談第三層的UTF編碼,即怎樣將Unicode定義的編碼轉換成程序數據。

4.2.1 UTF-8

UTF-8以字節爲單位對Unicode進行編碼。從Unicode到UTF-8的編碼方式如下:

Unicode編碼(16進制) UTF-8 字節流(二進制)
000000 - 00007F 0xxxxxxx
000080 - 0007FF 110xxxxx 10xxxxxx
000800 - 00FFFF 1110xxxx 10xxxxxx 10xxxxxx
010000 - 10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

UTF-8的特點是對不同範圍的字符使用不同長度的編碼。對於0x00-0x7F之間的字符,UTF-8編碼與ASCII編碼完全相同。UTF-8編碼的最大長度是4個字節。從上表可以看出,4字節模板有21個x,即可以容納21位二進制數字。Unicode的最大碼位0x10FFFF也只有21位。

例1:“漢”字的Unicode編碼是0x6C49。0x6C49在0x0800-0xFFFF之間,使用用3字節模板了:1110xxxx10xxxxxx10xxxxxx。將0x6C49寫成二進制是:0110 1100 0100 1001, 用這個比特流依次代替模板中的x,得到:111001101011000110001001,即E6 B1 89。

例2:“”字的Unicode編碼是0x20C30。0x20C30在0x010000-0x10FFFF之間,使用用4字節模板了:11110xxx10xxxxxx10xxxxxx10xxxxxx。將0x20C30寫成21位二進制數字(不足21位就在前面補0):0 0010 0000 1100 0011 0000,用這個比特流依次代替模板中的x,得到:11110000101000001011000010110000,即F0 A0 B0 B0。

4.2.2 UTF-16

UniToy有個“輸出編碼”功能,可以輸出當前選擇的文本編碼。因爲UniToy內部採用UTF-16編碼,所以輸出的編碼就是文本的UTF-16編碼。例如:如果我們輸出“漢”字的UTF-16編碼,可以看到0x6C49,這與“漢”字的Unicode編碼是一致的。如果我們輸出“”字的UTF-16編碼,可以看到0xD843, 0xDC30。“”字的Unicode編碼是0x20C30,它的UTF-16編碼是怎樣得到的呢?

4.2.2.1 編碼規則

UTF-16編碼以16位無符號整數爲單位。我們把Unicode編碼記作U。編碼規則如下:

  • 如果U<0x10000,U的UTF-16編碼就是U對應的16位無符號整數(爲書寫簡便,下文將16位無符號整數記作WORD)。
  • 如果U≥0x10000,我們先計算U'=U-0x10000,然後將U'寫成二進制形式:yyyy yyyy yyxx xxxx xxxx,U的UTF-16編碼(二進制)就是:110110yyyyyyyyyy110111xxxxxxxxxx。

爲什麼U'可以被寫成20個二進制位?Unicode的最大碼位是0x10ffff,減去0x10000後,U'的最大值是0xfffff,所以肯定可以用20個二進制位表示。例如:“”字的Unicode編碼是0x20C30,減去0x10000後,得到0x10C30,寫成二進制是:0001 0000 1100 0011 0000。用前10位依次替代模板中的y,用後10位依次替代模板中的x,就得到:11011000010000111101110000110000,即0xD843 0xDC30。

4.2.2.2 代理區(Surrogate)

按照上述規則,Unicode編碼0x10000-0x10FFFF的UTF-16編碼有兩個WORD,第一個WORD的高6位是110110,第二個WORD的高6位是110111。可見,第一個WORD的取值範圍(二進制)是11011000 00000000到11011011 11111111,即0xD800-0xDBFF。第二個WORD的取值範圍(二進制)是11011100 00000000到11011111 11111111,即0xDC00-0xDFFF。

爲了將一個WORD的UTF-16編碼與兩個WORD的UTF-16編碼區分開來,Unicode編碼的設計者將0xD800-0xDFFF保留下來,並稱爲代理區(Surrogate):

D800 DB7F High Surrogates 高位替代
DB80 DBFF High Private Use Surrogates 高位專用替代
DC00 DFFF Low Surrogates 低位替代

高位替代就是指這個範圍的碼位是兩個WORD的UTF-16編碼的第一個WORD。低位替代就是指這個範圍的碼位是兩個WORD的UTF-16編碼的第二個WORD。那麼,高位專用替代是什麼意思?我們來解答這個問題,順便看看怎麼由UTF-16編碼推導Unicode編碼。

解:如果一個字符的UTF-16編碼的第一個WORD在0xDB80到0xDBFF之間,那麼它的Unicode編碼在什麼範圍內?我們知道第二個WORD的取值範圍是0xDC00-0xDFFF,所以這個字符的UTF-16編碼範圍應該是0xDB80 0xDC00到0xDBFF 0xDFFF。我們將這個範圍寫成二進制:

110110111000000011011100 00000000 -11011011111111111101111111111111

按照編碼的相反步驟,取出高低WORD的後10位,並拼在一起,得到

1110 0000 0000 0000 0000 - 1111 1111 1111 1111 1111

即0xe0000-0xfffff,按照編碼的相反步驟再加上0x10000,得到0xf0000-0x10ffff。這就是UTF-16編碼的第一個WORD在0xdb80到0xdbff之間的Unicode編碼範圍,即平面15和平面16。因爲Unicode標準將平面15和平面16都作爲專用區,所以0xDB80到0xDBFF之間的保留碼位被稱作高位專用替代。

4.2.3 UTF-32

UTF-32編碼以32位無符號整數爲單位。Unicode的UTF-32編碼就是其對應的32位無符號整數。

4.2.4 字節序

根據字節序的不同,UTF-16可以被實現爲UTF-16LE或UTF-16BE,UTF-32可以被實現爲UTF-32LE或UTF-32BE。例如:

字符 Unicode編碼 UTF-16LE UTF-16BE UTF32-LE UTF32-BE
0x6C49 49 6C 6C 49 49 6C 00 00 00 00 6C 49
0x20C30 43 D8 30 DC D8 43 DC 30 30 0C 02 00 00 02 0C 30

那麼,怎麼判斷字節流的字節序呢?

 

Unicode標準建議用BOM(Byte Order Mark)來區分字節序,即在傳輸字節流前,先傳輸被作爲BOM的字符"零寬無中斷空格"。這個字符的編碼是FEFF,而反過來的FFFE(UTF-16)和FFFE0000(UTF-32)在Unicode中都是未定義的碼位,不應該出現在實際傳輸中。下表是各種UTF編碼的BOM:

UTF編碼 Byte Order Mark
UTF-8 EF BB BF
UTF-16LE FF FE
UTF-16BE FE FF
UTF-32LE FF FE 00 00
UTF-32BE 00 00 FE FF
發佈了34 篇原創文章 · 獲贊 7 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章