簡析Windows Notepad裏可選的字符編碼

羣裏 @lizheming 問到了Windows Notepad(記事本)中保存文件的編碼選項都是什麼意思……php

這篇文章就簡單測試一下Windows Notepad的行爲。html

Windows Notepad的可選編碼
▲ Windows Notepad的編碼包含ANSI、Unicode、Unicode big endian和UTF-8。程序員

警告

本文僅僅闡述一個普遍使用的軟件的技術事實,不表明做者支持或反對使用該軟件。
事實上做者推薦任什麼時候候都不使用 Windows Notepad 來處理計算機程序代碼。
本文僅在某一個簡體中文版64位Windows 7的實例下驗證,僅供參考。不保證在其餘相同或相異系統下可以重現一致的結果。segmentfault

注意

本文嚴格區分Unicode的編碼字節序列化
Unicode的編碼僅指使用數(一般寫成16進制數)來一對一的表明字符的工做。這個數的範圍僅受Unicode標準的約束,與計算機毫無關聯。
Unicode的字節序列化指爲了可以寫入計算機存儲器,而把一個Unicode標準範圍內的數,表示成N個字節的工做。瀏覽器

測試用例

測試用例爲:「錕斤拷【斷行】a【斷行】」。(錕斤拷是一種信仰。)函數

全部字符的GBK和Unicode編碼爲:測試

  • 錕 GBK=EFBF Unicode=U+951F
  • 斤 GBK=BDEF Unicode=U+65A4
  • 拷 GBK=BFBD Unicode=U+62F7

如下ASCII字符的GBK和Unicode編碼與ASCII一致:google

a=0x61 CR=0x0D LF=0x0A
(Windows一個換行符佔有兩個字符:CR+LF)編碼

ANSI

在簡體中文系統下,ANSI就是中華人民共和國國家標準定義的GBK編碼。spa

Windows Notepad使用ANSI存儲這個文件的結果以下:

EF BF  BD EF  BF BD  0D  0A  61  0D  0A
-----  -----  -----  --  --  --  --  --

簡單的使用GBK編碼存儲了全部的字符。最高位不是1的單字節並等同於ASCII,不然雙字節。

這裏要注意字節序(Endian)的問題[注A]。能夠看到這裏的字節序是大端在先(big-endian)的。

可是沒必要特地強調「大端在先的GBK」——由於從GB2312開始,標準就規定了存儲方式是大端在先的[注B]。後來的GBK和GB18030-2000向下兼容。

ANSI的麻煩就是依賴系統——其餘語言系統的ANSI就不是GBK了,打開GBK的文件必然亂碼。而且GBK的字符集自己也過小。
(千萬不要說「我只用中文」——少了Unicode那些符號,網上那些顏文字都打不出來)

Unicode系列

Windows Notepad所說的「Unicode」、「Unicode big endian」和UTF-8,全都是一樣的Unicode編碼的不一樣的字節序列化存儲方法。

UTF-16 和 BOM

這裏的Unicode指UTF-16[注C]。UTF-16是極其簡單粗暴的序列化方法——絕大多數的Unicode字符都在U+0000~U+FFFF的範圍內[注D],那就每一個字符用兩個字節,把Unicode編碼的原始值寫盤。

注意ASCII字符也必須浪費一倍的空間存儲高8位的0x00——由於若是把高8位的0略了,解析時就再也沒有其餘的依據去斷字。

對於UTF-16就存在大端和小端的問題了——UTF-16並不規定字節的大端在前仍是小端在前。但UTF-16並不包含表示字節序的信息,總不能人工看看哪一個解析是不亂碼的吧……

Unicode提供的解決方式是,把一個零寬無斷字空格符U+FEFF ZERO WIDTH NO-BREAK SPACE)以UTF-16的方式序列化以後,塞到文件的最前邊。這樣UTF-16解析器讀取文件的前兩個字節,若是是FE FF就是大端在前,FF FE就是小端在前。

這個塞進去的東西就叫BOM(Byte Order Mark,字節順序標記)。

值得一提的是,零寬無斷字空格符也經常使用於充當1個有效字符,破拆各類場合的字數限制。包括SegmentFault的問答和評論內容在內。

記事本的「Unicode」和「Unicode big endian」

單寫「Unicode」,根本就不是一種存儲方法的完整表達。由於這隻包含編碼而沒有字節序列化

M$出現這種錯誤,我一點都不以爲奇怪。死記結論就能夠了:Windows Notepad的「Unicode」就是UTF-16

Windows Notepad使用「Unicode」 = 小端在先的UTF-16,存儲這個文件的結果以下:

FF FE 1F 95 A4 65 F7 62 0D 00 0A 00 61 00 0D 00 0A 00
 -BOM- ----- ----- ----- ----- ----- ----- ----- ----- 
U+FEFF  951F  65A4  62F7  000D  000A  0061  000D  000A <--Unicode原始值

Windows Notepad使用「Unicode big endian」 = 大端在先的UTF-16,存儲這個文件的結果以下:

FE FF 95 1F 65 A4 62 F7 00 0D 00 0A 00 61 00 0D 00 0A
 -BOM- ----- ----- ----- ----- ----- ----- ----- ----- 
U+FEFF  951F  65A4  62F7  000D  000A  0061  000D  000A <--Unicode原始值

UTF-8

UTF-8是一種用1~4個字節表示1個Unicode字符的變長的字節序列化方法。具體的實現細節看這篇文章。UTF-8的好處在於:

  1. 不管是IETF的推薦,仍是實際業界的執行,UTF-8都是互聯網的標準。
  2. 向下兼容,ASCII字符UTF-8序列化後還是原樣,任何ASCII文件也是有效的UTF-8文件。
  3. 沒有字節序問題。UTF-8的字節序是由RFC3629定死的。

Windows Notepad使用UTF-8存儲這個文件的結果以下:

EF BB BF  E9 94 9F  E6 96 A4  E6 8B B7  0D   0A   61   0D   0A
 --BOM---  --------  --------  --------  --   --   --   --   --
U+ FEFF      951F      65A4      62F7   000D 000A 0061 000D 000A <--Unicode原始值

注意UTF-8前邊仍然塞進去了U+FEFF按照UTF-8序列化的結果EF BB BF,做爲前邊提到過的BOM字節順序標記。Windows Notepad存儲的UTF-8,是帶有BOM標記的UTF-8

可是若是僅僅對於UTF-8而言,字節序是沒有意義的。由於UTF-8的字節序被規範寫死,U+FEFF編碼後必然獲得EF BB FF,得不出其餘的。沒有二義性,BOM就失去了本來的意義。也許只有區別UTF-8文件和UTF-16文件的用處……

如何對待UTF-8文件的BOM,RFC3629的第6章有詳細的規定,不加詳述。

值得一提的是,BOM我想不少PHP程序員都經歷過而且恨之入骨——PHP不認識文件中的BOM頭並會將其做爲HTTP Response的正文送出。這甚至在無緩衝的狀況下,會致使header()等必須在Response開始前執行的函數直接失效。

因此PHP程序員老是會喜歡UTF-8 without BOM的編碼方式——這基本也就宣佈了Windows下的PHP開發,Windows Notepad徹底的淘汰出局,哪怕是任何一星半點代碼的臨時修改。

番外:Notepad++的字符編碼測試

ANSI沒有區別,但Notepad++支持選擇多國編碼的不一樣ANSI編碼方式(相似瀏覽器裏選編碼),能夠輕鬆生成或讀取Shift-JIS等其餘字符集的文件。適合用於對付日文老遊戲的README等文檔。

UCS-2 Big Endian、UCS-2 Little Endian和前邊UTF-16的兩個例子一致。注意UTF-16的文件不提供「無BOM」的存儲方法(提供了就壞了)。

UTF-8仍然表明「帶有BOM標記的UTF-8」。但同時提供PHP程序員最愛的UTF-8 without BOM,就像:

E9 94 9F  E6 96 A4  E6 8B B7  0D   0A   61   0D   0A
 --------  --------  --------  --   --   --   --   --
U+ 951F      65A4      62F7   000D 000A 0061 000D 000A <--Unicode原始值

Simple and clean.

註解
[注A] 對於一個雙(多)字節的數,必定會按8位截斷爲1字節後寫盤。那麼寫盤時先寫最低8位仍是先寫最高8位,就是所謂的「字節序」(Endian)問題。例如,數0x01020304寫盤時,是先寫最低8位的04 03 02 01,仍是先寫最高8位的01 02 03 04
先寫低8位的叫作小端在先(little-endian),先寫高8位的叫作大端在先(big-endian)。實際採用何種字節序受系統環境、標準規範和軟件實際編寫的多方面控制,不一律而論。
[注B] 字節序若是我沒弄錯,是GB2312採用的EUC字符編碼方法控制的。
[注C] 本文並不嚴格區分UTF-16UCS-2
[注D] Unicode的最大值實際上達到了U+10FFFF,超出了兩個字節可以存儲的限度。
但Unicode因爲歷史緣由,留下了U+D800~U+DFFF這一段永久保留不用的空缺區域。
所以對U+10000及以上的字符,UTF-16藉助了這部分空缺區域,對這些編碼超大的字符打破2字節16位的慣例,特別的用4字節32位去表示之。
這一部分編碼值太大的字符,超出了GBK的字符集範圍,所以本文將徹底忽略。若有機會再進一步測試。

相關文章
相關標籤/搜索