http://github.thinkingbar.com/charset-study/
http://www.zhihu.com/question/20650946
http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
http://www.zhihu.com/question/20171860
http://baike.baidu.com/subview/7671/8245153.htm
http://www.zhihu.com/question/20167122
http://www.cnblogs.com/me-sa/p/erlang-unicode_2.html
http://www.searchtb.com/2012/04/chinese_encode.html
http://www.unicode.org/faq/utf_bom.html#BOM
======
編碼問題的例子
在windows自帶的notepad(記事本)程序中輸入“聯通”兩個字(C1 AA CD A8 ),保存後再次打開,會發現“聯通”不見了,代之以“��ͨ”的亂碼。這是windows平臺上典型的中文編碼問題。即文件保存的時候是按照ANSI編碼(GB2312),打開的時候程序按照UTF-8方式對內容解釋,於是就出現了亂碼。避免亂碼的方式很簡單,在“文件”菜單中選擇“打開”命令,選擇保存的文件,然後選擇“ANSI”編碼,此時就能看到正常的“聯通”兩個字了。如果以ansi的方式打開保存爲utf8的"聯通"二字(E8 81 94 E9 80 9A ),會出現“鑱旈€ ”的亂碼,“旈”的ansi編碼剛好爲94 E9。
記事本將ansi保存爲utf8後變爲EF BB BF E8 81 94 E9 80 9A 。EF BB BF是windows軟件bom。
BOM(Byte Order Mark),是UTF編碼方案裏用於標識編碼的標準標記,在UTF-16裏本來是 FEFF,變成UTF-8就成了EF BB BF。這個標記是可選的,因爲UTF8字節沒有順序,所以它可以被用來檢測一個字節流是否是UTF-8編碼的。微軟做這種檢測,但有些軟件不做這種檢測, 而把它當作正常字符處理。
微軟在自己的UTF-8格式的文本文件之前加上了EF BB BF三個字節, windows上面的notepad等程序就是根據這三個字節來確定一個文本文件是ASCII的還是UTF-8的, 然而這個只是微軟暗自作的標記, 其它平臺上並沒有對UTF-8文本文件做個這樣的標記。
也 就是說一個UTF-8文件可能有BOM,也可能沒有BOM,那麼怎麼區分呢?三種方法。1,用EditPlus打開文件,切換到十六進制編輯模 式,察看文件頭部是否有EF BB BF。2,用Dreamweaver打開,察看頁面屬性,看“包括Unicode簽名BOM”前面是否有個勾。3,用Windows的記事本打開,選擇 “另存爲”,看文件的默認編碼是UTF-8還是ANSI,如果是ANSI則不帶BOM。
要想把一個文件去掉 BOM,使用EditPlus打開, 切換到十六進制編輯模式,把最前面三個字節(就是那該死的 EF BB BF)替換爲20,保存(注意關閉保存時自動備份的功能),再切換到默認編輯模式,把最前面的三個空格去掉就可以了。
所謂的unicode保存的文件實際上是utf-16,只不過恰好跟unicode的碼相同而已,但在概念上unicode與 utf是兩回事,unicode是內存編碼表示方案,而utf是如何保存和傳輸unicode的方案。utf-16還分高位在前 (LE)和高位在後(BE)兩種。官方的utf編碼還有utf-32,也分LE和BE。非unicode官方的utf編碼還有utf-7,主要用於郵件傳輸。utf-8的單字節部分是和iso-8859-1兼容的,這主要是一些舊的系統和庫函數不能正確處理utf-16而被迫出來的,而且對英語字符來說,
也節省保存的文件空間(以非英語字符浪費空間爲代價)。在iso-8859-1的時候,utf8和iso-8859-1都是用一個字節表示的,當表示其它 字符的時候,utf-8會使用兩個或三個字節。
使用轉換程序將gb2312文件轉換成UTF-8文件時要注意默認設置是不是帶BOM。php之類的解釋型腳本要注意編碼問題造成的亂碼。
在 ANSI 版本中,長度指的是字節數,在 Unicode 版本中,長度指的是字符的個數。沒有「ANSI 編碼」這種東西。不恰當地被稱作「ANSI」的是各種非 Uncode 的、遺留地 Windows code pages,詳見維基百科。「ANSI」這個概念沒有意義。 GB 2312、GBK、GB 18030 各自主要是對前一個方案的字符集擴展,編碼都是基於
EUC-CN 的。
如果是爲了跨平臺兼容性,只需要知道,在 Windows 記事本的語境中:
ANSI指的是對應當前系統 locale 的遺留(legacy)編碼。Windows 裏說的ANSI其實是 Windows code pages,這個模式根據當前 locale 選定具體的編碼,比如簡中 locale 下是 GBK。把自己這些 code page 稱作「ANSI」是 Windows 的臭毛病。在 ASCII 範圍內它們應該是和 ASCII 一致的。
Unicode指的是 UTF-16LE。把 UTF-16LE 稱作「Unicode」也是 Windows 的臭毛病。Windows 從 Windows 2000 開始就已經支持 surrogate pair 了,所以已經是 UTF-16 了,「UCS-2」這個說法已經不合適了。UCS-2 只能編碼 BMP 範圍內的字符,從 1996 年起就在 Unicode/ISO 標準中被 UTF-16
取代了(UTF-16 通過蛋疼的 surrogate pair 來編碼超出 BMP 的字符)。
UTF-8指的是帶 BOM 的 UTF-8。把帶 BOM 的 UTF-8 稱作「UTF-8」又是 Windows 的臭毛病。如果忽略 BOM,那麼在 ASCII 範圍內與 ASCII 一致。另請參見:「帶 BOM 的 UTF-8」和「無 BOM 的 UTF-8」有什麼區別?
GBK 等遺留編碼最麻煩,所以除非你知道自己在幹什麼否則不要再用了。
UTF-16LE 理論上其實很好,字節序也標明瞭,但 UTF-16 畢竟不常用。
UTF-8 本來是兼容性最好的編碼,但 Windows 偏要加 BOM 於是經常出問題。
所以,跨平臺兼容性最好的其實就是不用記事本。
建議用 Notepad++ 等正常的專業文本編輯器保存爲不帶 BOM 的 UTF-8。
如果文本中所有字符都在 ASCII 範圍內,那麼其實,記事本保存的所謂的「ANSI」文件,和 ASCII 或無 BOM 的 UTF-8 是一樣的。
UTF8是用3個字節儲存文字,對於文本文件,需要BOM來標示編碼,對於網頁,統一使用UTF8編碼,故無需BOM,部分舊網頁依然使用ANSI編碼,打開後是亂碼,只能手動選擇編碼了。UTF-8 僅僅是 Unicode 的一個 encoding form
在windows裏面記事本可以保存四種格式。
ansi,這種格式的文本沒有BOM的。
然後就是下面的三種格式,UTF-8 UTF-16(大端序)UTF-16(小端序)(即unicode)
編碼表示 (十六進制)表示 (十進制)
UTF-8 EF BB BF 239 187 191
UTF-16(大端序) FE FF 254 255
UTF-16(小端序) FF FE 255 254
(以下供參考)
UTF-32(大端序) 00 00 FE FF 0 0 254 255
UTF-32(小端序) FF FE 00 00 255 254 0 0
UTF-7 2B 2F 76和以下的一個字節:[ 38 | 39 | 2B | 2F ] 43 47 118和以下的一個字節:[ 56 | 57 | 43 | 47 ]
en:UTF-1 F7 64 4C 247 100 76
en:UTF-EBCDIC DD 73 66 73 221 115 102 115
en:Standard Compression Scheme for Unicode 0E FE FF 14 254 255
en:BOCU-1 FB EE 28及可能跟隨着FF 251 238 40及可能跟隨着255
GB-18030 84 31 95 33 132 49 149 51
關於字符集(character set)和編碼(encoding)
對於 ASCII、GB2312、Big5、GBK、GB18030 之類的遺留方案來說,基本上一個字符集方案只使用一種編碼方案。
比如 ASCII 標準本身就直接規定了字符集和字符編碼的方式,所以既是字符集又是編碼方案。
而 GB2312 只是一個區位碼形式的字符集標準,不過實際上基本都用 EUC-CN 來編碼,所以提及GB2312時也說的是一個字符集和編碼連鎖的方案。
GBK 和 GB 18030 等向後兼容於 GB2312 的方案也類似。
對 於 Unicode,字符集和編碼是明確區分的。Unicode/UCS 標準首先是個統一的字符集標準。而 Unicode/UCS 標準同時也定義了幾種可選的編碼方案,在標準文檔中稱作「encoding form」,主要包括 UTF-8、UTF-16 和 UTF-32。
所以,對 Unicode 方案來說,同樣的基於 Unicode 字符集的文本可以用多種編碼來存儲、傳輸。
所以,用「Unicode」來稱呼某個編碼方案不合適,屬於語義不清。
BOM——Byte Order Mark,就是字節序標記
在UCS 編碼中有一個叫做"ZERO WIDTH NO-BREAK SPACE"的字符,它的編碼是FEFF。而FFFE在UCS中是不存在的字符,所以不應該出現在實際傳輸中。UCS規範建議我們在傳輸字節流前,先傳輸字符"ZERO
WIDTH NO-BREAK SPACE"。這樣如果接收者收到FEFF,就表明這個字節流是Big-Endian的;如果收到FFFE,就表明這個字節流是Little-Endian的。因此字符"ZERO WIDTH NO-BREAK SPACE"又被稱作BOM。
UTF-8不需要BOM來表明字節順序,但可以用BOM來表明編碼方式。字符"ZERO WIDTH NO-BREAK SPACE"的UTF-8編碼是EF BB BF。所以如果接收者收到以EF BB BF開頭的字節流,就知道這是UTF-8編碼了。
UTF-8編碼的文件中,BOM佔三個字節。如果用記事本把一個文本文件另存爲UTF-8編碼方式的話,用UE打開這個文件,切換到十六進制編輯狀態就可以看到開頭的FFFE了。這是個標識UTF-8編碼文件的好辦法,軟件通過BOM來識別這個文件是否是UTF-8編碼,很多軟件還要求讀入的文件必須帶BOM。可是,還是有很多軟件不能識別BOM。
Firefox支持。PHP不支持BOM,不會忽略UTF-8編碼的文件開頭BOM的那三個字符。
“受COOKIE送出機制的限制,在這些文件開頭已經有BOM的文件中,COOKIE無法送出(因爲在COOKIE送出前PHP已經送出了文件頭),所以登入和登出功能失效。一切依賴COOKIE、SESSION實現的功能全部無效。”這個應該就是Wordpress後臺出現空白頁面的原因了,因爲任何一個被執行的文件包含了BOM,這三個字符都將被送出,導致依賴cookies和session的功能失效。
解決的辦法嘛,如果只包含英文字符(或者說ASCII編碼內的字符),就把文件存成ASCII碼方式。用UE等編輯器的話,點文件->轉換->UTF-8轉ASCII,或者在另存爲裏選擇ASCII編碼。如果是DOS格式的行尾符,可以用記事本打開,點另存爲,選ASCII編碼。如果包含中文字符的話,可以用UE的另存爲功能,選擇“UTF-8 無 BOM”即可。
在Linux平臺上如果使用cat等命令查看文件中的中文內容時,可能出現亂碼。這也是編碼的問題。簡單的說就是文件按照A編碼保存,但是cat命令按照當前Locale設定的B編碼去查看,在B和A不兼容的時候就出現了亂碼。
爲什麼寫這篇文章
中文編碼由於歷史原因牽扯到不少標準,在不瞭解的時候感覺一頭霧水;但其實理解編碼問題並不需要你深入瞭解各個編碼標準,只要你明白了來龍去脈,瞭解了關鍵的知識點,就能分析和解決日常開發工作中碰到的大部分編碼問題。有感於我看過的資料和文章要麼不夠全面,要麼略顯枯燥,所以通過這篇文章記錄下筆者在日常工作中碰到的中文編碼原理相關問題,目的主要是自我總結,如果能給讀者提供一些幫助那就算是意外之喜了。由於嚴謹的編碼標準對我來說是無趣的,枯燥的,難以記憶的,本文嘗試用淺顯易懂的生活語言解釋中文編碼相關的(也可能不相關的)一些問題,這也是爲什麼取名雜談的原因。本文肯定存在不規範不全面的地方,我會在參考資料裏給出官方文檔的鏈接,也歡迎讀者在評論中提出更好的表達方式&指出錯誤,不勝感激。
對編碼問題的理解我認爲分爲三個層次,第一個層次:概念,知道各個編碼標準的應用場景,瞭解之間的差異,能分析和解決常見的一些編碼問題。第二個層次:標準,掌握編碼的細節,如編碼範圍,編碼轉換規則,知道這些就能自行開發編碼轉換工具。第三個層次,使用,瞭解中文的編碼2進制存儲,在程序開發過程中選擇合理的編碼並處理中文。爲了避免讓讀者陷入編碼標準的黑洞無法脫身(不相信?看看unicode的規範就明白我的意思了),同時由於編碼查詢&轉換工具等都有現成工具可以使用,本文只涉及第一個層次,不涉及第二層次,在第三層次上會做一些嘗試。在本文的最後提供了相關鏈接供對標準細節感興趣的同學繼續學習。最後,本文不涉及具體軟件的亂碼問題解決,如ssh,shell,vim,screen等,這些話題留給劍豪同學專文闡述。
一切都是因爲電腦不識字
電腦很聰明,可以幫我們做很多事情,最開始主要是科學計算,這也是爲什麼電腦別名計算機。電腦又很笨,在她的腦子裏只有數字,即所有的數據在存儲和運算時都要使用二進制數表示。這在最初電腦主要用來處理大量複雜的科學計算時不是什麼大問題但是當電腦逐步走入普通人的生活時,情況開始變遭了。辦公自動化等領域最主要的需求就是文字處理,電腦如何來表示文字呢?這個問題當然難不倒聰明的計算機科學家們,用數字來代表字符唄。這就是“編碼”。
英文的終極解決方案:ASCII
每個人都可以約定自己的一套編碼,只要使用方之間瞭解就ok了。比如說咱倆約定0×10表示a,0×11表示b。在一開始也的確是這樣的,出現了各式各樣的編碼。這樣有兩個問題:1.各個編碼的字符集不一樣,有的多,有的少。2.相同字符的編碼也不一樣。你這裏a是0×10.他那裏a可能是0×30。於是你保存的文件他就不能直接用,必須要轉換編碼。隨着溝通範圍的擴大,採用不同編碼的人們互相通信就亂套了,這就是我們常說的:雞同鴨講。如果要避免這種混亂,那麼大家就必須使用相同的編碼規則,於是美國有關的標準化組織就出臺了ASCII(American Standard Code for Information Interchange)編碼,統一規定了英文常用符號用哪些二進制數來表示。ASCII是標準的單字節字符編碼方案,用於基於文本的數據。
ASCII最初是美國國家標準,供不同計算機在相互通信時用作共同遵守的西文字符編碼標準,已被國際標準化組織(International Organization for Standardization, ISO)定爲國際標準,稱爲ISO 646標準。適用於所有拉丁文字字母。ASCII 碼使用指定的7 位或8 位二進制數組合來表示128 或256 種可能的字符。標準ASCII 碼也叫基礎ASCII碼,使用7 位二進制數來表示所有的大寫和小寫字母,數字0 到9、標點符號, 以及在美式英語中使用的特殊控制字符。而最高位爲1的另128個字符(80H—FFH)被稱爲“擴展ASCII”,一般用來存放英文的製表符、部分音標字符等等的一些其它符號。
其中:0~31及127(共33個)是控制字符或通信專用字符(其餘爲可顯示字符),32~126(共95個)是字符(32是空格),其中48~57爲0到9十個阿拉伯數字,65~90爲26個大寫英文字母,97~122號爲26個小寫英文字母,其餘爲一些標點符號、運算符號等。
現在所有使用英文的電腦終於可以用同一種編碼來交流了。理解了ASCII編碼,其他字母型的語言編碼方案就觸類旁通了。
一波三折的中文編碼
第一次嘗試:GB2312
ASCII這種字符編碼規則顯然用來處理英文沒有什麼問題,它的出現極大的促進了信息在西方尤其是美國的傳播和交流。但是對於中文,常用漢字就有6000以上,ASCII 單字節編碼顯然是不夠用。爲了粉碎美帝國主義通過編碼限制中國人民使用電腦的無恥陰謀,中國國家標準總局發佈了GB2312碼即中華人民共和國國家漢字信息交換用編碼,全稱《信息交換用漢字編碼字符集——基本集》,1981年5月1日實施,通行於大陸。GB2312字符集中除常用簡體漢字字符外還包括希臘字母、日文平假名及片假名字母、俄語西裏爾字母等字符,未收錄繁體中文漢字和一些生僻字。 EUC-CN可以理解爲GB2312的別名,和GB2312完全相同。
GB2312是基於區位碼設計的,在區位碼的區號和位號上分別加上A0H就得到了GB2312編碼。這裏第一次提到了“區位碼”,我就連帶把下面這幾個讓人摸不到頭腦的XX碼一鍋端了吧:
區位碼,國標碼,交換碼,內碼,外碼
區位碼:就是把中文常用的符號,數字,漢字等分門別類進行編碼。區位碼把編碼表分爲94個區,每個區對應94個位,每個位置就放一個字符(漢字,符號,數字都屬於字符)。這樣每個字符的區號和位號組合起來就成爲該漢字的區位碼。區位碼一般用10進制數來表示,如4907就表示49區7位,對應的字符是“學”。區位碼中01-09區是符號、數字區,16-87區是漢字區,10-15和88-94是未定義的空白區。它將收錄的漢字分成兩級:第一級是常用漢字計3755個,置於16-55區,按漢語拼音字母/筆形順序排列;第二級漢字是次常用漢字計3008個,置於56-87區,按部首/筆畫順序排列。在網上搜索“區位碼查詢系統”可以很方便的找到漢字和對應區位碼轉換的工具。爲了避免廣告嫌疑和死鏈,這裏就不舉例了。
國標碼: 區位碼無法用於漢字通信,因爲它可能與通信使用的控制碼(00H~1FH)(即0~31,還記得ASCII碼特殊字符的範圍嗎?)發生衝突。於是ISO2022規定每個漢字的區號和位號必須分別加上32(即二進制數00100000,16進制20H),得到對應的國標交換碼,簡稱國標碼,交換碼,因此,“學”字的國標交換碼計算爲:
1
2
3
4
|
00110001
00000111 +
00100000 00100000 ------------------- 01010001
00100111 |
用十六進制數表示即爲5127H。
交換碼:即國標交換碼的簡稱,等同上面說的國標碼。
內碼:由於文本中通常混合使用漢字和西文字符,漢字信息如果不予以特別標識,就會與單字節的ASCII碼混淆。此問題的解決方法之一是將一個漢字看成是兩個擴展ASCII碼,使表示GB2312漢字的兩個字節的最高位都爲1。即國標碼加上128(即二進制數10000000,16進制80H)這種高位爲1的雙字節漢字編碼即爲GB2312漢字的機內碼,簡稱爲內碼。20H+80H=A0H。這也就是常說的在區位碼的區號和位號上分別加上A0H就得到了GB2312編碼的由來。
1
2
3
4
|
00110001
00000111 +
10100000 10100000 ------------------- 11010001
10100111 |
用十六進制數表示即爲D1A7H。
外碼:機外碼的簡稱,就是漢字輸入碼,是爲了通過鍵盤字符把漢字輸入計算機而設計的一種編碼。 英文輸入時,相輸入什麼字符便按什麼鍵,外碼和內碼一致。漢字輸入時,可能要按幾個鍵才能輸入一個漢字。 漢字輸入方案有成百上千個,但是這千差萬別的外碼輸入進計算機後都會轉換成統一的內碼。
最後總結一下上面的概念。中國國家標準總局把中文常用字符編碼爲94個區,每個區對應94個位,每個字符的區號和位號組合起來就是該字符的區位碼, 區位碼用10進制數來表示,如4907就表示49區7位,對應的字符是“學”。 由於區位碼的取值範圍與通信使用的控制碼(00H~1FH)(即0~31)發生衝突。每個漢字的區號和位號分別加上32(即16進制20H)得到國標碼,交換碼。“學”的國標碼爲5127H。由於文本中通常混合使用漢字和西文字符,爲了讓漢字信息不會與單字節的ASCII碼混淆,將一個漢字看成是兩個擴展ASCII碼,即漢字的兩個字節的最高位置爲1,得到的編碼爲GB2312漢字的內碼。“學”的內碼爲D1A7H。無論你使用什麼輸入法,通過什麼樣的按鍵組合把“學”輸入計算機,“學”在使用GB2312(以及兼容GB2312)編碼的計算機裏的內碼都是D1A7H。
第二次嘗試:GBK
GB2312的出現基本滿足了漢字的計算機處理需要,但由於上面提到未收錄繁體字和生僻字,從而不能處理人名、古漢語等方面出現的罕用字,這導致了1995年《漢字編碼擴展規範》(GBK)的出現。GBK編碼是GB2312編碼的超集,向下完全兼容GB2312,兼容的含義是不僅字符兼容,而且相同字符的編碼也相同,同時在字彙一級支持ISO/IEC10646—1和GB 13000—1的全部中、日、韓(CJK)漢字,共計20902字。GBK還收錄了GB2312不包含的漢字部首符號、豎排標點符號等字符。CP936和GBK的有些許差別,絕大多數情況下可以把CP936當作GBK的別名。
第三次嘗試:GB18030
GB18030編碼向下兼容GBK和GB2312。GB18030收錄了所有Unicode3.1中的字符,包括中國少數民族字符,GBK不支持的韓文字符等等,也可以說是世界大多民族的文字符號都被收錄在內。GBK和GB2312都是雙字節等寬編碼,如果算上和ASCII兼容所支持的單字節,也可以理解爲是單字節和雙字節混合的變長編碼。GB18030編碼是變長編碼,有單字節、雙字節和四字節三種方式。
其實,這三個標準並不需要死記硬背,只需要瞭解是根據應用需求不斷擴展編碼範圍即可。從GB2312到GBK再到GB18030收錄的字符越來越多即可。萬幸的是一直是向下兼容的,也就是說一個漢字在這三個編碼標準裏的編碼是一模一樣的。這些編碼的共性是變長編碼,單字節ASCII兼容,對其他字符GB2312和GBK都使用雙字節等寬編碼,只有GB18030還有四字節編碼的方式。這些編碼最大的問題是2個。1.由於低字節的編碼範圍和ASCII有重合,所以不能根據一個字節的內容判斷是中文的一部分還是一個獨立的英文字符。2.如果有兩個漢字編碼爲A1A2B1B2,存在A2B1也是一個有效漢字編碼的特殊情況。這樣就不能直接使用標準的字符串匹配函數來判斷一個字符串裏是否包含某一個漢字,而需要先判斷字符邊界然後才能進行字符匹配判斷。
最後,提一個小插曲,上面講的都是大陸推行的漢字編碼標準,使用繁體的中文社羣中最常用的電腦漢字字符集標準叫大五碼(Big5),共收錄13,060箇中文字,其中有二字爲重覆編碼(實在是不應該)。Big5雖普及於中國的臺灣、香港與澳門等繁體中文通行區,但長期以來並非當地的國家標準,而只是業界標準。倚天中文系統、Windows等主要系統的字符集都是以Big5爲基準,但廠商又各自增刪,衍生成多種不同版本。2003年,Big5被收錄到臺灣官方標準的附錄當中,取得了較正式的地位。這個最新版本被稱爲Big5-2003。
天下歸一Unicode
看了上面的多箇中文編碼是不是有點頭暈了呢?如果把這個問題放到全世界n多個國家n多語種呢?各國和各地區自己的文字編碼規則互相沖突的情況全球信息交換帶來了很大的麻煩。
要真正徹底解決這個問題,上面介紹的那些通過擴展ASCII修修補補的方式已經走不通了,而必須有一個全新的編碼系統,這個系統要可以將中文、日文、法文、德文……等等所有的文字統一起來考慮,爲每一個文字都分配一個單獨的編碼。於是,Unicode誕生了。Unicode(統一碼、萬國碼、單一碼)爲地球上(以後會包括火星,金星,喵星等)每種語言中的每個字符設定了統一併且唯一的二進制編碼,以滿足跨語言、跨平臺進行文本轉換、處理的要求。在Unicode裏,所有的字符被一視同仁,漢字不再使用“兩個擴展ASCII”,而是使用“1個Unicode”來表示,也就是說,所有的文字都按一個字符來處理,它們都有一個唯一的Unicode碼。Unicode用數字0-0x10FFFF來映射這些字符,最多可以容納1114112個字符,或者說有1114112個碼位(碼位就是可以分配給字符的數字)。
提到Unicode不能不提UCS(通用字符集Universal Character Set)。UCS是由ISO制定的ISO 10646(或稱ISO/IEC 10646)標準所定義的標準字符集。UCS-2用兩個字節編碼,UCS-4用4個字節編碼。Unicode是由unicode.org制定的編碼機制,ISO與unicode.org是兩個不同的組織, 雖然最初制定了不同的標準; 但目標是一致的。所以自從unicode2.0開始, unicode採用了與ISO 10646-1相同的字庫和字碼, ISO也承諾ISO10646將不會給超出0x10FFFF的UCS-4編碼賦值, 使得兩者保持一致。大家簡單認爲UCS等同於Unicode就可以了。
在Unicode中:漢字“字”對應的數字是23383。在Unicode中,我們有很多方式將數字23383表示成程序中的數據,包括:UTF-8、UTF-16、UTF-32。UTF是“UCS Transformation Format”的縮寫,可以翻譯成Unicode字符集轉換格式,即怎樣將Unicode定義的數字轉換成程序數據。例如,“漢字”對應的數字是0x6c49和0x5b57,而編碼的程序數據是:
1
2
3
|
BYTE data_utf8[]
= {0xE6, 0xB1, 0x89, 0xE5, 0xAD, 0x97}; //
UTF-8編碼 WORD data_utf16[]
= {0x6c49, 0x5b57}; //
UTF-16編碼 DWORD data_utf32[]
= {0x6c49, 0x5b57}; //
UTF-32編碼 |
這裏用BYTE、WORD、DWORD分別表示無符號8位整數,無符號16位整數和無符號32位整數。UTF-8、UTF-16、UTF-32分別以BYTE、WORD、DWORD作爲編碼單位。“漢字”的UTF-8編碼需要6個字節。“漢字”的UTF-16編碼需要兩個WORD,大小是4個字節。“漢字”的UTF-32編碼需要兩個DWORD,大小是8個字節。根據字節序的不同,UTF-16可以被實現爲UTF-16LE或UTF-16BE,UTF-32可以被實現爲UTF-32LE或UTF-32BE。
下面介紹UTF-8、UTF-16、UTF-32、BOM。
UTF-8,UTF-8 僅僅是 Unicode 的一個 encoding form
UTF-8以字節爲單位對Unicode進行編碼。從Unicode到UTF-8的編碼方式如下:
Unicode編碼(16進制) | UTF-8 字節流(二進制) |
000000 – 00007F | 0xxxxxxx |
000080 – 0007FF | 110xxxxx 10xxxxxx |
000800 – 00FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
010000 – 10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
UTF-8的特點是對不同範圍的字符使用不同長度的編碼。對於0×00-0x7F之間的字符,UTF-8編碼與ASCII編碼完全相同。UTF-8編碼的最大長度是4個字節。從上表可以看出,4字節模板有21個x,即可以容納21位二進制數字。Unicode的最大碼位0x10FFFF也只有21位。總結了一下規律:UTF-8的第一個字節開始的1的個數代表了總的編碼字節數,後續字節都是以10開始。由上面的規則可以清晰的看出UTF-8編碼克服了中文編碼的兩個問題。
例1:“漢”字的Unicode編碼是0x6C49。0x6C49在0×0800-0xFFFF之間,使用3字節模板了:1110xxxx 10xxxxxx 10xxxxxx。將0x6C49寫成二進制是:0110 1100 0100 1001, 用這個比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。
例2:Unicode編碼0x20C30在0×010000-0x10FFFF之間,使用用4字節模板了:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx。將0x20C30寫成21位二進制數字(不足21位就在前面補0):0 0010 0000 1100 0011 0000,用這個比特流依次代替模板中的x,得到:11110000 10100000 10110000 10110000,即F0 A0 B0 B0。
UTF-16
UTF-16編碼以16位無符號整數爲單位。我們把Unicode編碼記作U。編碼規則如下: 如果U<0×10000,U的UTF-16編碼就是U對應的16位無符號整數(爲書寫簡便,下文將16位無符號整數記作WORD)。中文範圍 4E00-9FBF,所以在UTF-16編碼裏中文2個字節編碼。如果U≥0×10000,我們先計算U’=U-0×10000,然後將U’寫成二進制形式:yyyy yyyy yyxx xxxx xxxx,U的UTF-16編碼(二進制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx。
UTF-32
UTF-32編碼以32位無符號整數爲單位。Unicode的UTF-32編碼就是其對應的32位無符號整數。
字節序
根據字節序(對字節序不太瞭解的同學請參考http://en.wikipedia.org/wiki/Endianness)的不同,UTF-16可以被實現爲UTF-16LE(Little Endian)或UTF-16BE(Big Endian),UTF-32可以被實現爲UTF-32LE或UTF-32BE。例如:
Unicode編碼 | UTF-16LE | UTF-16BE | UTF-32LE | UTF-32BE |
0x006C49 | 49 6C | 6C 49 | 49 6C 00 00 | 00 00 6C 49 |
0x020C30 | 43 D8 30 DC | D8 43 DC 30 | 30 0C 02 00 | 00 02 0C 30 |
那麼,怎麼判斷字節流的字節序呢?Unicode標準建議用BOM(Byte Order Mark)來區分字節序,即在傳輸字節流前,先傳輸被作爲BOM的字符”零寬無中斷空格”。這個字符的編碼是FEFF,而反過來的FFFE(UTF-16)和FFFE0000(UTF-32)在Unicode中都是未定義的碼位,不應該出現在實際傳輸中。下表是各種UTF編碼的BOM:
UTF編碼 | Byte Order Mark |
UTF-8 | EF BB BF |
UTF-16LE | FF FE |
UTF-16BE | FE FF |
UTF-32LE | FF FE 00 00 |
UTF-32BE | 00 00 FE FF |
總結一下,ISO與unicode.org都敏銳的意識到只有爲世界上每種語言中的每個字符設定統一併且唯一的二進制編碼才能徹底解決計算機世界信息交流中編碼衝突的問題。由此誕生了UCS和unicode,而這兩個規範是一致的。在Unicode裏,所有的字符被一視同仁,也就是說,所有的文字都按一個字符來處理,它們都有一個唯一的Unicode碼。UTF-8、UTF-16、UTF-32分別定義了怎樣將Unicode定義的數字轉換成程序數據。UTF-8以字節爲單位對Unicode進行編碼,一個英文字符佔1個字節,漢字佔3個字節;UTF-16以16位無符號整數爲單位對Unicode進行編碼,中文英文都佔2個字節;UTF-32以32位無符號整數爲單位對Unicode進行編碼,中文英文都佔4個字節。可以在http://www.unicode.org/charts/unihan.html 查看漢字的unicode碼以及UTF-8、UTF-16、UTF-32編碼。
中文二進制存儲
介紹了這麼多的編碼知識,真正的文件內容是什麼樣子的呢?下面我們就通過實驗看看在筆者Linux機器上 “中文”這兩個字在不同的編碼下保存的文件內容。下面是我的實驗過程,有興趣的同學可以在自己的機器上重做一下。window平臺上的情況類似這裏就不贅述了。
實驗需要需要使用2個工具:
1. od 查看文件內容:http://www.gnu.org/software/coreutils/manual/html_node/od-invocation.html
2. iconv 編碼轉換工具:http://www.gnu.org/software/libiconv/
漢字 | Unicode(ucs-2)10進製表示 | Utf-8 | Utf-16 | Utf32 | 區位碼 | GB2312/GBK/GB18030 |
中 | 20013 | E4 B8 AD | 4E2D | 00004E2D | 5448 | D6D0 |
文 | 25991 | E6 96 87 | 6587 | 00006587 | 4636 | CEC4 |
機器環境:
os: Red Hat Enterprise Linux AS release 4
Cpu: Intel(R) Xeon(R) CPU
locale:LC_ALL=zh_CN.utf-8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
//生成utf8編碼下的文件 echo
–n "中文" >
foo.utf8 //檢查foo的內容: od
-t x1 foo.utf8 0000000
e4 b8 ad e6 96 87 //轉換爲utf16編碼 iconv
-f utf-8 -t utf-16 foo.utf8 > foo.utf16 //查看foo.utf16內容 od
-t x1 foo.utf16 0000000
ff fe 2d 4e 87 65 Ff
fe是BOM(還記得嗎?通過BOM來字節流的字節序),其餘部分的確是UTF-16LE編碼的內容 //轉換爲utf32編碼 iconv
-f utf-16 -t utf-32 foo.utf16 > foo.utf32 //查看foo.utf32內容 od
-t x1 foo.utf32 0000000
ff fe 00 00 2d 4e 00 00 87 65 00 00 Ff
fe是BOM,的確是UTF-32LE編碼的內容 //轉換爲gb2312編碼 iconv
-f utf-8 -t gb2312 foo.txt > foo.gb2312 od
-t x1 foo.gb2312 0000000
d6 d0 ce c4 //轉換爲GBK編碼 iconv
-f utf-8 -t gbk foo.txt > foo.gbk od
-t x1 foo.gbk 0000000
d6 d0 ce c4 //轉換爲GB18030編碼 iconv
-f utf-8 -t gb18030 foo.txt > foo.gb18030 od
-t x1 foo.gb18030 0000000
d6 d0 ce c4 |
C語言中文處理
先明確一個概念:程序內部編碼和程序外部編碼。程序內部編碼指的是中文字符在程序運行時在內存中的編碼形式。程序外部編碼則是中文字符在存儲或者傳輸時的編碼形式。程序外部編碼的最直觀的例子就是當把中文存儲到硬盤文件中時選擇的編碼。
根據程序內部編碼和程序外部編碼是否一致,C/C++的中文處理有兩種常見的方式:
1. 內外編碼相同。輸入輸出時不需要考慮編碼轉換,程序內部處理時把中文字符當做普通的2進制數據流進行處理。
2. 內外編碼不同。輸入輸出的時候根據應用需要選擇合適的編碼格式進行編碼轉換;程序內部統一編碼處理。
方法1的優點不言而喻,由於內外統一,不需要進行轉換。不足是如果不是C標準庫支持的編碼方式,那麼字符串處理函數需要自己實現。比如說標準strlen函數不能計算中文編碼&UTF-8等的字符串長度,而需要根據編碼標準自行實現。GBK等中文編碼除了計算字符串長度的函數外,字符串匹配函數也要自己實現(原因看上文中文編碼總結)。當需要支持的編碼格式不斷增多時,處理函數的開發和維護就需要付出更大的代價。
方法2針對方法1的不足加以改進。在程序內部可以優先選擇C標準庫支持的編碼方式,或者根據需要自己實現對某一特定編碼格式的完整支持,這樣任何編碼都可以先轉換爲支持的編碼,代碼通用性比較好。
那麼C標準庫對中文編碼的支持如何呢?目前Linux平臺一般使用GNU C library,內建了對單字節的char和寬字符wchar_t的支持。Char大家都很熟悉了,處理中文需要的wchar_t要重點介紹一下。從實現上來說在linux平臺上可以認爲wchar_t是4byte的int,內部存儲字符的UTF32編碼。由於標準庫已經內建了對wchar_t比較完備的支持,如使用wcslen 計算字符串長度,使用wcscmp進行字符串比較等等。所以比較簡單的方式是使用上面的方法2,同時選擇wchar_t作爲內部字符的表示。做到這一點還是比較容易的,在輸入輸出的時候通過mbrtowc/wcrtomb 進行單個字符的內外編碼轉換,以及通過mbsrtowcs/wcsrtombs 進行字符串的內外編碼轉換即可。這裏需要注意兩點:
1. 代碼中字符串常量的表示不同。舉例說明:Char c=’a’; Wchar_t wc=L’中’;
2. 上面兩組函數的轉換是依賴locale設置的,即locale決定了外部編碼的類型。確切的說是LC_CTYPE決定了外部編碼的類型。默認情況下程序啓動時使用標準“C”locale,而不是LC系列的環境變量指定的。所以需要首先調用下面的函數:setlocale (LC_ALL, “”);這樣程序就使用了用戶通過設置LC系列環境變量選擇的Locale。
關於locale的話題比較大,這裏就不深入了,留待下一篇文章吧.
上面的方法很完美,是嗎?不是嗎?得到這麼多的好處不是無代價的,最明顯的代價就是內存,任何一個字符,不管中文還是英文如果保持在wchar_t裏就需要4個byte,就這一個理由就足以限制了這個方案在關注內存使用的應用場景下的使用。
Python的中文處理
對Python來說由於內建unicde的支持,所以採用輸入輸出的時候進行轉換,內部保持unicode的方式使用是個不錯的方案。http://docs.python.org/tutorial/introduction.html#unicode-strings這裏作爲起點,有興趣的同學自學吧。
編碼選擇建議:
1. 只有英文:毫不猶豫選擇內外編碼都選擇ASCII,通用且存儲代價小。
2. 主要存中文,對存儲大小比較敏感:內外部編碼根據文字使用範圍選擇GB2312或者GBK,自行實現使用到的字符串處理函數。
3. 通用性第一,處理簡單:外部選擇UTF-8,內部可以使用UTF-8或者UTF-32(即wchar_t)
參考資料:
http://baike.baidu.com/view/25492.htm
http://baike.baidu.com/view/25421.htm
http://baike.baidu.com/view/40801.htm
http://www.ibm.com/developerworks/cn/linux/i18n/unicode/linuni/
http://www.gnu.org/software/libc/manual/html_node/index.html