天天看點

學點Unicode又不會死——Unicode的流言終結者和編碼大揭秘

如果你是一個生活在2003年的程式員,卻不了解字元、字元集、編碼和unicode這些基礎知識。那你可要小心了,要是被我抓到你,我會讓你在潛水艇裡剝六個月洋蔥來懲罰你。

這個邪惡的恐吓是joel spolsky在十年前首次發出的。不幸的是,很多人認為他隻是在開玩笑,是以,現在仍有許多人不能完全了解unicode,以及unicode、utf-8、utf-16之間的差別。這就是我寫這篇文章的原因。

言歸正傳,設想在一個晴朗的下午,你收到一封電子郵件,它來自一個你高中之後就失去聯系的朋友,并帶有一個txt格式(也稱為純文字格式)的附件。這個附件包含下面這樣一串二進制bits:

0100100001000101010011000100110001001111

email的正文是空的,這使它更加神秘。在你啟動常用的文本編輯器打開這個附件之前,你有沒有想過,文本編輯器是怎麼将二進制形式翻譯成字元的?這其中有兩個關鍵問題:

位元組是怎樣分組的?(例如1個位元組的字元和2個位元組的字元)

一個或多個位元組是怎麼映射到字元上的?

位元組是怎麼分組的,如8 bits或16 bits一組,這也被稱作編碼單元。

編碼單元和字元之間的映射關系。例如,在ascii碼中,十進制65映射到字母a上

字元編碼和字元集之間有微小的差別。不過通常它和你無關,除非你在設計一個底層的庫。

ascii碼是上個世紀最流行的編碼體系之一,至少在西方是這樣。下圖顯示了ascii碼中編碼單元是怎麼映射到字元上的。

學點Unicode又不會死——Unicode的流言終結者和編碼大揭秘

asciifull

有一個即使在經驗豐富的程式員中也非常常見的誤解就是,純文字使用ascii碼并且每個字元都是8 bits。

事實是,沒有這樣的「純文字」。如果在記憶體或者硬碟中有一個你不知道編碼的字元串,那你就無法翻譯或者顯示它。這絕對沒有第二條路可選。

那麼當你剛剛收到的附件沒有指定編碼格式的時候,計算機會如何翻譯它呢?這是否意味着你就永遠也讀不到失去聯系的老朋友想跟你說的話了呢?在我們找到答案之前,我們首先回到那個年代————那個用錢能買到的最大硬碟是29mb的時代。

<a target="_blank"></a>

很久以前,計算機制造商有自己的表示字元的方式。他們并不需要擔心如何和其它計算機交流,并提出了各自的方式來将字形渲染到螢幕上。随着計算機越來越流行,廠商之間的競争更加激烈,在不同的計算機體系間轉換資料變得十分蛋疼,人們厭煩了這種自定義造成的混亂。

最終,計算機制造商一起制定了一個标準的方法來描述字元。他們定義使用一個位元組的低7位來表示字元,并且制作了如上圖所示的對照表來映射七個比特的值到一個字元上。例如,字母a是65,c是99,~是126等等, ascii碼就這樣誕生了。原始的ascii标準定義了從0到127 的字元,這樣正好能用七個比特表示。不過好景不長……

為什麼選擇了7個比特而不是8個來表示一個字元呢?我并不關心。但是一個位元組是8個比特,這意味着1個比特并沒有被使用,也就是從128到255的編碼并沒有被制定ascii标準的人所規定,這些美國人對世界的其它地方一無所知甚至完全不關心。

其它國家的人趁這個機會開始使用128到255範圍内的編碼來表達自己語言中的字元。例如,144在阿拉伯人的ascii碼中是گ,而在俄羅斯的ascii碼中是ђ。即使在美國,對于未使用區域也有各種各樣的利用。ibm pc就出現了“oem 字型”或”擴充ascii碼”,為使用者提供漂亮的圖形文字來繪制文本框并支援一些歐洲字元,例如英鎊(£)符号。

學點Unicode又不會死——Unicode的流言終結者和編碼大揭秘

lpcec9qu3zw3u5uldh_bnawejbesszsmhiv2jm-pyemhgj2ky0vkvn0ousiaqvmv4kmsg_gmhhlhwcbem-uem4wcxkm5hcungr3r7bibhm1ievimyks2cxidbg

用ibm擴充字元集繪制的很酷的dos啟動畫面

再強調一遍,ascii碼的問題在于盡管所有人都在0-127号字元的使用上達成了一緻,但對于128-255号字元卻有很多很多不同的解釋。你必須告訴計算機使用哪種風格的ascii碼才能正确顯示128-255号的字元。

這對于北美人和不列颠群島的人來說不算什麼問題,因為無論使用哪種風格的ascii碼,拉丁字母的顯示都是一樣的。英國人還需要面對的問題是原始的ascii碼中不包含英鎊符号,但是這個已經無關緊要了。

與此同時,在亞洲有更讓人頭疼的問題。亞洲語言有更多的字元和字形需要被存儲,一個位元組已經不夠用了。是以他們開始使用兩個位元組來存儲字元,這被稱作dbcs(雙位元組編碼方案)。在dbcs中,字元串操作變得很蛋疼,你應該怎麼做str++或str–?

這些問題成為了系統開發者的噩夢。例如,ms dos必須支援所有風格的ascii碼,因為他們想把軟體賣到其他國家去。他們提出了「内碼表」這一概念。例如,你需要告訴dos(通過使用”chcp”指令)你想使用保加利亞語的内碼表,它才能顯示保加利亞字母。内碼表的更換會應用到整個系統。這對使用多種語言工作的人來說是一個問題,因為他們必須頻繁的在幾個内碼表之間來回切換。

盡管内碼表是一個好主意,但是它不是一個簡潔的解決方案,它隻是一個hack技術或者說是簡單的修正來讓編碼系統可以工作。

最終,美國人意識到他們應該提出一種标準方案來展示世界上所有語言中的所有字元,以便緩解程式員的痛苦和避免字元編碼引發的第三次世界大戰。出于這個目的,unicode誕生了。

unicode背後的想法非常簡單,然而卻被普遍的誤解了。unicode就像一個電話本,标記着字元和數字之間的映射關系。joel稱之為「神奇數字」,因為它們可能是随機指定的,而且不會給出任何解釋。官方術語是碼位(code point),總是用u+開頭。理論上每種語言中的每種字元都被unicode協會指定了一個神奇數字。例如希伯來文中的第一個字母א,是u+2135,字母a是u+0061。

unicode并不涉及字元是怎麼在位元組中表示的,它僅僅指定了字元對應的數字,僅此而已。

關于unicode的其它誤解包括:unicode支援的字元上限是65536個,unicode字元必須占兩個位元組。告訴你這些的人應該去換換腦子了。

記住,unicode隻是一個用來映射字元和數字的标準。它對支援字元的數量沒有限制,也不要求字元必須占兩個、三個或者其它任意數量的位元組。

unicode字元是怎樣被編碼成記憶體中的位元組這是另外的話題,它是被utf(unicode transformation formats)定義的。

兩個最流行的unicode編碼方案是utf-8和utf-16。讓我們看看它們的細節。

utf-8

utf-8是一個非常驚豔的概念,它漂亮的實作了對ascii碼的向後相容,以保證unicode可以被大衆接受。發明它的人至少應該得個諾貝爾和平獎。

在utf-8中,0-127号的字元用1個位元組來表示,使用和us-ascii相同的編碼。這意味着1980年代寫的文檔用utf-8打開一點問題都沒有。隻有128号及以上的字元才用2個,3個或者4個位元組來表示。是以,utf-8被稱作可變長度編碼。

回到文章開始的問題,來自你老朋友的附件的位元組流如下:

這個位元組流在ascii和utf-8中表示相同的字元:hello

utf-16

另一個流行的可變長度編碼方案是utf-16,它使用2個或者4個位元組來存儲字元。然而,人們逐漸意識到utf-16可能會浪費存儲空間,但那是另一個話題了。

endian讀作end-ian或者indian。這個術語的起源可以追溯到格列佛遊記。(小說中,小人國為水煮蛋應該從大的一端(big-end)剝開還是小的一端(little-end)剝開而争論,争論的雙方分别被稱為“大端派”和“小端派”。)

低位元組序和高位元組序隻是一個關于在記憶體中存儲和讀取一段位元組(被稱作words)的約定。這意味着當你讓計算機用utf-16把字母a(占兩個位元組)存在記憶體中時,使用哪種位元組序方案決定了你把第一個位元組放在第二個位元組的前面還是後面。這麼說有點不太容易懂,讓我們來看一個例子:當你使用utf-16存下來自你朋友的附件時,在不同的系統中它的後半部分可能是這樣的:

00 68 00 65 00 6c 00 6c 00 6f(高位元組序,高位位元組被存在前面)

68 00 65 00 6c 00 6c 00 6f 00(低位元組序,低位位元組被存在前面)

位元組序方案隻是一個微處理器架構設計者的偏好問題,例如,intel使用低位元組序,motorola使用高位元組序。

位元組順序标記(bom)

如果你經常要在高低位元組序的系統間轉換文檔,并且希望區分位元組序,還有一種奇怪的約定,被稱作bom。bom是一個設計得很巧妙的字元,用來放在文檔的開頭告訴閱讀器該文檔的位元組序。在utf-16中,它是通過在第一個位元組放置fe ff來實作的。在不同位元組序的文檔中,它會被顯示成ff fe或者fe ff,清楚的把這篇文檔的位元組序告訴了解釋器。

bom盡管很有用,但并不是很簡潔,因為還有一個類似的概念,稱作「魔術字」(magic byte),很多年來一直被用來表明檔案的格式。bom和魔術字間的關系一直沒有被清楚的定義過,是以有的解釋器會搞混它們。

還記得文章開頭的問題嗎,既然沒有「純文字」檔案這回事,那你的文本編輯器和浏覽器為什麼每次都能正确的顯示内容呢?答案是,那些軟體欺騙了你,這也是為什麼那麼多人對編碼一無所知。當軟體不能确定編碼的時候,它會猜測。大部分時候,它會猜測是否是涵蓋了ascii碼的utf-8,還是iso-8859-1,也有可能猜其他能想到的任意字元集。因為英文中使用的拉丁字母表在幾乎所有的字元集中都能顯示,包括utf-8,是以即使編碼猜錯了,英文字母看起來也是正确的。

但是,如果你在浏覽網頁時看到�符号,這意味着這個網頁的編碼不是你的浏覽器猜測的那個。這時你可以點開浏覽器的檢視——&gt;字元編碼菜單來嘗試不同的編碼。

如果你沒時間讀整篇文章或者你僅僅是略讀了一下前面的内容。那請你確定你能了解下面的幾條:

這個世界上從來沒有純文字這回事,如果你想讀出一個字元串,你必須知道它的編碼。

unicode是一個簡單的标準,用來把字元映射到數字上。unicode協會的人會幫你處理所有幕後的問題,包括為新字元指定編碼。

unicode并不告訴你字元是怎麼編碼成位元組的。這是被編碼方案決定的,通過utf來指定。

還有最重要的:

永遠記得通過content-type或者meta charset标簽來顯式指定你的文檔的編碼。這樣浏覽器就不需要猜測你使用的編碼了,他們會準确的使用你指定的編碼來渲染文檔。

原文釋出時間為:2014-02-15

本文來自雲栖社群合作夥伴“linux中國”

繼續閱讀