每一個軟件開發人員絕對必須掌握的關於 Unicode 和字符集的最基礎的知識 (轉)

每一個軟件開發人員絕對必須掌握的關於 Unicode 和字符集的最基礎的知識

關鍵字:     Unicode, Character Set, 字符集, UTF-8, ANSI, ASCII, UTF-7
原文標題:    The Absolute Minimum Every Software Developer Absolutely, Positively Must Know
             About Unicode and Character Sets(No Excuses!)
原文鏈接:    http://www.joelonsoftware.com/printerFriendly/articles/Unicode.html
作者:       Joel Spolsky
翻譯、摘要:  木野狐(ChenRong2003[at]hotmail.com)
日期:       2004-11-29 

 

ASCII 碼
------------------------------------------------------------------------------------
 7 位(00~7F)。 32 ~ 127 表示字符。32 是空格, 32 以下是控制字符(不可見)。
第8位沒有被使用。全世界很多人同時對這個位的含義發展了不同的用處。比如 IBM PC 中的 OEM 字符集。
最後就 128 位以下的用處達成共識,制定了 ASCII 標準。
而 128 位以上的可能有不同的解釋,這些不同的解釋就叫做 code pages.
甚至有用於在同一臺電腦上解釋多種語言的 code page.

同時,在亞洲發生了更加瘋狂的事情。亞洲語言的字符集通常數以千計, 8 位已經不足以表達,這通常用一種
很凌亂的,叫做 DBCS(雙字節字符集,double byte character set) 的系統來解決。
這種系統中,有些字符佔用 1 字節,有些 2 字節。這樣一來,在字符串中向前解析很容易,而倒退卻很麻煩。
程序員們被建議,不要使用 s++ 或 s-- 來前進和後退,而使用一些函數,比如 Windows 的 AnsiNext 和
AnsiPrev. 因爲這些函數知道是怎麼回事。

這些不同的假設(code page)在單個的機器上沒有問題。而隨着 Internet 的發展,字符串要從一個機器上移到
另一個機器上,這就產生了問題。於是, Unicode 出現了。

Unicode
---------------------------------------------------------------------------------------
Unicode 是一個勇敢的成就。它把在這個星球上的每一個合理的文字系統整合成了一個單一的字符集。
很多人還存在這樣的誤解: Unicode 僅僅是 16 位的這麼簡單,每個字符佔 16 位,所以一共有 65536 個可能的字符。
然而,這是錯誤的。不過不要緊,因爲這是大部分人都會犯的一個普遍的錯誤。

實際上,Unicode 理解字符的方式是截然不同的,而這是我們必須瞭解的。
到目前爲止,我們都曾經認爲:一個字符對應到一些在磁盤上或內存中儲存的位(bits). 如: A -> 0100 0001
而在 Unicode 中, 一個字符實際上對應一種叫做 code point 的東西。
比如 A 這個字符,是抽象的(原文:platonic,柏拉圖式的,理想的)一個概念。
無論是 Times New Roman 或者 Helvetica 或者其他的什麼字體中,都代表同一個字符。但是它和小寫的字母 a 不同。
但是在其他的語言,比如希伯萊語(Hebrew) 或者德語(German), 阿拉伯語(Arabian) 中,同一個字母的不同的字形代表的含義是否
相同,是有爭議的。經過長時間的爭論,這些也終於被確定了。

每一個字母表中的每一個抽象的字母,都被賦予了一個數字,比如 U+0645. 這個叫做 code point.
U+ 表示: Unicode, 數字是 16 進制的。
你可以通過 charmap 命令來查看所有這些編碼。(Windows 2000/XP 中). 或者訪問 Unicode 的網站(http://www.unicode.org
Unicode 中 code point 的數字的大小是沒有限制的,而且也早就超過了 65535. 所以不是每個字符都能存儲在兩個字節中。
那麼,一個字符串 "Hello", 在 Unicode 中會表示成 5 個 code points :
  U+0048 U+0065 U+006C U+006C U+006F
只不過是一些數字。但我們現在還沒有提到如何在磁盤或者 Email 中表示這些信息,這就是我們下面要提到的編碼(Encoding) 乾的事情。

Encodings (編碼)
-------------------------------------------------------------------------
最初的 Unicode Encoding, 使用兩個字節表示一個字符。那麼 "Hello" 表示爲:
  00 48 00 65 00 6C 00 6C 00 6F
實際上,還有一種表示方式:
  48 00 65 00 6C 00 6C 00 6F 00
到底高位字節在前還是低位字節在前面,是兩種不同的模式。這要看特定的 CPU 在何種模式下工作的更快。 所以這兩種都有。
這就有了兩種不同的 Unicode 表示方式了,爲了區分,人們又採用了一種奇異的方式:
在每一個 Unicode 字符串的前面,加上 FEFF (這稱爲 Unicode 字節順序標誌,Unicode Byte Order Mark).
如果你交換高位和低位次序,那麼會加上一個 FFFE. 這樣,讀這個字符串的人才知道要對每兩個相鄰的字節進行交換。
但在最初的時候,並不是每一個 Unicode 字符串都有這個標誌的。

這看起來很不錯。可程序員們開始抱怨了,“看看那些零!”。因爲有些是美國人,他們使用英語。而英語中很少需要使用 U+00FF 以上的
字符, 有些人無法忍受採用雙倍的存儲空間來存儲每個字符。
基於這些原因,很多人決定忽視 Unicode, 而同時,事情變得更糟了。

然後人們制定了 UTF-8. UTF-8 是用於保存 Unicode code points 的另一套系統。
每一個 U+ 數字,在內存中佔用 8 bit. 在 UTF-8 中,任何一個 0~127 的 code point 佔用一個字節。
只有 128 以及更大的才佔用 2, 3, 直到 6 個字節。
具體如下圖所示:

16進制的最小的數    16進制的最大的數   內存中的字節序列
------------------------------------------------------------------------------
00000000          0000007F         0vvvvvvv
00000080          000007FF         110vvvvv 10vvvvvv
00000800          0000FFFF         1110vvvv 10vvvvvv 10vvvvvv
00010000          001FFFFF         11110vvv 10vvvvvv 10vvvvvv 10vvvvvv
00200000          03FFFFFF         111110vv 10vvvvvv 10vvvvvv 10vvvvvv 10vvvvvv
04000000          7FFFFFFF         1111110v 10vvvvvv 10vvvvvv 10vvvvvv 10vvvvvv 10vvvvvv

這看起來很不錯,其中的英文字符和 ASCII 中一樣。所以美國人根本沒意識到有什麼錯誤。只有世界上的其他國家需要使用高位的字節。
特別的,"Hello" 這個字符串,Unicode code point 爲 U+0048 U+0065 U+006C U+006C U+006F, 會被存儲爲 48 65 6C 6C 6F。
和 ASCII, ANSI, 以及在這個星球上的任何一個 OEM 的字符集中表示的含義都一樣。
現在,如果你需要表示重音的字符,或者希臘語,你需要使用多個字節來表示一個 code point. 但美國人不會介意這些。
(UTF-8 還有一個好處就是,老的字符串處理程序使用一個爲 0 的字節來表示 null-terminator, 不會截斷字符串)

到目前爲止已經介紹了三種 Unicode 的表示方法:

傳統的雙字節表示方法, 稱爲 UCS-2(因爲有 2 個字節) 或者 UTF-16(因爲有 16 個位)
而且你還要搞清楚是高位在前的,還是高位在後的 UCS-2.

還有一種就是新的 UTF-8. 如果你的程序只使用英文的話,它仍然會工作正常。

實際上還有一堆的其他辦法對 Unicode 進行編碼:
有 UTF-7,這種編碼方式大部分和 UTF-8 相同,但保證高位一定爲 0.
所以如果你必須通過某種 Email 系統傳送 Unicode,這些系統認爲 7 位足夠了,那使用 UTF-7 會正常。
還有 UCS-4, 儲存每一個 code point 爲 4 個字節。它的優點是每一個字符都保存爲同樣長的。但很明顯,缺點是浪費太多存儲空間了。

所以,現在你思考問題要把每一個字符想象成抽象的一個 unicode code point. 而它們同樣可以使用任何舊的方式編碼。
舉例來說,你可以把 Unicode 字符串 Hello (U+0048 U+0065 U+006C U+006C U+006F) 編碼(encode)爲
ASCII, 或者古老的 OEM 希臘語編碼,或者希柏萊 ANSI 編碼,等等。而有些字符串不能顯示!
也就是說,假如你要表示一個在某個編碼中沒有對應的 Unicode code point, 通常會顯示爲一個 ? 或者一個白色的小方框。

英文常用的一些編碼有, Windows-1252(Windows 9x 標準 for 西歐語言)
以及 ISO-8859-1, aka Latin-1(對任何西歐語言也有效)
如果用這些編碼來嘗試存儲俄文字符,你會得到一堆的 ?

UTF 7, 8, 16 以及 32 都有一個優點,能夠正確的存儲任何的 code point.

最簡單,也是最重要的幾個概念
====================================================================
一個字符串不指定它使用什麼編碼是沒有意義的。
再也不要假定, “純”文本(plain text) 是 ASCII.
沒有 “純文本” 這個東西。

如果你有一個字符串,在內存中,在文件中,或者在 Email 消息裏,你必須知道它的編碼是什麼。否則你無法正確的解釋或者顯示給用戶。
所有的諸如 “我的網頁不能正常顯示了”,或者 ”Email 消息不能正常顯示了“ 之類的愚蠢問題, 都是因爲, 沒有告訴你到底是使用的那種編碼,
UTF-8 還是 ASCII 還是 ISO 8859-1 或者 Windows 1252 ?? 那麼自然無法正常的解釋和顯示,甚至不知道字符串該在哪裏結束。

那麼如何保留這樣的編碼標誌,來表示字符串的編碼? 有一些基本的辦法。
比如對於 Email 來說,在表單的 header 中加上:

Content-Type:text/plain;charset="UTF-8"

對於 Web 頁面來說,原來的做法是, Web 服務器隨着 web 頁面本身一起,發送一個類似於 Content-Type 的 http header.
(不是在 HTML 裏面,而是作爲一個 response header 在 HTML 頁之前發送)

這樣做有一個問題。如果你的 Web 服務器同時有多個站點,站點由多個不同的人用不同的語言開發的程序混在一起。那麼 Web 服務器將無從得知,
每一個文件是用什麼編碼方式寫的。這樣也就無法發送正確的 Content-Type header.
如果你能夠在每一個 HTML 文件中記錄 Content-Type 信息,那麼就很方便了。可這念頭似乎也很瘋狂,因爲你還沒有知道用什麼編碼方式去
讀取這個文件,又怎麼能讀出編碼信息呢?
幸好,幾乎每一種編碼中,對 32~127 的字符都解釋的相同。所以你可以在每一個 html 文件中這麼寫:

<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">

但是要注意, 這個 meta 標籤必須放在 head 中靠前面的位置才能保證不會出問題。 因爲 Web 服務器讀到這裏的時候,就會停止解析,
然後用讀到的這個編碼方式重新解析頁面。

那麼,作爲 Web 瀏覽器來說,如果沒有在 meta 標籤中或者 http headers 中發現 Content-Type, 會怎麼樣呢?
IE 是這麼做的:
先嚐試去猜,根據特定的字節出現在各種語言的典型的編碼中的頻率。
如果編碼設定不正常,用戶可以通過 View|Encoding 菜單來嘗試不同的編碼方式。(當然,不是每個人都知道該這樣做)

在 VB, COM, Windows NT/2000/XP 中,默認的字符串類型是 UCS-2(2字節)的。
在 C++ 代碼中, 我們可以定義字符串爲 wchar_t(wide char),同時用 wcs 系列的函數代替 str 系列的函數。
如 wcscat, wcslen, 而不是 strcat, strlen.
在 C 代碼中,要創建 UCS-2 字符串的話,只要在前面加一個 "L", 如 L"Hello"

對於 Web 頁面,最好統一爲使用 UTF-8 編碼。 這個編碼已經被各種 web 瀏覽器支持了很多年了。

 
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章