字符集與字元編碼 (charset & encoding)
亂碼是個大坑,相信每個人都遇過,而且是個繞不過去的坑。我理解每個程式設計師都應該寫一篇編碼相關的博文,梳理自己對這一塊的理解,下面是我反覆理解多次之後的學習小結。
1、從記事本的不同編碼說起:
開啟記事本,輸入“我我”,儲存為ansi編碼(其實是gb2312,這也是預設編碼)。再分別另存為unicode(其實是utf-16 little endian)、unicodeBigEndian(其實是utf-16 big endian)、utf8,用UltraEdit開啟,切換到二進位制模式,內容如下:
編碼 | 內容 |
ansi | CE D2 | CE D2 |
unicode | FF FE | 11 62 | 11 62 |
unicode big endian | FE FF | 62 11 | 62 11 |
utf-8 | EF BB BF | E6 88 91 | E6 88 91 |
可以看出,“我”在gb2312裡的編碼是CE D2,在utf-16 little endian裡是11 62,在utf-8裡是E6 88 91。FF FE是檔案頭,用來標識這個檔案是unicode little endian格式的,同理,EF BB BF標識檔案是utf-8格式。所謂的endian,是位元組順序,不同的作業系統,可能小位元組在前、也可能大位元組在前,既然不一樣,就用一個檔案頭(學名叫BOM,byte order mark,位元組順序識別符號)來標識。具體方法是:找一個在unicode裡不存在的字元(FF FE)來表示,EF BB BF是FF FE在utf-8裡的編碼。各種編碼的BOM詳見下表:
bytes | encoding |
EF BB BF | utf-8 |
FF FE | utf-16 little endian |
FE FF | utf-16 big endian |
FF FE 00 00 | utf-32 little endian |
00 00 FE FF | utf-32 big endian |
2、亂碼的各種現象
文字儲存在檔案裡都是位元組byte,那這些byte陣列是怎麼顯示為具體的字元呢?以ansi格式的“我”為例,CE D2顯示在介面上的步驟是這樣的:windows首先將CE D2轉換成它內部使用的編碼格式unicode,然後按照unicode編碼去字型檔案中查詢字型影象,最後將影象畫在視窗的指定位置上。如下:
- 字元首先以某種編碼儲存在檔案中。
- windows將檔案中的編碼對映為unicode編碼。
- windows根據unicode編碼去字型檔案中查詢字型影象,並畫在視窗上。
這3步中的每一步錯了,都體現為一種典型的亂碼。
- 錯誤1:如果弄錯了編碼格式,比如將“我”的utf-16編碼11 62錯存成了gb2312的檔案,就會出現亂碼。
- 錯誤2:如果對映到unicode出錯,例如出現了unicode裡未定義的字元編碼,windows就會使用預設字元,通常是?。比如“我”的gb2312編碼CE D2在unicode裡尚未使用,所以對映不到任何一個有效的unicode編碼。
- 錯誤3:如果對映到了unicode編碼,但在字型檔案中找不到對應的字元,windows就會顯示字型檔案中的預設影象:空白或方格。
3、各種編碼ascii、ansi(gb2312/big5/...)、unicode(utf8/16/32/...)
1) ascii用1個位元組(共255個)表示所有英文字元。缺點很明顯,就是不夠用。對於歐洲那些表音的字母類,就已經捉襟見肘了,再遇到中日韓的表義字元動輒上萬,就更不夠了。於是ansi想出來填坑,但結果是反而把事情搞麻煩了。其實如果一開始就想到字元不夠用的問題,再直接整出個全球統一字符集unicode,就不會有這個大坑了。所以說白了,亂碼問題是個歷史原因造成的問題(話說軟體裡那麼大坑,哪些不是歷史原因造成的!Y2K也是一例,但是誰也沒那個遠見,所以只能迂迴前進了)。
2) 然後,各國都發現ascii不夠用,於是各自造出自己的編碼來滿足需要,中國造出gb2312、臺灣造出big5大五碼、日本造出shift_jis、其他阿拉伯國家、印度也是類似的。先是IBM弄了內碼表(CodePage)、再是微軟繼承了這一套再加了些自己的定義,整成了ansi,包括所有地區的內碼表。通過這些內碼表,windows可以實現各地區編碼與unicode之間的相互轉換。(注:內碼表的具體檔案為:c:\windows\system32\*.nls檔案)內碼表是為了相容已有的程式而存在的,如果我們能強制所有程式都轉到unicode,那各地區的ansi編碼也就沒有存在的必要了,說白了只是發展過程中的中間產物。
3) 最後是unicode。
3.1) 先說說字符集與字型編碼的區別。對大多數字符集來說,字符集裡的編碼 = 字元編碼。比如“我”字,在gb2312中的編碼是CE D2,在big5中是A7 DA。唯獨在Unicode裡例外,Unicode使用4個位元組/32位(實際只用了31位,最高位必須為0)唯一表示全世界所有可能的字元。unicode的容量為231-1 ≈ 21億個,截止到2005年只用了10萬個,所以應該足夠用了。但每個字元4位元組,太浪費空間了,於是就產生了各種編碼方式,常見的有utf-8、utf-16、utf-32等。這裡,unicode是字符集,utf-8/16/32是字元編碼。同樣是“我”字,在unicode裡是00 00 62 11,在utf-8裡是E6 88 91,在utf-16裡是62 11,在utf-32裡才是00 00 62 11。
3.2) unicode到utf8/16/32的轉換方式就不詳述了。無非又是一些位操作,對理解這個問題的主幹無益。有興趣的可以參看本文最後的引用。
4、對比一下各編碼的儲存效率
- 儲存英文時:utf-8 = ansi > utf-16 > utf-32
- 儲存中文時:ansi ≈ utf-16 > utf-8 > utf-32
當然,這裡說的是大多數的情況,中英文混雜、多語言混雜、小語種文字都需要具體討論。
5、參考