原文地址:http://www.joelonsoftware.com/articles/Unicode.html
作者:Joel Spolsky
譯文:http://local.joelonsoftware.com/wiki/Talk:Chinese_(Simplified)
你曾經(jīng)是否覺得HTML中的”Content-Type”標(biāo)簽充滿神秘?雖然你知道這個東西必須出現(xiàn)在HTML中,但對于它到底干嗎你可能一無所知。
你是否曾經(jīng)收到過來自你保加利亞朋友的郵件,到處都是”???? ?????? ??? ????”?
我很失望,因?yàn)槲野l(fā)現(xiàn)許多軟件開發(fā)人員到現(xiàn)在為止都還沒有對字符集、編碼、Unicode有一個清晰的認(rèn)識,這是個事實(shí)。幾年前,在測試FogBUGZ項(xiàng)目時,忽然想看看它能不能接收用日文寫的電子郵件。這個世界上會有人用日文寫電子郵件?我不知道。測試結(jié)果很糟糕。我仔細(xì)看了用來解析MIME (Multipurpose Internet Mail Extenisons)格式的郵件所用的ActiveX控件,發(fā)現(xiàn)了它在字符集上面做的蠢事。于是我們不得不重新寫一段代碼,先消除Active控件的錯誤,然后再完成正確的轉(zhuǎn)換。類似的事情在我研究另一個商業(yè)庫的時候同樣發(fā)生了,這個庫關(guān)于字符編碼這部分的實(shí)現(xiàn)簡直糟透了。我找到它的開發(fā)者,把存在問題的包指給他,他卻表示對于此無能為力。像很多程序員一樣,他只希望這個缺陷會被人們遺忘。
事實(shí)并非如他所愿。因?yàn)槲野l(fā)現(xiàn),像PHP這么流行的網(wǎng)頁開發(fā)工具,竟然在實(shí)現(xiàn)上也完全忽略了多種字符編碼的存在(譯者注:這篇文章寫于2003年,現(xiàn)在的 PHP可能已經(jīng)糾正了這個問題吧),盲目地只使用8個比特來表示字符,于是開發(fā)優(yōu)秀的國際化的Web應(yīng)用程序變成了一場夢。我想說,受夠了。
我申明:在2003年,如果你是一個程序員,但你卻對字符、字符集、編碼和Unicode一無所知,那么你別讓我抓到你。如果落在我手里,我會讓你待在潛水艇里剝六個月的洋蔥,我發(fā)誓。
另外,還有一件事:
在這篇文章里,我所講的是每一個工作中的程序員都應(yīng)該知道的知識。所有以為”純文本 = ASCII碼 = 一個字符就是8比特”的人不單單錯了,而且錯得離譜。如果你仍然堅(jiān)持使用這種方式編寫程序,那么你比一個不相信細(xì)菌的存在醫(yī)生好不到哪里去。所以在你讀完這篇文章以前,不要再寫半行代碼。
在我開始之前,必須說明白,如果你已經(jīng)了解了國際化,可能你會覺得這篇文章過于簡單。沒錯,我的的確確是想架一座最短的橋,讓任何人都可以理解發(fā)生了什么事,懂得如何寫出可以在非英文語言環(huán)境是正常工作的代碼。還得指出,字符處理僅僅是軟件國際化中的一小部分,但一口吃不成個胖子,今天我們只看什么是字符集。
可能你以為我要開始談非常古老的字符集如EBCDIC之類的,實(shí)際上我不會。EBCDIC與你的生活無關(guān),我們不需要回到那么遠(yuǎn)。

回到一般遠(yuǎn)就行了。當(dāng)Unix剛出來的時候,K&R寫了《The C Programming Language》一書,那時一切都很簡單。EBCDIC已經(jīng)慚慚不用,因?yàn)樾枰硎镜淖址挥心切┎粠е匾舻挠⑽淖帜福珹SCII完全可以勝任。ASCII使用數(shù)字32到 127來表示所有的英文字母,比如空格是32,字母”A”是65等等。使用7個比特就可以存儲所有這樣字符。那個時代的大多數(shù)計算機(jī)使用8個比特來,所以你不但可以存儲全部的ASCII,而且還有一個比特可以多出來用作其他。如果你想,你可以把它用作你不可告人的目的。32以下的碼字是不可打印的,它們屬于控制字符,像7表示響鈴,12表示打印機(jī)換紙。
所有的一切都看起來那么完美,當(dāng)然前提你生在一個講英文的國家。

因?yàn)橐粋€字節(jié)有8個比特,而現(xiàn)在只用了7個,于是很多人就想到”對呀,我們可以使用128-255的碼字來表示其他東西”。麻煩來了,這么多人同時出現(xiàn)了這樣的想法,而且將之付諸實(shí)踐。于是IBM-PC上多了一個叫OEM字符集的東西,它包括了一些在歐洲語言中用到的重音字符,還有一些畫圖的字符,比如水平線、垂直線等,水平線在右端會帶一個小彎鉤,垂直線會如何等等。使用這些畫圖字符你可以畫出漂亮的框、畫出光滑的線條,在老式的烘干機(jī)上的8088電腦上你依然可以看到這些字符。事實(shí)上,當(dāng)PC在美國之外的地方開始銷售的時候,OEM字符集就完全亂套了,所有的廠商都開始按照自己的方式使用高128個碼字。比如在有些PC上,130表示é,而在另外一些在以色列出售的計算機(jī)上,它可能表示的是希伯來字母ג,所以當(dāng)美國人把包含résumés這樣字符的郵件發(fā)到以色列時,就為變?yōu)閞גsumגs。在大多數(shù)情況下,比如俄語中,高128個碼字可能用作其他更多的用途,那么你如何保證俄語文檔的可靠性呢?
最終ANSI標(biāo)準(zhǔn)結(jié)束了這種混亂。在標(biāo)準(zhǔn)中,對于低128個碼字大家都無異議,差不多就是ASCII了,但對于高128個碼字,根據(jù)你所在地的不同,會有不同的處理方式。我們稱這樣相異的編碼系統(tǒng)為碼頁(code pages)。舉個例子,比如在以色列發(fā)布的DOS中使用的碼頁是862,而在希臘使用的是737。它們的低128個完全相同,但從128往上,就有了很大差別。MS-DOS的國際版有很多這樣的碼頁,涵蓋了從英語到冰島語各種語言,甚至還有一些”多語言”碼頁。但是還得說,如果想讓希伯來語和希臘語在同一臺計算機(jī)上和平共處,基本上沒有可能。除非你自己寫程序,程序中的顯示部分直接使用位圖。因?yàn)橄2畞碚Z對高128個碼字的解釋與希臘語壓根不同。
同時,在亞洲,更瘋狂的事情正在上演。因?yàn)閬喼薜淖帜赶到y(tǒng)中要上千個字母,8個比特?zé)o論如何也是滿足不了的。一般的解決方案就是使用DBCS- “雙字節(jié)字符集”,即有的字母使用一個字節(jié)來表示,有的使用兩個字節(jié)。所以處理字符串時,指針移動到下一個字符比較容易,但移動到上一個字符就變得非常危險了。于是s++或s–不再被鼓勵使用,相應(yīng)的比如Windows下的AnsiNext和AnsiPrev被用來處理這種情況。
可惜,不少人依然堅(jiān)信一個字節(jié)就是一個字符,一個字符就是8個比特。當(dāng)然,如果你從來都沒有試著把一個字符串從一臺計算機(jī)移到另一臺計算機(jī),或者你不用說除英文以外的另一種語言,那么你的堅(jiān)信不會出問題。但是互聯(lián)網(wǎng)出現(xiàn)讓字符串在計算機(jī)間移動變得非常普遍,于是所有的混亂都爆發(fā)了。非常幸運(yùn),Unicode適時而生。
Unicode 是一個勇敢的嘗試,它試圖用一個字符集涵蓋這個星球上的所有書寫系統(tǒng)。一些人誤以為Unicode只是簡單的使用16比特的碼字,也就是說每一個字符對應(yīng) 16比特,總共可以表示65536個字符。這是完全不正確的。不過這是關(guān)于Unicode的最普遍的誤解,如果你也這樣認(rèn)為,不用感到不好意思。
事實(shí)上,Unicode使用一種與之前系統(tǒng)不同的思路來考慮字符,如果你不能理解這種思路,那其他的也就毫無意義了。
到現(xiàn)在為止,我們的做法是把一個字母映射到幾個比特,這些比特可以存儲在磁盤或者內(nèi)存中。
A -> 0100 0001
在Unicode中,一個字母被映射到一個叫做碼點(diǎn)(code point)的東西,這個碼點(diǎn)可以看作一個純粹的邏輯概念。至于碼點(diǎn)(code point)如何在內(nèi)存或磁盤中存儲是另外的一個故事了。
在Unicode中,字母A可看做是一個柏拉圖式的理想,僅存在于天堂之中:(我的理解是字母A就是一個抽象,世界上并不存在這樣的東西,如果數(shù)學(xué)里面的0、1、2等一樣)
這個柏拉圖式的A與B不同,也與a不同,但與A和A相同。這個觀點(diǎn)就是Times New Roman字體中的A與Helvetica字體中的A相同,與小寫的”a”不同,這個應(yīng)該不會引起太多的異議。但在一些語言中,如何辨別一個字母會有很大的爭議。比如在德語中,字母 ß是看做一個完整的字母,還是看做ss的一種花式寫法?如果在一個字母的形狀因?yàn)樗幵谝粋€單詞的末尾而略有改變,那還算是那個字母嗎?阿拉人說當(dāng)然算了,但希伯來人卻不這么認(rèn)為。但無論如何,這些問題已經(jīng)被Unicode委員會的這幫聰明人給解決了,盡管這花了他們十多年的時間,盡管其中涉及多次政治味道很濃的辯論,但至少現(xiàn)在你不用再為這個操心了,因?yàn)樗呀?jīng)被解決。
每一個字母系統(tǒng)中的每一個柏拉圖式的字母在Unicode中都被分配了一個神奇的數(shù)字,比如像U+0639。這個神奇數(shù)字就是前面提到過的碼點(diǎn)(code point)。U+的意思就是”Unicode”,后面跟的數(shù)字是十六進(jìn)制的。U+0639表示的是阿拉伯字母Ain。英文字母A在Unicode中的表示是U+0041。你可以使用Windows 2000/XP自帶的字符表功能或者Unicode的官方網(wǎng)站(www.unicode.org)來查找與字母的對應(yīng)關(guān)系。
事實(shí)上Unicode可以定義的字符數(shù)并沒有上限,而且現(xiàn)在已經(jīng)超過65536了。顯然,并不是任何Unicode字符都可以用2個字節(jié)來表示了。
舉個例子,假設(shè)我們現(xiàn)在有一個字符串:
在Unicode中,對應(yīng)的碼點(diǎn)(code point)如下:
瞧,僅僅是一堆碼點(diǎn)而已,或者說數(shù)字。不過到現(xiàn)在為止,我們還沒有說這些碼點(diǎn)究竟是如何存儲到內(nèi)存或如何表示在email信息中的。
#p#副標(biāo)題#e#
要存儲,編碼的概念當(dāng)然就被引入進(jìn)來。
Unicode最早的編碼想法,就是把每一個碼點(diǎn)(code point)都存儲在兩個字節(jié)中,這也就導(dǎo)致了大多數(shù)人的誤解。于是Hello就變成了:
這樣對嗎?如下如何?
技術(shù)上說,我相信這樣是可以的。事實(shí)上,早期的實(shí)現(xiàn)者們的確想把Unicode的碼點(diǎn)(code point)按照大端或小端兩種方式存儲,這樣至少已經(jīng)有兩種存儲Unicode的方法了。于是人們就必須使用FE FF作為每一個Unicode字符串的開頭,我們稱這個為Unicode Byte Order Mark。如果你互換了你的高位與低位,就變成了FF FE,這樣讀取這個字符串的程序就知道后面字節(jié)也需要互換了。可惜,不是每一個Unicode字符串都有字節(jié)序標(biāo)記。
現(xiàn)在,看起來好像問題已經(jīng)解決了,可是這幫程序員仍在抱怨。”看看這些零!”他們會這樣說,因?yàn)樗麄兪敲绹耍麄冎豢床粫a點(diǎn)不會超過U+00FF的英文字母。同時他們也是California的嬉皮士,他們想節(jié)省一點(diǎn)。如果他們是得克薩斯人,可能他們就不會介意兩倍的字節(jié)數(shù)。但是這樣California節(jié)儉的人卻無法忍受字符串所占空間翻倍。而且現(xiàn)在大堆的文檔使用的是ANSI和DBCS字符集,誰去轉(zhuǎn)換它們?于是這幫人選擇忽略Unicode,繼續(xù)自己的路,這顯然讓事情變得更糟。
于是非常聰明的UTF-8的概念被引入了。UTF-8是另一個系統(tǒng),用來存儲字符串所對應(yīng)的Unicode的碼點(diǎn) (code points)-即那些神奇的U+數(shù)字組合,在內(nèi)存中,而且存儲的最小單元是8比特的字節(jié)。在UTF-8中,0-127之間的碼字都使用一個字節(jié)來存儲,超過128的碼字使用2,3甚至6個字節(jié)來存儲。

這顯然有非常好的效果,因?yàn)橛⑽牡奈谋臼褂肬TF-8存儲的形式完全與ASCII一樣了,所以美國人壓根不會注意到發(fā)生了什么變化。舉個例子,Hello — U+0048 U+0065 U+006C U+006C U+006C U+006F,將會被存儲為48 65 6C 6C 6F,這與ASCII、與ANSI標(biāo)準(zhǔn)、與所有這個星球上的OEM字符集顯然都是一樣的。現(xiàn)在,如果你需要使用希臘字母,你可以用幾個字節(jié)來存儲一個碼字,美國人永遠(yuǎn)都不會注意到。(干嗎得美國人注意,無語,美國人寫的文章…)
到現(xiàn)在我已經(jīng)告訴了你三種Unicode的編碼方式,傳統(tǒng)的使用兩個字節(jié)存儲的稱之為UCS-2或者UTF-16,而且你必須判斷空間是大端的UCS-2還是小端的UCS-2。新的UTF-8標(biāo)準(zhǔn)顯然更流行,如果你恰巧有專門面向英文的程序,顯然這些程序不需要知道UTF-8的存在依然可以工作地很好。
事實(shí)上,還有其它若干對Unicode編碼的方法。比如有個叫UTF-7,和UTF-8差不多,但是保證字節(jié)的最高位總是0,這樣如果你的字符不得不經(jīng)過一些嚴(yán)格的郵件系統(tǒng)時(這些系統(tǒng)認(rèn)為7個比特完全夠用了),就不會有信息丟失。還有一個UCS-4,使用4個字節(jié)來存儲每個碼點(diǎn)(code point),好處是每個碼點(diǎn)都使用相同的字節(jié)數(shù)來存儲,可惜這次就算是得克薩斯人也不愿意了,因?yàn)檫@個方法實(shí)在太浪費(fèi)了。
現(xiàn)在的情況變成了你思考事情時所使用的基本單元–柏拉圖式的字母已經(jīng)被Unicode的碼點(diǎn)完全表示了。這些碼點(diǎn)也可以完全使用其它舊的編碼體系。比如,你可以把 Hello對應(yīng)的Unicode碼點(diǎn)串(U+0048 U+0065 U+006C U+006C U+006F)用ASCII、OEM Greek、Hebrew ANSI或其它上百個編碼體系來編碼,不過需要注意一點(diǎn),有些字母會無法顯示。如果你要表示的Unicode碼點(diǎn)在你使用的編碼體系中壓根沒有對應(yīng)的字符,那么你可能會得到一個小問號”?”,或者得到一個”�”。
許多傳統(tǒng)的編碼體系僅僅能編碼Unicode碼點(diǎn)中的一部分,其余全部會被顯示為問號。比較流行的英文編碼體系有Windows-1252(Windows 9x中的西歐語言標(biāo)準(zhǔn))和ISO-8859-1,還有aka Latin-1。但是如果想用這些編碼體系來編碼俄語或者希伯來語就只能得到一串問號了。UTF 7,8,16,和32都可以完全正確編碼Unicode中的所有碼點(diǎn)。
如果你完全忘掉了我剛剛解釋過的內(nèi)容,沒有關(guān)系,請記住一點(diǎn),如果你不知道一個字符串所使用的編碼,這個字符串在你手中也就毫無意義。你不能再把腦袋埋進(jìn)沙中以為”純文本”就是ASCII。事實(shí)上,
那么我們?nèi)绾蔚弥粋€字符串所使用的空間是何種編碼呢?對于這個問題已經(jīng)有了標(biāo)準(zhǔn)的作法。如果是一份電子郵件,你必須在格式的頭部有如下語句:
Content-Type: text/plain; charset=”UTF-8″
對于一個網(wǎng)頁,傳統(tǒng)的想法是Web服務(wù)器會返回一個類似于Content-Type的http頭和Web網(wǎng)頁,注意,這里的字符編碼并不是在HTML中指出,而是在獨(dú)立的響應(yīng)headers中指出。
這帶來了一些問題。假設(shè)你擁有一個大的Web服務(wù)器,擁有非常多的站點(diǎn),每個站點(diǎn)都包括數(shù)以百計的Web頁面,而寫這些頁面的人可能使用不同的語言,他們在他們自己計算機(jī)上的FrontPage等工具中看到頁面正常顯示就提交了上來,顯然,服務(wù)器是沒有辦法知道這些文件究竟使用的是何種編碼,當(dāng)然 Content-Type頭也沒有辦法發(fā)送了。
如果可以把Content-Type夾在HTML文件中,那不是會變得非常方便?這個想法會讓純粹論者發(fā)瘋,你如何在不知道它的編碼的情況下讀一個HTML文件呢?答案很簡單,因?yàn)閹缀跛械木幋a在32-127的碼字都做相同的事情,所以不需要使用特殊字符,你可以從HTML文件中獲得你想要的Content-Type。
<html>
<head>
<meta http-equiv=”Conent-Type” content=”text/html” charset=”utf-8″>
注意,這里的meta標(biāo)簽必須在head部分第一個出現(xiàn),一旦瀏覽器看到這個標(biāo)簽就會馬上停止解析頁面,然后使用這個標(biāo)簽中給出的編碼從頭開始重新解析整個頁面。
如果瀏覽器在http頭或者meta標(biāo)簽中都找不到相關(guān)的Content-Type信息,那應(yīng)該怎么辦?Internet Explorer做了一些事情:它試圖猜測出正確的編碼,基于不同語言編碼中典型文本中出現(xiàn)的那些字節(jié)的頗率。因?yàn)楣爬系?比特的碼頁(code pages)傾向于把它們的國家編碼放置在128-255碼字的范圍內(nèi),而不同的人類語言字母系統(tǒng)中的字母使用頗率對應(yīng)的直方圖會有不同,所以這個方法可以奏效。雖然很怪異,但對于那些老忘記寫Content-Type的幼稚網(wǎng)頁編寫者而言,這個方法大多數(shù)情況下可以讓他們的頁面顯然OK。直到有一天,他們寫的頁面不再滿足”letter-frequency-distribution”,Internet Explore覺得這應(yīng)該是朝鮮語,于是就當(dāng)朝鮮語來顯示了,結(jié)果顯然糟透了。這個頁面的讀者們立刻就遭殃了,一個保加利亞語寫的頁面卻用朝鮮語來顯示,效果會怎樣?于是讀者使用 查看–>編碼 菜單來不停地試啊試,直到他終于試出了正確的編碼,但前提是他知道可以這樣做,事實(shí)上大多數(shù)人根本不會這樣做。
在我的公司開發(fā)的一款Web頁面管理軟件CityDesk的最新版本中,我們決定像Visual Basic、COM和Windows NT/2000/XP所做的那樣,整個過程中使用UCS-2(兩個字節(jié))Unicode。在我們寫的C++代碼中,我們把所有的char類型換成了wchar_t,所有使用str函數(shù)的地方,換成了相應(yīng)的wcs函數(shù)(如使用wcscat和wcslen來替代strcat和strlen)。如果想在C中創(chuàng)建一個UCS-2的字符串,只需在字符串前面加L即可:L”Hello”。
當(dāng)CityDesk發(fā)布頁面的時候,它把所有的頁面都轉(zhuǎn)換成了UTF-8編碼,而差不多所有的瀏覽器都對UTF-8有不錯的支持。這就是”Joel On Software”(就是作者的首頁)編碼的方式,所以即使它擁有29個語言版本,至今也未聽到有一個人抱怨頁面無法瀏覽。
這篇文章已經(jīng)有點(diǎn)長了,而且我也沒有辦法告訴你關(guān)于字符編碼和Unicode的所有應(yīng)該了解的知識,但讀到現(xiàn)在我想你已經(jīng)掌握到基本的概念,回去編程時可以使用抗生素而不是螞蝗和咒語了,這就看做是留給你的作業(yè)吧。
]]>