前幾天看文初的《精武門之Web安全研討會首日感受 》,說到利用字符集攻擊時提到以前寶寶寫的一篇有關國際化的文章,趁機再次拜讀了寶寶的這篇大作,不得不感慨寶寶的寫作功底,無敵!這麼好的文章不分享出來實在是太可惜了,在此將寶寶的大作轉帖於此;
作者序
在我開發 Java 程序的幾年中,遇到得最多,也是別人向我提問最多的問題,就是各種各樣看似稀奇古怪的中文亂碼問題了。網上也有許多解釋和解決 Java 中文問題的文章,但水平參差不齊,有一些文章甚至是錯誤的。
此外,我們公司自己的 Java 程序從一開始就採用了錯誤的方式處理中文問題,雖能解一時之急,卻引出了越來越多的深遠的問題。每當我聽到有的同事還在討論如何特殊處理雙字節的中文 GB 碼,就感慨他們思路的狹隘。試問,今天我們可以用特殊的方式處理我們所熟悉的中文編碼,可是今後我們怎樣才能應付日文版、韓文版、或世界其它國家語言的產品開發呢?
在我看來,與其說這些問題是 “ 中文化問題 ” ,不如說是 “ 國際化問題 ” 。所謂的 “ 漢化 ” 這 種說法已經隨時代遠去了。想想看,這個詞帶有明顯的小農經濟的色彩:自家漢化自家用,哪管世界變化多。經過漢化的軟件,常常意味着:版本落後、不兼容、不 穩定。爲什麼會這樣呢?根本原因是,從軟件的設計階段,就沒有考慮國際用戶的需要,沒有采用國際通用的標準。事後要彌補自然難上加難。
所以讓我們把眼光放開,想一想 “ 國際化 ” 。當然國際化的目的還是生產出 “ 漢化 ” 的軟件,但我們可以用同樣的方法 “ 韓化 ” 、 “ 日化 ” 、 “ 阿拉伯化 ” ,統稱爲 “ 本地化 ” —— 這就是 “ 國際化 ” 的目的。國際化和本地化有兩個很體面的英文縮寫: I18n ( Internationalization )和 L10n ( Localization )。
想要開發出國際化的軟件產品,首先要了解國際標準,而不是使用東拼西湊的權宜之計。本文首先從相關國際標準的討論切入,相信正確地理解和應用這些標準,所有的 “ 中文化問題 ” 或 “ 國際化問題 ” 都會迎刃而解。
字符編碼簡介
ASCII 碼
從學計算機的那天開始,老師就告訴我們在計算機裏面,所有的英文字母都對應到一個數字編碼,這就是 ASCII 碼( American Standard Code for Information Interchange )。 ASCII 碼是很久很久以前( 1968 年)制定的。它只使用了一個 8 位字節中的低 7 位,總共是 127 個編碼位。這樣的方案很快就不夠使用了。
單字節編碼的發展
在 80 年代早期,一些現在流行的標準(如 ISO 8859 和 Unicode )還未出現。那時爲了支持多種地區的語言,各大組織機構或 IT 廠商開始發明它們自己的編碼方案,以便彌補 ASCII 編碼的不足。一時間,各種互不相容的字符編碼方案成百花齊放之勢。
爲了避免混亂, ISO 組織在 1998 年之後,陸續發表了一系列代號爲 8859 的標準,作爲 ASCII 編碼的標準擴展,終於統一了單字節的西方字符的編碼。 ISO 是設在瑞士的國際標準化組織的簡稱( International Organization for Standardization )。
ISO-8859-1 ( Latin1 - 西歐字符)
ISO-8859-1 覆蓋了大多數西歐語言,包括:法國、西班牙、葡萄牙、意大利、荷蘭、德國、丹麥、瑞典、挪威、芬蘭、冰島、愛爾蘭、蘇格蘭、英格蘭等,因而也涉及到了整個美洲大陸、澳大利亞和非洲很多國家的語言。
此外, ISO-8859-1 後來被採納爲 ISO-10646 標準(後面會講到)的首頁,換句話說, Unicode 的最開頭 256 個字符編碼和 ISO-8859-1 是一一對應的。正是由於這個特殊性,使很多人產生了對 ISO-8859-1 編碼的誤用。
ISO-8859 標準還包括:
- ISO-8859-2 ( Latin2 - 中、東歐字符)
- ISO-8859-3 ( Latin3 - 南歐字符)
- ISO-8859-4 ( Latin4 - 北歐字符)
- ISO-8859-5 ( Cyrillic - 斯拉夫語)
- ISO-8859-6 ( Arabic - 阿拉伯語)
- ISO-8859-7 ( Greek - 希臘語)
- ISO-8859-8 ( Hebrew - 希伯來語)
- ISO-8859-9 ( Latin5 )
- ISO-8859-10 ( Latin6 )
- ISO-8859-11 ( Thai - 泰國語)
- ISO-8859-12 (保留)
- ISO-8859-13 ( Latin7 )
- ISO-8859-14 ( Latin8 )
- ISO-8859-15 ( Latin9 )
但是 ISO 8859 系列標準的字符編碼,還是互不相容,不可能同時使用的。畢竟它們只是單字節的編碼方案。而且,它們和多字節的編碼方案如中文編碼 GB2312 和 BIG5 也是不相容的。那些歐洲字符(最高位爲 1 的字符),在 GB2312 和 BIG5 中被認爲是雙字節漢字編碼的首字節。
多字節編碼的發展
單字節編碼只有 256 個碼位( 28 =256 ),而中文字符何止千千萬,單字節編碼不可能滿足中文編碼的需要。於是爲了適應東方文字信息處理的需要, ISO 又制定了 ISO 2022 標準( Character code structure and extension techniques ),提供了七位與八位編碼字符集的擴充方法的標準。我國根據 ISO 2022 制定了國家標準 GB2311 —— 《信息交換用七位編碼字符集的擴充方法》,並根據該標準制定了國家標準 GB2312-80 編碼。其他東方國家和地區也制定了各自的字符編碼標準,如日本的 JIS0208 ,韓國的 KSC5601 ,臺灣地區的 CNS11643 等。
BIG5
BIG5 是從 CNS11643 的早期版本發展而來的,雖然沒有包括 CNS11643 的全部內容,但卻是目前臺灣、香港地區普遍使用的一種繁體漢字的市場標準,包括 440 個符號,一級漢字 5401 個、二級漢字 7652 個,共計 13060 個漢字。
GB2312-80
全稱是《信息交換用漢字編碼字符集 基本集》, 1980 年發佈,是中文信息處理的國家標準,在大陸及海外使用簡體中文的地區(如新加坡等)是強制使用的唯一中文編碼。
· 雙字節編碼
· A1-A9 :符號區,包含 682 個符號
· B0-F7 :漢字區,包含 6763 個漢字
GB2312 碼共收錄 6763 個簡體漢字、 682 個符號,其中漢字部分:一級字 3755 ,以拼音排序,二級字 3008 ,以偏旁排序。該標準的制定和應用爲規範、推動中文信息化進程起了很大作用。
GBK
漢字內碼擴展規範( GBK )是國家技術監督局 1995 年爲中文 Windows 95 所制定的新的漢字內碼規範。
· 雙字節編碼, GB2312-80 的擴充,在碼位上和 GB2312-80 兼容。
· 範圍: 8140 ~ FEFE (剔除 xx7F )共 23940 個碼位。
· 包含 21003 個漢字,包含了 ISO 10646 中的全部中日韓漢字,簡、繁體字融於一庫。
嚴格說, GBK 不能算是國家標準,最多算是一個商業標準。而 GB18030 纔是真正的國家標準。
GB18030-2000
全稱是《信息交換用漢字編碼字符集》,是我國的強制標準,所有不支持 GB18030 標準的軟件將不能作爲產品出售。
· 單字節、雙字節、四字節編碼。
· 向下與 GB2312 編碼兼容。
· 支持 GB 13000.1-1993 中的全部中、日、韓( CJK )統一漢字字符和全部 CJK 統一漢字擴展 A 的字符。
雖然 GB18030 標準非常強大,但它是一箇中國大陸的標準。在編碼上,除了和 GB2312 以外,還是不能和世界上其它任何一種字符編碼統一。
終極標準 —— Unicode 和 ISO 10646
前面所講的一切字符編碼方案,都是針對局部地區或少數語言文字的,沒有辦法同時表達所有的語言文字,或在多種語言平臺上交換。這對今天極其頻繁的國際信息交流是不相稱的。
爲了提高計算機的信息處理和交換功能,使得世界各國的文字都能在計算機中處理,從 1984 年起, ISO 組織就開始研究制定一個全新的標準:通用多八位編碼字符集( Universal Multiple-Octet Coded Character Set ),簡稱 UCS 。標準的編號爲: ISO 10646 。這一標準爲世界各種主要語言的字符 ( 包括簡體及繁體的中文字 ) 及附加符號,編制統一的內碼。
統一碼( Unicode )是 Universal Code 的縮寫,是由另一個叫 “Unicode 學術學會 ” ( The Unicode Consortium )的機構制定的字符編碼系統。 Unicode 與 ISO 10646 國際編碼標準從內容上來說是同步一致的。
Unicode 是 Java 語言和 XML 的基礎,所以我們要稍微詳細地介紹一下 Unicode 以及 ISO 10646 標準。
注意: 不夠耐心的讀者可以跳過本章的餘下部分。但顯然瞭解本章所描述的 Unicode 及相關編碼的技術細節,有利於你更好地理解和應用 Unicode 。
Unicode 和 ISO 10646 的關係
在 1991 年, Unicode 學術學會與 ISO 國際標準化組織決定共同制訂一套適用於多種語言文本的通用編碼標準。 Unicode 與 ISO 10646 國際編碼標準於 1992 年 1 月正式合作發展一套通用編碼標準。自此,兩個組織便一直緊密合作,同步發展 Unicode 及 ISO 10646 國際編碼標準。
ISO 10646 ( UCS ) |
Unicode |
1993 年, ISO 組織發表 ISO 10646 國際編碼標準的第一個版本,全名是 ISO/IEC 10646-1:1993 。它收錄了 20902 個表意字符( ideograph ,中日韓文均屬表意字符)。 |
同年, Unicode 學術學會根據 ISO/IEC 10646-1:1993 修訂了 Unicode 1.0 ,發佈 Unicode 1.1 。 |
不斷改善和修訂 ISO 10646 標準。 |
1996 年發表 Unicode 2.0 , 1998 年發表 Unicode 2.1 ,根據 ISO 10646 做了一些改善和修訂,新增了歐元符號。 |
2000 年 10 月發表了 ISO 10646 第二版的第一部分: ISO/IEC 10646-1:2000 ,新增收了 6,582 個表意字符於擴展區 A 中( CJK Unified Ideographs Extension A )。 |
2000 年 2 月,發表 Unicode 3.0 ,也包含了同樣的 CJK Ext A 。 |
2001 年,發表了 ISO/IEC 10646 的第二部分,增收了 42711 個表意字符於擴展區 B 裏。 |
2001 年, Unicode 發表 3.1 版,將 CJK Ext B 納入新版 Unicode 中。 |
雖然兩個組織保持如此密切的合作關係,但 Unicode 和 ISO 10646 還是有區別的。 ISO 10646 着重定義字符編碼,而 Unicode 則在此基礎上,爲這些字符及編碼數據提出應用的方法以及對語義數據作補充。
UCS 的結構
UCS 的結構是一個四維的編碼空間,每一維由一個字節(八位二進制位)組成,範圍是 00 到 FF 。總體上分爲 128 個羣組 (Group 00-7F) ,每一羣組由 256 個平面 (Plane 00-FF) 組成,每一平面有 256 行 (Row 00-FF) ,每一行 256 個編碼位 (Cell 00-FF) 。所以,每一平面包括 65,536 個字符位 (Character Position 0000-FFFF) 。
整個編碼字符集的每個字符都由 4 個字節,按 “ 組 - 面 - 行 - 列 ” 的順序表示。所以 UCS 的可編碼空間爲: 128 × 256 × 256 × 256 = 231 。
UCS 將其第一個平面 (00 羣組中的 00 平面 ) 稱作基本多語種平面( Basic Multilingual Plane , BMP )。
在 UCS 中,目前只有 00 組是重要的, Unicode 學術學會斷言,在可以預見的將來,甚至不可能用完 00 組中的前 17 個平面( 00 平面到 10 平面)。因此, Unicode 只定義了 ISO 10646 的第 00 組的前 17 個平面。事實上,目前絕大多數字符,都分配在第 00 平面 BMP 中。
下表中列出了 BMP 中的字符分配情況:
區間 |
描述 |
( 0000-1FFF )基本拼音字符區 |
包括所有拼讀文字的字母拼音和音標。它的字符集一般較小,如:拉丁文、西里爾文、希臘文、希伯來文、阿拉伯文、泰文、天成文書(梵文)等。 |
( 2000-28FF )符號區 |
包括許多種用於標點、數學、化學、科技及其它特殊用途上的 “ 符號 ” 和 “ 丁貝符 ” (示意圖形符號)。 |
( 2E80-33FF )中日韓語音及符號區 |
包括用於中國、日本、韓國語言中的標點、符號、字根(筆畫)及發音等字符。 |
( 3400-9FA5 )中日韓漢字字符區 |
由 27,484 箇中日韓(越)的統一漢字組成。 |
( A000-A4C6 )彝族字符區 |
由 1,165 箇中國南方彝族音節和 50 個其字根組成。 |
( AC00-D7A3 )韓字符拼音區 |
由 11,172 個預先組合的韓字符拼音音節組成。 |
( D800-DFFF )代理區 |
這個區被平分爲 1024 個 “ 高半代理區 ” ( D800-DBFF )碼位和 1024 個 “ 低半代理區 ” ( DC00-DFFF )碼位,用來形成代理對,可以得到超過一百萬個擴充編碼位。 |
( E000-F8FF )私人專用區 |
包含 6,400 個編碼位,用於用戶或開發商自行定義的字符編碼。 |
( F900-FA2D )兼容字符區 |
包括一些被許多行業協會和國家標準廣泛使用的字符,但在 Unicode 編碼中有不同的表現形式。包含一些專用字符。 |
UCS 的表現形式
UCS 有兩種方式來表示一個字符編碼:四字節正規形式( UCS-4 , Four-octet canonical form )和雙字節基本平面形式( UCS-2 , Two-octet BMP form )。
UCS-4 —— 四字節正規形式
UCS-4 用 4 個字節來表示一個字符。第一個字節表示組( Group ),第二表示平面( Plane ),第三表示行( Row ),第四表示單元號或列( Cell )。
UCS-2 —— 雙字節基本平面形式
當系統只使用 BMP 的字符碼時,可以省略羣組和平面中的八位,將字符碼由 32 個位縮短爲 16 個位( 2 個字節)。標記爲 UCS-2 。
Unicode 和 UCS-2 同樣採用 16 位編碼。所以一般 可以把 Unicode 和 UCS-2 看作是同一樣東西 。
代理對( Surrogate Pair )
UCS-4 定義了 4 個字節表示一個字符,用來應付將來的擴展是綽綽有餘。可是 Unicode 和 UCS-2 只定義了 2 個字節,卻很容易用盡。代理對( Surrogate Pair )的設計在這種背景下應運而生。
UCS-2 在 BMP 中開闢了一個特殊的區間( D800 - DFFF ) -- 代理區,並平分成兩個區,分別稱爲高半代理區( High-half Zone , D800 - DBFF ),和低半代理區( Low-half Zone , DC00 - DFFF ),各有 1024 個碼位。使用時,從高低兩個代理區中各取一個編碼組成一個四字節的代理,來表示一個在 BMP 以外平面上的編碼字符位。這樣一來,總共可以多表示 1024×1024 個字符,映射到 00 羣組中的 01 到 10 平面(共 16 個平面)。
代理對提供了用 BMP 的 2 字節編碼來表示在基本多文種平面( BMP )之外的 16 個平面編碼的機制。一些不常用的字符可以用代理對錶示。目前,只有 ISO/IEC 10646-2:2001 和 Unicode 3.1 才使用到代理對。
高半代理區和低半代理區的劃分,使編碼位相互區分開。非代理區字符一定不會在這個區裏。因爲高半代理區和低半代理區不相交,所以很容易決定字符值的邊界。一個完好的文本中,高半代理碼和低半代理碼總是按先後成對出現。
如果在實現上沒有刪除代理碼或在代理碼對中插入字符,數據的完整性就可得到保證。即使數據有殘損,也只是局部的。一個殘缺的碼隻影響一個字符。因爲高半代理區和低半代理區不相交,且成對出現,錯碼不會傳到文本的其它部分。
具體來說,一個代理對( H , L )由碼值爲 D800-DBFF 的高半代理碼 H 和碼值爲 DC00-DFFF 低半代理碼 L 組成。將一個字符映射到 UCS-4 碼位中。假設 N 是 UCS-4 碼值,則有:(以下所有數字均爲 16 進制)
N = (H - D800) × 400 + (L - DC00) + 10000
於是得到 N 的碼值爲 10000 到 10FFFF 。
注意
Unicode 3.0 沒有用到代理對,直到 3.1 才增加了 CJK Ext B ,用到了 02 平面,需要使用代理對才能訪問。但 99.99% 的情況下,根本用不到那些字。此外, JDK1.4 只支持到 Unicode 3.0 ,所以目前 Java 還不能應用代理對。
UTF 編碼
UTF 爲 UCS Transformation Format 的縮寫,意爲 “UCS 轉換格式 ” 。 UCS 只是一個字形和內碼上的標準,並沒有定義實際在計算機上存取的方法,而 UTF 便定義了一整套的計算機存取 UCS 編碼的轉換格式,並考慮了與其它編碼方式兼容。常用的格式有 UTF-8 和 UTF-16 。有時也用到 UTF-7 來進行 7 位數據傳輸。
UTF-16
UTF-16 是用定長 16 位( 2 字節)來表示的 UCS-2 或 Unicode 轉換格式。它將 Unicode 的編碼值變成 2 字節的 Big-endian (高位字節在前,低位字節在後)或 Little-endian (低位字節在前,高位字節在後)編碼。 UTF-16 利用代理對來訪問 BMP 之外的字符編碼。
Java 使用 Big-endian 系統,而 Intel 系列處理器內部使用 Little-endian 系統(學彙編語言和 C 語言的人都知道)。
例如: “ 中國 ” 兩字, Unicode 是 4E2D 56FD ,在 Windows 上用 UTF-16 編碼,結果爲四個字節: 2D 4E FD 56 ;如果使用 Java 輸出,結果爲: 4E 2D 56 FD 。
使用 UTF-16 有什麼缺點呢?很顯然,
1. 所有原本 1 個字節就可以表示的西方字符,現在要用 2 個字節來表示,體積大了一倍。
2. 學過 C 的人都知道, 0x00 代表 C 字符串的結尾。但是用 UTF-16 來表示單字節字符( ISO-8859-1 )時,高位字節爲 0x00 。這樣就會使 C 語言庫函數發生誤判。用 UTF-16 表示文件名、網址等,全引出無數的問題。
3. 字符的邊界不好找。程序處理時必須從字符串的頭部開始掃描,纔可能正確地找出一個字符的邊界,效率較低。此外,萬一壞掉一個字節,這個字節之後的字符都會錯位,壞掉一片。
所有的這些問題,在 UTF-8 中都不存在。
但是, UTF-16 也有其天然的優點:它直接表現了字符編碼的整數值。所以 UTF-16 是最直接的 Unicode 表示法。此外,它是定長的,這大大簡化了字符串的操作。 Java 語言就是用 UTF-16 格式將字符存儲在內存中的。正是這樣,才使 Java 的 Unicode 字符串的操作格外簡單高效。
UTF-8
UTF-8 使用了變長技術,在每一個編碼區域有不同的字碼長度:
1. 對 UCS-2 ,由 1 字節至 3 字節構成;
2. 如果 UCS-2 使用了代理對,則 UTF-8 最長可到 4 字節;
3. 對 UCS-4 ,由 1 字節至 6 字節構成。
因爲以字節( 8 位)爲組成單元,故稱爲 “UTF-8 ” 。對於英文文本, UTF-8 的文件大小比其它轉換格式都小。
在 UTF-8 內,字符由 1 個至 6 個字節爲組合。下表列舉出了不同範圍的 UCS 碼轉換成 UTF-8 的規則。英文字母 “x” 代表可以用來記錄 Unicode 碼值的區域。
UCS-4 區域(十六進制) |
UTF-8 字節組合(二進制) |
0000 0000 —— 0000 007F |
0xxxxxxx |
0000 0080 —— 0000 07FF |
110xxxxx 10xxxxxx |
0000 0800 —— 0000 FFFF |
1110xxxx 10xxxxxx 10xxxxxx |
0001 0000 —— 001F FFFF |
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
0020 0000 —— 03FF FFFF |
111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
0400 0000 —— 7FFF FFFF |
1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
在 UTF-8 內,
1. 如果一個字節,最高位(第 8 位)爲 0 ,表示這是一個 ASCII 字符( 00 - 7F )。可見,所有 ASCII 編碼已經是 UTF-8 了。
2. 如果一個字節,以 11 開頭,連續的 1 的個數暗示這個字符的字節數,例如: 110xxxxx 代表它是雙字節 UTF-8 字符的首字節。
3. 如果一個字節,以 10 開始,表示它不是首字節,需要向前查找才能得到當前字符的首字節。
可見 UTF-8 可以有效地保證數據的完整性,避免出現編碼的錯位。即使偶然出現 “ 壞字 ” ,也不會影響到後續的文本。
那麼 UTF-8 有什麼缺點呢?顯然,對於在 BMP 中的中文字來說,需要用 3 個字節才能表示,比使用 UTF-16 或直接使用雙字節的 GB2312 編碼大了 0.5 倍。
上文說了一大通,總結一下,其實很簡單:
- 字符編碼是抽象字符在計算機中的數字表示。
- 字符編碼集( character set ,簡稱字符集)是一批字符編碼的集合。世界上存在大量互不兼容的字符集,給國際交流帶來了困難。
- ASCII 碼是最古老的字符編碼,它總共只定義了 7 位共 128 個字母、數字和符號。但它是其它所有字符編碼的基礎。
- Unicode 用 16 位整數編碼,將世界上所有主要文字的字符統一起來了。如果利用代理對( surrogate pair )最多可以表示從 0 到 1FFFF 的字符。然而絕大多數情況下,只需要用到 0 到 FFFF 之間的字符就足夠了。
- Unicode 常用 UTF-8 和 UTF-16 來表示。 7 位的 ASCII 碼不用作任何變化,就已經是 UTF-8 了。但 UTF-8 需要用 3 個字節來表示一個漢字。
- ISO 8859 系列字符集,定義了單字節字符編碼的標準。其中最特殊的是 ISO-8859-1 編碼,它的編碼和 Unicode 中最開始的 256 個字符編碼完全相同。
- GB18030 編碼是中國大陸的國家標準,在字彙上等同於 Unicode ,在編碼上和 GB2312 編碼以及 GBK 編碼兼容。