關鍵字: 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 服務器讀到這裡的時候,就會停止解析,
然後用讀到的這個編碼方式重新解析頁面。
那麼,作為 網頁瀏覽器來說,如果沒有在 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 編碼。 這個編碼已經被各種 網頁瀏覽器支援了很多年了。
(完)