談談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)。
我還找了兩篇看上去不錯的資料,不過因爲我開始的疑問都找到了答案,所以就沒有看:
- "Understanding Unicode A general introduction to the Unicode Standard" (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&item_id=IWS-Chapter04a)
- "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的原文根本沒提到這一點。
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進制數據,可以看到:
我們看到:文件“例子GBK.txt”有10個字節,依次是“D7 D6 B7 FB BA CD B1 E0 C2 EB”,這就是記事本從文件中讀到的內容。記事本是用來打開文本文件的,所以它會調用Windows的文本顯示函數將讀到的數據作爲文本顯示。Windows首先將文本數據轉換到它內部使用的編碼格式:Unicode,然後按照文本的Unicode去字體文件中查找字體圖像,最後將圖像顯示到窗口上。總結一下前面的分析,文字的顯示應該是這樣的:
- 步驟1:文字首先以某種編碼保存在文件中。
- 步驟2:Windows將文件中的文字編碼映射到Unicode。
- 步驟3:Windows按照Unicode在字體文件中查找字體圖像,畫到窗口上。
如果上述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文件時,可以通過輸入"字"來插入字符“字”。不過在設計字符編碼時,我們還是習慣用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 |