[概述]
????? 在Windows操作系統中使用記事本新建一個文本文件,在文件里面寫入“聯通”兩個字并保存。當再次打開這個文本文件時候,在記事本中看到得卻不是剛剛輸入的“聯通”,而是亂碼。網絡上有人把這個奇怪現象包裝成把戲,如果你曾遇到過這種把戲就會知道,他們往往讓你建立兩個文本文件進行對比,其中一個輸入“聯通”,另外一個可能是“移動”等等,最后試圖八卦地讓你相信聯通、移動和微軟之間有著種種恩怨情仇。
[解釋]
????? 這是一個字符編碼應用的奇怪現象,講的明白點,可以說是記事本開小差了!記事本為什么會犯錯誤?記事本犯了怎樣的錯誤呢?也許你會迫不及待的想知道這些問題,如果是這樣,我不會讓你空腹而歸的。
????? 在簡體中文操作系統中默認的本地字符集編碼是GBK編碼,除非你在保存記事本文本文件時候選擇了其他編碼方式,否則用記事本錄入的字符信息將使用GBK編碼進行儲存。巧合的是,“聯通”這兩個字符的GBK編碼具有UTF-8編碼的特征,記事本犯下的錯誤正是將GBK編碼存放的記錄有“聯通”兩個字符的文件誤認為UTF-8編碼的文件。或許你會問,UTF-8編碼的文件不是以“EF BB BF”三個特殊字節開頭嗎?既然這樣,記事本怎么會犯這么低級的錯誤呢?沒錯,UTF-8編碼規定使用UTF-8編碼的文件以“EF BB BF”三個特殊字節開頭,但并不是強制性要求,早期的UTF-8編碼文件就不遵循這個規定。因此記事本不能依靠文件的開頭字節判斷一個文件是否是UTF-8編碼,而只能對文件中的數據進行簡單的編碼分析來確定。正是這個原因,才有了字符編碼應用中的這個奇怪又無法避免的現象。
????? 在簡體中文操作系統中默認的本地字符集編碼是GBK編碼,除非你在保存記事本文本文件時候選擇了其他編碼方式,否則用記事本錄入的字符信息將使用GBK編碼進行儲存。巧合的是,“聯通”這兩個字符的GBK編碼具有UTF-8編碼的特征,記事本犯下的錯誤正是將GBK編碼存放的記錄有“聯通”兩個字符的文件誤認為UTF-8編碼的文件。或許你會問,UTF-8編碼的文件不是以“EF BB BF”三個特殊字節開頭嗎?既然這樣,記事本怎么會犯這么低級的錯誤呢?沒錯,UTF-8編碼規定使用UTF-8編碼的文件以“EF BB BF”三個特殊字節開頭,但并不是強制性要求,早期的UTF-8編碼文件就不遵循這個規定。因此記事本不能依靠文件的開頭字節判斷一個文件是否是UTF-8編碼,而只能對文件中的數據進行簡單的編碼分析來確定。正是這個原因,才有了字符編碼應用中的這個奇怪又無法避免的現象。
[細節]
????? 如果上面的解釋對于你來說只是杯開胃紅酒,那我還是塊點把主食呈上吧,一份大峽谷香烤豬肋排。UTF-8編碼采用1-3個字節對字符進行編碼,編碼字節數與字符的Unicode編碼值有嚴格的對應關系,讓我們回憶下UTF-8編碼和Unicode的對應關系吧。
????? Unicode編碼值???????????????????????????? ?UTF-8編碼結構
????? \u0001 - \u007E?????????????????????????? 0XXXXXXX
????? \u0080 - \u07FF 和 \u0000?????????? ?110XXXXX 10XXXXXX
????? \u0800 - \uFFFF??????????????????????????? 1110XXXX 10XXXXXX 10XXXXXX
????? “聯通”這兩個字符的GBK編碼值是“C1 AA CD A8",GBK編碼方式使用兩個字節對一個字符進行編碼,因此以GBK編碼方式存放的錄有“聯通”兩個字符的文件的大小為四個字節。接下來分別觀察“聯通”這兩個字符GBK編碼值的二進制形式,你有發現有趣的事。
????? 聯??? GBK??? 十六進制:C1 AA??? 二進制:1100 0001,1010 1010
????? 通??? GBK??? 十六進制:C1 AA??? 二進制:1100 1101,1010 1000
????? 請注意上面二進制數據的著色部分,你想到了什么?對,它們和UTF-8編碼結構中的補充位完全一致,UTF-8編碼的補充位使得編碼值更有規律,而記事本剛好憑借這個特征區分UTF-8編碼的文件。存有“聯通”兩個字符的文件的所有數據都符合這個特征,就是這樣,記事本徹底的將文件誤認為UTF-8編碼的文件。
????? 將錯就錯,讓我們來看看這個錯誤是怎樣收場的。如果把“聯通”的GBK編碼值當作UTF-8編碼值,那文件就成為一個寫有數據“C1 AA CD A8”并以UTF-8編碼的文件,當使用記事本再次打開的時候會看到什么呢?只要將UTF-8編碼轉換成Unicode編碼就知道了。UTF-8編碼“C1 AA CD A8”轉換成Unicode編碼后,編碼值為“6A 00 68 03”(轉換方法請參考本Blog中的《字符編碼》一文)。0x006A這個Unicode編碼值位于\u0001 - \u007E之間,若要轉換為UTF-8編碼,顯然只能用一個字節進行編碼,因此“聯”的GBK編碼“C1 AA”雖然特征上貌似UTF-8編碼,但它卻不對應任何一個UTF-8編碼。接著看0x0368這個Unicode編碼值,這個值對應了字符“?”,這也正是我們將在記事本中看到的內容。或許你會說我看到的是一個黑色矩形啊,這只是字體的原因,你將字體改為宋體或者其他字體,看到的就是字符“?”。
????? 對于中文字符,UTF-8編碼要用三個字節進行編碼,因此,如果你使用記事本錄入“聯通”,然后選擇以UTF-8編碼方式保存的話,文件大小應為9個字節(包含三個字節的開頭數據),而同樣的文件GBK編碼卻是4個字節。最后附上“聯通”的GBK、UTF-8、Unicode編碼值,以及記事本的錯誤思維。
????? 聯通??GBK? C1 AA CD A8??? UTF-8? E8 81 94 E9 80 9A??? Unicode? 54 80 1A 90
????? 聯通? GBK? C1 AA CD A8??? UTF-8? C1 AA CD A8????????????Unicode? 6A 00 68 03? (將GBK值誤認為UTF-8值的結果)