編碼歪傳 在Windows上做開發的同學,一定要選擇「使用UTF-8無BOM格式」保存

我保證這是最後一篇了,而且這次的內容絕對都是很具體的,具體得連每篇博客開頭例行的摘要我都不知道該寫什麼了!

典型亂碼亂碼、問號、方塊

用文本編輯器打開一個文件,如果編碼不兼容,有時候會看到??????的東西,有時候會看到一團亂七八糟的文字,通常我們就統稱亂碼了。怎麼用編碼的知識來理解呢?

前文中我們有說到實用的很多編碼方式都用的是變長位元組編碼,很多位元組都要結合它的上下文去解釋才是對的。例如:用UTF-8的演算法去解析GBK的文件,就很容易發些這麼些種情況:

  1. 一個位元組序列並不是合法的UTF-8字元,比如以11111110開頭的位元組序列。
  2. 一個位元組序列碰巧符合UTF-8規則。

反過來看,用GBK的演算法去解析UTF-8的文件其實也差不多,遇到第一種情況在顯示的時候可能就用問號代替,而遇到第二種情況就是出現一些風馬牛不相及的雜亂文字。

方塊其實和問號本質上一樣的,但方塊在現代瀏覽器里還有個很常見的情況,就是一個字元的編號在字體當中並沒有定義,於是在排版和渲染的適合「智能」地用一個方塊來表示它了。看到方塊可以結合上下文,如果上下文當中的非英字元顯示正確的,那麼方塊可能是一些特殊符號,比如Emoji。

在寫服務端程序的時候要小心處理「半個字元」的問題,例如我們在前級對超長的數據進行截斷處理,剛好截斷掉一個變長編碼的位元組序列,就會出現「半個字元」。一般半個字元都是鐵定會亂碼,一些容錯比較差的程序甚至會掛,比如一些做的不好的PHP的C擴展,嚴重的時候會出core。所以程序不懂編碼就別瞎截,甚至考慮到某些語言文字里的組合字元,就是知道編碼也別瞎截(真是細思恐極);

BOM

BOM就是Browser Object Model瀏覽器對象模型,不好意思拿錯劇本了。

BOM(Byte-Order Mark,位元組序標記)是Unicode碼點U+FEFF。它被定義來放在一個UTF-16文件的開頭,如果位元組序列是FEFF那麼這個文件就是大端序,如果位元組序列是FFFE那麼這個文件就是小端序。

UTF-8本身是沒有位元組序的問題的(因為它是以單個位元組為最小單位),但是Windows裡面很多編輯器(比如記事本)會多此一舉的在UTF-8文件開頭加入EF BB FF也就是U+FEFF的UTF-8編碼。

如果你的PHP文件裡面有一個這東西你就倒了大霉了,可能會:

  • 什麼也看不見,可能是PHP引擎根本處理不了這個源代碼。
  • 頁面展現錯亂的情況,一般是因為在<doctype>之前輸出的非空格內容造成了瀏覽器選擇錯誤的doctype。
  • 頁面上面有及格亂七八糟的字元,瀏覽器把它當字元展示出來了。
  • 於是建議在Windows上做開發的同學,一定要選擇「使用UTF-8無BOM格式」保存,所以用記事本寫代碼裝X就不好使了,用Notepad++的可以注意選一下,它支持的文件編碼格式挺豐富的,用一些比較先進的跨平台編輯器比如WebStorm、SublimeText它們都是沒BOM的。

    錕斤拷

    亂碼之所以叫亂碼,就是因為它是「亂」的。但是亂碼當中最出名的就是「錕斤拷」,他出現次數太多了以至於看起來根本就沒那麼「亂」。這就納了悶了,為什麼全中國的網站亂碼裡面都會有這個?

    原因是,在將一些國家語言編碼體系,比如GB、BIG-5、EUC-JP等,轉換為Unicode的過程中,多少有一些字元是不在Unicode中的(比如一些偏旁部首在Unicode里是後來才收錄的),甚至它本身在原來的編碼體系裡面就是非法字元的情況。

    Unicode規定了U+FFFD當作一個佔位符用來表示這些字元,用UTF-8編碼它就是EF BF BD,連續多個這樣的位元組序列出現就成了EF BF BD EF BF BD。如果是一個UTF-8的解析程序還好,而如果用一個GB的解析程序去打開,一個漢字2位元組,就成了「錕斤拷」。這裡就是一個例子,用UTF-8編碼打開是問號,用GBK編碼打開的話就會看到錕斤拷,用hexdump或者UltraEdit這類任何16進位編輯器看的話就能看到裡面都是EF BF BD

    要避免錕斤拷一個重要的點就是盡量減少程序當中的編碼轉換。比如輸入是UTF-8,但是一個舊的模塊是GBK,把UTF-8轉成GBK交給舊的模塊處理,處理過程中舊模塊多多少少有些BUG的可能,再轉回來的時候就容易錕斤拷了。一個項目的源代碼在團隊裡面被不同的人(他們編輯器配置不盡相同)開來開去,存來存去,也很容易出現錕斤拷。

    燙燙燙、屯屯屯

    這個和編碼轉換其實沒啥關係,在VC的DEBUG模式下,會把未初始化的棧內存全部填成0xCC,未初始化的堆內存填成0xCD,這樣做是讓你一眼就能看出來你開了內存沒初始化。

    而用GBK編碼的話,CC CC就是「燙」,CD CD就是「屯」。

    URL Encode和Base64URL Encode

    URL Encode又稱為「百分號編碼」它主要用來在URI裡面將特殊字元進行轉義,因為像/&=等等這類字元在URI裡面本身是有功能性的。

    對於ASCII字元的編碼很簡單就是用%後跟ASCII編碼的16進位表示,例如/的ASCII char code是47,16進位表示是2F,於是它的URL Encode結果就是%2F

    對於非ASCII字元,將它的每個位元組進行相同規則的轉換,例如中文「編碼」的Unicode char code是U+7F16 7801,UTF-8編碼的位元組序列是E7 BC 96 E7 A0 81,所以它按照UTF-8編碼的URL Encode結果就是%E7%BC%96%E7%A0%81

    可以看出,URL Encode編碼非ASCII字元的時候,結果與使用的字元編碼有關。因此在頁面上提交表單、發起Ajax請求等操作的時候需要注意編碼。瀏覽器會按照當前頁面所使用的字元編碼對錶單體提交進行URL Encode,但使用JavaScript的encodeURIencodeURIComponent的時候則總是會使用UTF-8(參考MDN)。

    表單提交的時候編碼是非常非常重要的,一旦錯了服務端解開數據的時候就會跪。比如Github在它們的搜索表單裡面放了一個<input name="utf8" type="hidden" value="?">,其中那個對鉤?是U+2713,UTF-8編碼是E2 9C 93,他們可以在服務端檢測這個參數的值對不對從而對URL里用的編碼進行一個初步檢測。雖然我沒有看到他們使用其他編碼的情況,不過這樣也算是一個編碼協商和Check的手段吧。

    在JavaScript中使用escape也可以達到URL Encode的效果,但是它對於非ASCII字元使用了一種非標準的的實現,例如「編碼」會被escape%u7F16%u7801這種%uxxxx奇怪的表示,W3C把這個函數廢棄了,身為一名前端還用是打臉的哦。

    Base64

    Base64是一種用可見字元表示二進位數據的方法。它用了64個可見字元[A-Za-z0-9+/]

    Base64的編碼程序非常簡單,由於64=2^6,6和8的最小公倍數是24,也就是3byte,因此對輸入數據以3byte為一個單位,查表把它轉換成4個可見字元。

    如果輸入末尾不足3byte,那就補足,補1個byte就在輸出末尾添加一個=,補2個byte同理。

    Base64經常用來在一些文本協議裡面保存二進位數據,比如HTTP協議,或者電子郵件的附件啊什麼的。同時因為它的輸出對於人類而言不可讀,可以起到一些「混淆加密」的作用,事實上就有修改64個字元的排布來做一個變形Base64實現一個簡單加密演算法的例子。從密碼學的角度看它基本上沒什麼強度可言,但是足夠簡單,可以起到防君子不防小人的作用。

    由於一個字元只能編碼6bit,自身卻佔了8bit,8/6=1.33,因此使用Base64來表示數據的時候會浪費1/3的體積。對於在CSS裡面用Base64的data-url方式表示圖片,用之前不妨簡單估算一下,膨脹的體積和一個HTTP請求頭比起來會相差多少,說不定漲太多了已經損失掉省一個請求的收益了。

    尾聲

    終於整個系列都要結束了,理論的也好,實用的也好,基本上我覺得該說的都說了,要是以後再遇到亂碼,一定會很快知道問題所在。

    最後還是要佩服並感謝一下ISO和Unicode聯盟,做了這麼偉大的事情將全世界的語言文字統一收錄和編碼,而這當中包括了那麼多我們根本沒聽說過的奇怪的語言文字。正是因為他們的努力奠定了互聯網是一個無國界的世界,每天我們都能通過它獲得來自任何地方任何語言的信息。

    哦,我上面說的不是某國的互聯網。


    推薦閱讀:

    墓碑的格式及碑文書寫
    五七言格律詩的四個基本平仄格式是什麼?
    2-0. 基本的命盤格式
    論律絕詩的格式化
    寫信的格式大全

    TAG:選擇 | UTF8 | 編碼 | 格式 | 保存 | Windows |