確定文本文件的編碼

jopen 9年前發布 | 19K 次閱讀 編碼

在上一篇中,探討了文件名編碼以及非文本文件中的文本內容的編碼,在這里,將介紹更為重要的文本文件的編碼。

混亂的現狀

設想一下,如果在保存文本文件時,也同時把所使用的編碼的信息也保存在文件內容里,那么,在再次讀取時,確定所使用的編碼就容易多了。

很多的非文件文件比如圖片文件通常會在文件的頭部加上所謂的“magic number(魔法數字)”來作為一種標識。所謂的“magic number”,其實它就是一個或幾個固定的字節構成的固定值,用于標識文件的種類(類似于簽名)。比如bmp文件通常會以“42 4D”兩字節開頭。

又比如Java的class文件,則是以四字節的“ca fe ba be”打頭。(咖啡寶貝?)

確定文本文件的編碼 

即便沒有文件后綴名,根據這些信息也是確定一個文件類型的手段。

沒有編碼信息

那么,對于文本文件,有沒有這樣的好事呢?可以簡單建立一個文本文件“foo.txt”,里面輸入兩個簡單的字符,比如“hi”,保存,然后再查看文件的大小屬性

確定文本文件的編碼

然后,我們很遺憾地發現,大小只有2,也即“hi”兩個字符的大小,這意味著沒有保存額外的所用編碼的信息。

用十六進制形式查看,也可以發現這兩個字節就是hi兩字符的編碼:

確定文本文件的編碼

關于字母的ASCII編碼,可查看深入圖解字符集與字符集編碼(八)——ASCII和ISO-8859-1

那么,現在很清楚了,文本文件僅僅是內容的字節序列,沒有其它額外的信息。

BOM?

當然,說絕對沒有額外信息也不完全正確,在之前的關于BOM的介紹中,我們看到BOM其實可以看成是一種額外的信息。

參見深入圖解字符集與字符集編碼(七)——BOM

保持內容不變,簡單地“另存為”一下,在編碼一欄選擇“UTF-8”,再次查看屬性將會發現大小變成了5.

確定文本文件的編碼

再次查看十六進制形式時,就會發現除了原來的“68 69”外,還多出了UTF-8的BOM:“ef bb bf”

確定文本文件的編碼

也正是以上三個與內容無關的字節使得大小變成了5.

這個信息是與所用編碼有關的,不過它僅能確定與Unicode相關的編碼。

嚴格地說,BOM的目的是用于確定字節序的。

另一方面,對于UTF-8而言,現在通常不建議使用BOM,因為UTF-8的字節序是固定的,所以很多的UTF-8編碼的文本文件其實是沒有BOM的,不能簡單地認為沒有BOM就不是UTF-8了。

比如eclipse中生成的UTF-8文件默認就是不帶BOM的。微軟的筆記本應該是比較特殊的情況。

綜上所述,文本文件通常沒有一個特殊的頭部信息來供確定所用的編碼,另一方面,編碼的種類又是五花八門,那么如何去確定編碼呢?

確定編碼的步驟

不妨就以記事本為考察對象,去探究一下它是如何確定編碼的。

利用BOM

前面說了,BOM作為一種額外的信息,間接地表明了所使用的編碼。盡管它原本的意圖是要指明字節序,但曲線救國一下也未必不可。況且記事本還主動地為UTF-8也寫入了BOM,不加以利用這一信息自然是不明智的。

注:對UTF-16來說,BOM是必須的,因為它是存在字節序的,弄反了字節序一個編碼就會變成另一個編碼了,那就徹底亂套了。不過一般很少用UTF-16編碼來保存文件的,更多是在內存中使用它作為一種統一的編碼。

但對于UTF-8,很多時候也是沒有BOM的,記事本遇到UTF-8 without BOM時又該怎么辦呢?

我猜,我猜,我猜猜

如果內容中沒有編碼信息,又要去確定它使用的編碼,這不是為難人是什么?好在“坑蒙拐騙”中的第二招“蒙”可以拿來用用。

“蒙”其實也是要講點技術含量的,簡單點自然就是是模式匹配了,或許一個或幾個正則式就完了;復雜點,什么概率論,統計學,大數據統統給它弄上去,那逼格立馬就高了有木有?當然了,記事本也就是一跑龍套的...

記事本跟“聯通”有仇?

在編碼界有這么一個傳說:記事本跟“聯通”有仇。這是怎么一回事呢?

新建一個文本文件“test.txt”,錄入兩個漢字“聯通”,保存,關閉程序然后再次打開這一文件:

確定文本文件的編碼

咦,這是什么鬼?咱們的“聯通”呢?

深入分析

這其實就是競猜失敗的結果了,準確地講,記事本把編碼給猜成了UTF-8.

為什么說是猜成UTF-8造成的呢?我也沒見過源碼!接下來會根據出現的現象,已有的證據來作出我們的推論,然后還會做些實驗去驗證。(沒錯,這就是科學!)

首先是這樣一個事實:當我們保存時,使用的是缺省編碼,也就是GBK。

“聯通”兩字的GBK編碼如下:

確定文本文件的編碼

然后,是這樣一個現象:再次打開時,記事本突然就翻臉不認人了,顯示出了一些奇怪的字符。

嚴格地講,有三個字符,兩個問號及一個C一樣的字符,后面會分析為何會這樣。

以上字節咋一看也沒啥子特別的,領頭字節也沒有恰好等于UTF-16或UTF-8的BOM,絕對標準的GBK模式,為啥記事本對它另眼相看了呢?

關于GBK等編碼,可參見深入圖解字符集與字符集編碼(九)——GB2312,GBK,GB18030

那么,一個合理的猜測就是記事本可能把它當成了無BOM的UTF-8編碼。

而對于UTF-8編碼來說,它的編碼模式還是很有自己特色的,那么我們換成二進制形式查看以上編碼:

確定文本文件的編碼

看到以上編碼,相信對UTF-8編碼模式有一些了解的都知道是怎么回事了,我也用顏色的下標標注了關鍵的部分。

在前面的篇章中,也曾經幾次說到過UTF-8的編碼模式:

確定文本文件的編碼

深入圖解字符集與字符集編碼(三)——定長與變長深入圖解字符集與字符集編碼(四)——Unicode

對比一下,不難發現上述兩個編碼完全符合二字節模式!也難怪記事本犯迷糊了。

自然,要做出一些“科學”的發現,你還是需要一定的基礎的。所以在這里也給出了很多前面文章的鏈接。

顯示疑云

那么,為何又顯示成了那樣的效果呢?

既然推斷它是UTF-8編碼,讓我們人肉解碼一下:

前兩字節構成一組:11000001 10101010

有效的碼位有三段:11000001 10101010

重組成碼點之后是:00000000 01101010

以上碼點寫成16進制是U+006A,這明明是一個一字節的碼點!對應的字母其實是小寫字母“j”。

所以,這里其實是有錯誤的。這個碼點不應該用二字節來編碼。

如果你讀過前面關于Unicode的篇章,就會明白,對于UTF-8編碼而言,碼點在U+0000~U+007F(0-127)間的用一字節模式編碼。碼點在U+0080~U+07FF(128-2047)間的才用二字節模式編碼。

別問為什么!這叫烏龜的屁股——龜腚(規定)。

理論上講,二字節的空間是完全可以囊括一字節編碼的那些碼點的,各種模式間其實是有重疊與冗余的。但如果一個碼點適用于更少字節,那么它應該優先用更少字節的編碼模式。

所以,通常說UTF-8的二字節模式是“110x xxxx, 10xx xxxx”,但并非所有滿足這些模式的編碼都是合法的UTF-8二字節編碼。這里面其實是有個坑的。二字節首個碼點為U+0080,對應二字節模式首字節 為“1100 0010”,那么,所有合法的二字節模式首字節不應該小于此值。

顯然,親愛的微軟的寫記事本的程序員們,你們偷懶了!你們至少應該可以避免與“聯通”結仇。

那么,雖然能夠解出相應的碼點,但其實是非法的組合,這也就是結果顯示出“?”的原因。這通常是一個明顯的解釋失敗的標志。

注:“?”本身是一個合法的Unicode字符,碼點為U+FFFD,對應的UTF-8編碼為:EF BF BD。如果字體不支持的話,可參見http://www.fileformat.info/info/unicode/char/fffd/index.htm

確定文本文件的編碼

這可不是“顯示不出來”造成的,它本身就長這樣。

這個碼點的含義為“replacement character”(替換字符)。當碰到非法的字節時,顯示系統就用它來替換,然后顯示這個替換字符來表示發生了替換。所以,那些真正“顯示不出來”的 東西已經被替換了。(如果文件中本身就包含這個字符的話,替換上去的和原有的實際是無法區分的。)

在這里,程序實際臨時在內存中做了替換,如果你對它進行拷貝,得到的也將是它的值,而不是原來的值。

所以,顯示層面出現了問號(包括早期的ASCII中那個問號“?”,U+001A,也常用作替換字符),不代表它不清楚如何顯示,而完全是因為最終交給它去渲染的就是“問號”字符。(可能是替換上去的,也可能本身就有的。)

在顯示層面,原來的值實際已經丟失了。

至于為何顯示了兩個問號,大概是把兩個字節當作了兩次失敗。個人認為顯示一個問號也能說得通。

再來看第二組

后兩字節構成一組:11001101 10101000

有效的碼位有三段:11001101 10101000

重組成碼點之后是:00000011 01101000

以上碼點寫成16進制是U+0368,對應的字母其實是所謂的COMBINING LATIN SMALL LETTER C?(<--這里第二個C就是U+0368)。見“http://www.fileformat.info/info/unicode/char/0368/index.htm

顯示時,它會緊貼在前面的字母上,這是Unicode中個人感覺比較奇葩的一些內容,如果你有興趣,這是wiki的一些介紹http://en.wikipedia.org/wiki/Combining_character

前面亂碼的截圖中,那個怪怪的C就是這樣來的,感覺好像與前面一個問號是一體的一樣。

確定文本文件的編碼

“聯通”這一案例還真多梗。

至此,冤有頭,債有主,一切都水落石出。最終我們的猜測被證實。

直接證據!

其實,在變成怪怪的字符后,如果我們點擊“另存為”,在彈出的對話框中會發現編碼成了“UTF-8”

確定文本文件的編碼

這是赤裸裸的證據,直接表明了記事本把編碼誤判成了UTF-8!簡直是鐵證如山呀!

打破模式

刪掉test.txt,從新再建立,這次把聯通的好基友“電信”也一起錄入,錄入“聯通電信”四個字,保存并再次打開時記事本就不再抽瘋了,因為“電信”兩字的GBK編碼模式與UTF-8不能匹配了,讀者可自行驗證一下,這里就不再貼圖展示了。

注:不要直接刪除原來的亂碼字符并重新錄入,前面“另存為”已經表明它已經成了UTF-8編碼,直接在原文件修改將導致以UTF-8編碼保存。所以應該刪除原文件。

有句話叫“無巧不成書”,記事本跟“聯通”有仇,這其實就是一個因為樣本太少而誤判的典型例子。

缺省編碼,ANSI是個什么玩意

如果既沒有BOM,又無法猜測出所使用的編碼,那是否就只能是兩眼一抹黑了呢?

還好,計算機世界還有件貼心的小棉襖叫“缺省”。

其實,當你保存任何一個文本文件時,指定一個編碼是必不可少的一個步驟。

與此類似,讀取一個文本文件時,或者說是比如Java中new一個Reader字符流時,又或者是string.getBytes時,你其實都是需要指定一個編碼的。

但很多時候,我們并沒有感覺到需要這一步驟,原因就是“缺省”在為我們默默地服務。

缺省這玩意,怎么說它好呢?當它正常時,你好,我好,大家好。當它不正常時,你甚至不知道哪兒出錯了。你過于依賴它,它很可能成為你的定時炸彈。不得不說,很多時候我們其實是抱著炸彈在擊鼓傳花,還玩得不亦樂乎,直到“轟”的一聲,咦?頭上什么時候多了個圈?

以記事本為例,當我們新建一個文件并保存時,其實是有個選項的,通常,這里會缺省地選上“ANSI”

確定文本文件的編碼

那么這里的ANSI又是個什么鬼呢?

ANSI(American National Standards Institute美國國家標準學會)與它字面的意思并不相符,它也不是一種真正意義上的編碼。

通常把它理解成平臺缺省編碼,它具體指代什么則通常與平臺所在地區的Windows發行版本有關。

像我們這些火墻內的大陸人民,多數人用的Windows版本,ANSI指的是GBK;在香港臺灣地區,它可能是Big5;在一些歐洲地區,它則可能是ISO-8859-1。

除了ANSI之外,在這里還有其它的選項。

其實在這里短短的一個下拉列表,處處都是坑呀,說多了都是淚。

1. ANSI,前面已說,就不說了。

2. Unicode。其實是UTF-16,具體地講是UTF-16 little endian(UTF-16 LE)。這個缺省為Little Endian也僅是微軟平臺的缺省。其它平臺未必是如此。

3. Unicode big endian。與之前類似,就是UTF-16 big endian(UTF-16 BE)。Unicode現在的含義太寬泛,可以指Unicode字符集,可以指Unicode碼點,也可以指整個Unicode標準。現在看來,把 UTF-16繼續叫成Unicode實在是很坑爹,除了容易引發誤解,我還真沒想到它還能有什么其它好處~

4. UTF-8.其實是“帶BOM的UTF-8”,而真正推薦的缺省做法是“不帶BOM”。微軟就是任性!

還需要注意的是,不同的操作系統對于缺省有不同的策略。

比如現在很多的Linux的操作系統都把UTF-8當成了缺省的編碼,無論你在什么地區都是如此。這對于減少混亂還是有幫助的。

因為文件內容沒有編碼的信息,各個系統平臺對于缺省的規定又各不相同,種種情況導致了亂碼問題層出不窮,下一篇,將探討引入編碼信息的一些實踐。

來自:http://my.oschina.net/goldenshaw/blog/413412

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!