每个软件开发人员绝对必须了解 Unicode 和字符集的绝对最低限度(没有任何借口!
有沒有想過那個神秘的 Content-Type 標簽?你知道,你應該放在HTML中的那個,你永遠不知道它應該是什么?
你有沒有收到過朋友發(fā)來的郵件,主題是“???? ?????? ??? ????”?
我沮喪地發(fā)現(xiàn),有多少軟件開發(fā)人員并沒有真正完全了解字符集、編碼、Unicode 等神秘世界。幾年前,F(xiàn)ogBUGZ的Beta測試人員想知道它是否可以處理日語傳入的電子郵件。日語?他們有日語電子郵件嗎?我不知道。當我仔細查看我們用于分析 MIME 電子郵件的商業(yè) ActiveX 控件時,我們發(fā)現(xiàn)它對字符集做了完全錯誤的事情,因此我們實際上必須編寫英雄代碼來撤消它所做的錯誤轉(zhuǎn)換并正確地重做它。當我查看另一個商業(yè)庫時,它也有一個完全損壞的字符代碼實現(xiàn)。我與該軟件包的開發(fā)人員通信,他有點認為他們“對此無能為力”。像許多程序員一樣,他只是希望一切都能以某種方式結(jié)束。
但事實并非如此。當我發(fā)現(xiàn)流行的 Web 開發(fā)工具 PHP 幾乎完全無視字符編碼問題時,我輕率地使用 8 位字符,幾乎不可能開發(fā)出好的國際 Web 應用程序,我想,夠了。
所以我要宣布:如果你是 2003 年的一名程序員,你不知道字符、字符集、編碼和 Unicode 的基礎知識,而我抓住了你,我會懲罰你,讓你在潛艇里剝洋蔥 6 個月。我發(fā)誓我會的。
還有一件事:
這并不難。
在這篇文章中,我將向你介紹每個在職程序員都應該知道的內(nèi)容。所有關(guān)于“純文本 = ascii = 字符是 8 位”的東西不僅是錯誤的,而且是無可救藥的錯誤,如果你仍然以這種方式編程,你并不比不相信細菌的醫(yī)生好多少。在閱讀完本文之前,請不要再編寫一行代碼。
在我開始之前,我應該警告你,如果你是那些了解國際化的少數(shù)人之一,你會發(fā)現(xiàn)我的整個討論有點過于簡單化了。我真的只是想在這里設定一個最低標準,以便每個人都能理解正在發(fā)生的事情,并可以編寫代碼,希望能夠處理除英語子集以外的任何語言的文本,這些語言不包括帶重音的單詞。我應該警告你,字符處理只是創(chuàng)建國際通用軟件所需的一小部分,但我一次只能寫一件事,所以今天是字符集。
歷史視角
理解這些東西的最簡單方法是按時間順序排列。
你可能會認為我要在這里談論像 EBCDIC 這樣的非常古老的字符集。好吧,我不會。EBCDIC與您的生活無關(guān)。我們不必回到那么久以前。
回到半古代,當Unix被發(fā)明出來,K&R正在編寫C編程語言時,一切都非常簡單。EBCDIC正在退出。唯一重要的字符是舊的無重音英文字母,我們有一個稱為 ASCII 的代碼,它能夠使用 32 到 127 之間的數(shù)字來表示每個字符??崭袷?32,字母“A”是 65,等等。這可以方便地以 7 位存儲。當時大多數(shù)計算機都使用 8 位字節(jié),所以你不僅可以存儲所有可能的 ASCII 字符,而且你有一整塊空余,如果你是邪惡的,你可以將其用于你自己的狡猾目的:WordStar 的昏暗燈泡實際上打開了高位來指示單詞中的最后一個字母, 譴責 WordStar 僅提供英文文本。低于 32 的代碼稱為不可打印,用于 cusing。開玩笑。它們用于控制字符,例如 7 會使您的計算機發(fā)出嗶嗶聲,而 12 會導致當前頁面的紙張從打印機中飛出并送入新頁面。
一切都很好,假設你是一個說英語的人。
因為字節(jié)最多有 128 位的空間,所以很多人開始想,“天哪,我們可以將代碼 255-128 用于我們自己的目的。麻煩的是,很多人同時有這個想法,他們對從255到8088的空間應該去哪里有自己的想法。IBM-PC 有一個后來被稱為 OEM 字符集的東西,它為歐洲語言提供了一些重音字符和一堆線條繪制字符......單杠、豎杠、右邊掛著小叮叮當當?shù)男「茆彽?,你可以用這些畫線字符在屏幕上制作漂亮的方框和線條,你仍然可以在干洗店的 128 電腦上看到它們運行。事實上,一旦人們開始在美國以外的地方購買 PC,各種不同的 OEM 角色集就被想象出來,它們都使用前 130 個角色來達到自己的目的。例如,在某些 PC 上,字符代碼 128 將顯示為 é,但在以色列銷售的計算機上,它是希伯來字母 Gimel (
),因此當美國人將他們的簡歷發(fā)送到以色列時,他們會以 r
sum
s 的形式到達。在許多情況下,例如俄語,對于如何處理上面的 <> 個字符有很多不同的想法,因此您甚至無法可靠地交換俄語文檔。
最終,這種 OEM 免費產(chǎn)品被編入 ANSI 標準。在 ANSI 標準中,每個人都同意在 128 以下做什么,這與 ASCII 幾乎相同,但有很多不同的方法可以處理 128 及以上的字符,具體取決于您居住的地方。這些不同的系統(tǒng)稱為代碼頁。例如,在以色列,DOS 使用一個名為 862 的代碼頁,而希臘用戶使用 737。它們在 128 以下相同,但與 128 以上不同,所有有趣的字母都駐留在那里。MS-DOS的國家版本有幾十個這樣的代碼頁,可以處理從英語到冰島語的所有內(nèi)容,他們甚至有一些“多語言”代碼頁,可以在同一臺計算機上使用世界語和加利西亞語!哇!但是,除非您編寫自己的自定義程序來使用位圖圖形顯示所有內(nèi)容,否則在同一臺計算機上獲取希伯來語和希臘語是完全不可能的,因為希伯來語和希臘語需要不同的代碼頁,對高數(shù)字有不同的解釋。
與此同時,在亞洲,更瘋狂的事情正在發(fā)生,因為亞洲字母表有數(shù)千個字母,而這些字母永遠無法容納 8 位。這通常是通過稱為DBCS的混亂系統(tǒng)來解決的,DBCS是“雙字節(jié)字符集”,其中一些字母存儲在一個字節(jié)中,而其他字母則存儲在兩個字節(jié)中。在一根繩子中向前移動很容易,但向后移動幾乎是不可能的。鼓勵程序員不要使用 s++ 和 s– 來前后移動,而是調(diào)用 Windows 的 AnsiNext 和 AnsiPrev 等函數(shù),它們知道如何處理整個混亂。
但是,大多數(shù)人只是假裝一個字節(jié)是一個字符,一個字符是 8 位,只要你從不將字符串從一臺計算機移動到另一臺計算機,或者說一種以上的語言,它就會一直有效。但是,當然,一旦互聯(lián)網(wǎng)出現(xiàn),將字符串從一臺計算機移動到另一臺計算機就變得司空見慣,整個混亂局面就崩潰了。幸運的是,Unicode已經(jīng)被發(fā)明出來了。
統(tǒng)一碼
Unicode是一個勇敢的努力,它創(chuàng)造了一個單一的字符集,包括地球上所有合理的書寫系統(tǒng),以及一些虛構(gòu)的書寫系統(tǒng),如克林貢語。有些人誤以為 Unicode 只是一個 16 位代碼,其中每個字符需要 16 位,因此有 65,536 個可能的字符。實際上,這是不正確的。這是關(guān)于Unicode最常見的神話,所以如果你這么想,不要感到難過。
事實上,Unicode對字符的思考方式是不同的,你必須理解Unicode對事物的思考方式,否則什么都不會有意義。
到目前為止,我們假設一個字母映射到一些可以存儲在磁盤或內(nèi)存中的位:
A -> 0100 0001
在Unicode中,一個字母映射到一個叫做碼位的東西,這仍然只是一個理論概念。該代碼點如何在內(nèi)存或磁盤上表示是一個完整的故事。
在Unicode中,字母A是柏拉圖式的理想。它只是漂浮在天堂:
一個
這個柏拉圖式的 A 與 B 不同,與 a 不同,但與 A 和 A 和 A 相同。Times New Roman 字體中的 A 與 Helvetica 字體中的 A 是相同的字符,但與小寫的“a”不同,這種想法似乎沒有太大爭議,但在某些語言中,僅僅弄清楚字母是什么就會引起爭議。德語字母 ? 是真正的字母還是只是一種花哨的 ss 書寫方式?如果一個字母的形狀在單詞末尾發(fā)生了變化,那是一個不同的字母嗎?希伯來語說“是”,阿拉伯語說“不是”。無論如何,Unicode聯(lián)盟的聰明人在過去十年左右的時間里一直在弄清楚這一點,伴隨著大量高度政治化的辯論,你不必擔心。他們已經(jīng)想通了。
每個字母表中的每個柏拉圖字母都由Unicode聯(lián)盟分配一個幻數(shù),其寫法如下:U+0639。這個幻數(shù)稱為碼位。U+ 表示“Unicode”,數(shù)字是十六進制的。U+0639 是阿拉伯字母 Ain。英文字母 A 是 U+0041。您可以使用 Windows 2000/XP 上的 charmap 實用程序或訪問 Unicode 網(wǎng)站找到它們。
Unicode 可以定義的字母數(shù)量沒有真正的限制,事實上它們已經(jīng)超過了 65,536 個,因此并非每個 Unicode 字母都可以真正壓縮成兩個字節(jié),但這無論如何都是一個神話。
好的,假設我們有一個字符串:
你好
在 Unicode 中,它對應于以下五個代碼點:
U+0048 U+0065 U+006C U+006C U+006F。
只是一堆代碼點。數(shù)字,真的。我們還沒有說任何關(guān)于如何將其存儲在內(nèi)存中或在電子郵件中表示它的任何內(nèi)容。
編碼
這就是編碼的用武之地。
Unicode編碼的最早想法,導致了關(guān)于兩個字節(jié)的神話,是,嘿,讓我們把這些數(shù)字分別存儲在兩個字節(jié)中。所以你好變成了
00 48 00 65 00 6C 00 6C 00 6F
右?沒那么快!難道不能也是:
48 00 65 00 6C 00 6C 00 6F 00 ?
嗯,從技術(shù)上講,是的,我確實相信它可以,事實上,早期的實現(xiàn)者希望能夠以高端或低端模式存儲他們的 Unicode 碼位,無論他們的特定 CPU 在哪個模式下最快,瞧,現(xiàn)在是晚上和早上,已經(jīng)有兩種方法來存儲 Unicode。因此,人們被迫想出一個奇怪的約定,即在每個Unicode字符串的開頭存儲一個FE FF;這稱為 Unicode 字節(jié)順序標記,如果您要交換高字節(jié)和低字節(jié),它看起來像 FF FE,讀取字符串的人會知道他們必須每隔一個字節(jié)交換一次。唷。并非每個 Unicode 字符串的開頭都有一個字節(jié)順序標記。
有一段時間,這似乎已經(jīng)足夠好了,但程序員們卻在抱怨?!翱纯茨切┝悖 彼麄冋f,因為他們是美國人,他們看到的是英文文本,很少使用U+00FF以上的代碼點。他們也是加利福尼亞的自由派嬉皮士,他們想要保護(冷笑)。如果他們是德克薩斯人,他們不會介意消耗兩倍的字節(jié)數(shù)。但是那些加利福尼亞的懦夫無法忍受將字符串所需的存儲量增加一倍的想法,無論如何,已經(jīng)有所有這些使用各種 ANSI 和 DBCS 字符集的狗狗文檔,誰來轉(zhuǎn)換它們?莫伊?僅出于這個原因,大多數(shù)人決定忽略Unicode好幾年,與此同時,情況變得更糟。
因此發(fā)明了 UTF-8 的絕妙概念。UTF-8 是另一個系統(tǒng),用于使用 8 位字節(jié)將 Unicode 碼位字符串(即那些神奇的 U+ 數(shù)字)存儲在內(nèi)存中。在 UTF-8 中,從 0 到 127 的每個碼位都存儲在一個字節(jié)中。只有 128 及以上的代碼點使用 2、3 存儲,實際上最多存儲 6 個字節(jié)。
這有一個巧妙的副作用,即英語文本在 UTF-8 中看起來與在 ASCII 中完全相同,因此美國人甚至不會注意到任何錯誤。只有世界其他地方必須跳過鐵環(huán)。具體來說,U+0048 U+0065 U+006C U+006C U+006F 的 Hello 將存儲為 48 65 6C 6C 6F,看哪!與存儲在 ASCII 和 ANSI 以及地球上的每個 OEM 字符集中的相同。現(xiàn)在,如果你膽大妄為地使用重音字母、希臘字母或克林貢字母,你將不得不使用幾個字節(jié)來存儲一個碼位,但美國人永遠不會注意到。(UTF-8 還具有一個很好的屬性,即想要使用單個 0 字節(jié)作為 null 終止符的無知舊字符串處理代碼不會截斷字符串)。
到目前為止,我已經(jīng)告訴了您三種編碼Unicode的方法。傳統(tǒng)的 save-it-in-two-byte 方法稱為 UCS-2(因為它有兩個字節(jié))或 UTF-16(因為它有 16 位),您仍然需要弄清楚它是高端 UCS-2 還是低端 UCS-2。還有流行的新 UTF-8 標準,它有一個很好的特性,如果你有幸巧合地使用英語文本和腦死亡程序,而這些程序完全不知道除了 ASCII 之外還有任何東西,它也可以很好地工作。
實際上還有很多其他的Unicode編碼方法。有一種叫做 UTF-7 的東西,它很像 UTF-8,但保證高位永遠為零,所以如果你必須通過某種嚴厲的警察國家電子郵件系統(tǒng)傳遞 Unicode,認為 7 位就足夠了,謝謝你,它仍然可以毫發(fā)無損地擠過去。有 UCS-4,它以 4 個字節(jié)的形式存儲每個碼位,它有一個很好的特性,即每個碼位都可以存儲在相同數(shù)量的字節(jié)中,但是,天哪,即使是德克薩斯人也不會如此大膽地浪費那么多內(nèi)存。
事實上,現(xiàn)在你正在考慮由Unicode碼位表示的柏拉圖式理想字母,這些Unicode碼位也可以用任何老式的編碼方案進行編碼!例如,您可以用 ASCII 編碼 Hello 的 Unicode 字符串 (U+0048 U+0065 U+006C U+006C U+006F),或者舊的 OEM 希臘語編碼,或希伯來語 ANSI 編碼,或者迄今為止發(fā)明的數(shù)百種編碼中的任何一種,但有一個問題:某些字母可能不會顯示!如果嘗試在嘗試表示它的編碼中沒有等效的 Unicode 碼位,通常會得到一個小問號:?或者,如果你真的很好,一個盒子。你得到了哪個?-> ?
有數(shù)百種傳統(tǒng)編碼只能正確存儲一些碼位,并將所有其他碼位更改為問號。一些流行的英語文本編碼是 Windows-1252(西歐語言的 Windows 9x 標準)和 ISO-8859-1,又名拉丁語 1(也適用于任何西歐語言)。但是嘗試將俄語或希伯來語字母存儲在這些編碼中,你會得到一堆問號。UTF 7、8、16 和 32 都具有能夠正確存儲任何代碼點的良好屬性。
關(guān)于編碼的最重要的一個事實
如果你完全忘記了我剛才解釋的一切,請記住一個極其重要的事實。在不知道字符串使用什么編碼的情況下?lián)碛凶址菦]有意義的。您不能再把頭埋在沙子里,假裝“純”文本是 ASCII。
沒有純文本這樣的東西。
如果在內(nèi)存中、文件或電子郵件中有一個字符串,則必須知道它采用的編碼,否則無法正確解釋它或向用戶顯示它。
幾乎每一個愚蠢的“我的網(wǎng)站看起來像胡言亂語”或“當我使用口音時她無法閱讀我的電子郵件”的問題都歸結(jié)為一個天真的程序員,他不明白一個簡單的事實,即如果你不告訴我特定字符串是使用 UTF-8 還是 ASCII 或 ISO 8859-1(拉丁語 1)或 Windows 1252(西歐)編碼的, 您根本無法正確顯示它,甚至無法弄清楚它的結(jié)束位置。有超過一百種編碼,在代碼點 127 以上,所有賭注都已關(guān)閉。
我們?nèi)绾伪A粲嘘P(guān)字符串使用的編碼的信息?嗯,有標準的方法可以做到這一點。對于電子郵件,表單的標題中應包含一個字符串
內(nèi)容類型:text/plain;字符集=“UTF-8”
對于網(wǎng)頁,最初的想法是 Web 服務器將返回一個類似的 Content-Type http 標頭以及網(wǎng)頁本身——不是在 HTML 本身中,而是作為在 HTML 頁面之前發(fā)送的響應標頭之一。
這會導致問題。假設您有一個大型 Web 服務器,其中包含許多站點和數(shù)百個頁面,這些頁面由許多人以多種不同的語言貢獻,并且所有頁面都使用他們的 Microsoft FrontPage 副本認為適合生成的任何編碼。Web 服務器本身并不知道每個文件是用什么編碼編寫的,因此它無法發(fā)送 Content-Type 標頭。
如果您可以使用某種特殊標簽將 HTML 文件的 Content-Type 直接放在 HTML 文件本身中,那就太方便了。當然,這讓純粹主義者發(fā)瘋了......你怎么能讀HTML文件,直到你知道它是什么編碼?!幸運的是,幾乎所有常用的編碼都對 32 到 127 之間的字符執(zhí)行相同的操作,因此您始終可以在 HTML 頁面上走到這一步,而無需開始使用有趣的字母:
<html>
<head>
<meta http-equiv=“內(nèi)容類型” content=“text/html;字符集=UTF-8“>
但是這個元標記實際上必須是<head>部分的第一件事,因為一旦 Web 瀏覽器看到這個標簽,它就會停止解析頁面,并在使用您指定的編碼重新解釋整個頁面后重新開始。
如果 Web 瀏覽器在 http 標頭或 meta 標記中找不到任何 Content-Type,它們會怎么做?Internet Explorer 實際上做了一些非常有趣的事情:它試圖根據(jù)各種語言的典型編碼中各種字節(jié)在典型文本中出現(xiàn)的頻率來猜測使用了哪種語言和編碼。因為各種舊的 8 位代碼頁傾向于將其國家字母放在 128 到 255 之間的不同范圍內(nèi),并且因為每種人類語言都有不同的字母使用特征直方圖,這實際上有機會奏效。這確實很奇怪,但它似乎確實經(jīng)常起作用,以至于天真的網(wǎng)頁作者從來不知道他們需要 Content-Type 標頭,在 Web 瀏覽器中查看他們的頁面,它看起來還不錯,直到有一天,他們寫的東西不完全符合他們母語的字母頻率分布,而 Internet Explorer 確定它是韓語并這樣顯示它, 我認為,證明波斯特爾定律關(guān)于“在你發(fā)出的東西上保守,在你接受的東西上自由”的觀點,坦率地說,這不是一個好的工程原則。無論如何,這個網(wǎng)站是用保加利亞語寫的,但似乎是韓國人(甚至沒有凝聚力的韓國人),可憐的讀者在做什么?他使用 View |編碼菜單并嘗試一堆不同的編碼(東歐語言至少有十幾種),直到圖片更清晰。如果他知道這樣做,大多數(shù)人都不知道。
對于我公司發(fā)布的最新版本的 CityDesk(網(wǎng)站管理軟件),我們決定在內(nèi)部使用 UCS-2(雙字節(jié))Unicode 進行所有操作,這是 Visual Basic、COM 和 Windows NT/2000/XP 用作其本機字符串類型。C++代碼中,我們只是將字符串聲明為 wchar_t(“寬字符”)而不是 char,并使用 wcs 函數(shù)而不是 str 函數(shù)(例如,wcscat 和 wcslen 而不是 strcat 和 strlen)。 要在 C 代碼中創(chuàng)建文字 UCS-2 字符串,只需在它前面加上一個 L,如下所示:L“Hello”。
當 CityDesk 發(fā)布網(wǎng)頁時,它會將其轉(zhuǎn)換為 UTF-8 編碼,該編碼多年來一直受到 Web 瀏覽器的良好支持。這就是 Joel on Software 的所有 29 種語言版本的編碼方式,我還沒有聽說過一個人在查看它們時遇到任何問題。
這篇文章越來越長,我不可能涵蓋所有關(guān)于字符編碼和 Unicode 的知識,但我希望如果你已經(jīng)讀到這里,你有足夠的知識回到編程,使用抗生素而不是水蛭和咒語,我現(xiàn)在將交給你。