編碼歪傳——番外篇

Source : http://jimliu.net/2015/03/07/something-about-encoding-extra/

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

典型亂碼

亂碼、問號、方塊

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

前文中我們有說到實用的很多編碼方式都用的是變長位元組編碼,很多位元組都要結合它的上下文去解釋才是對的。例如:用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和Base64

URL 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聯盟,做了這麼偉大的事情將全世界的語言文字統一收錄和編碼,而這當中包括了那麼多我們根本沒聽說過的奇怪的語言文字。正是因為他們的努力奠定了網際網路是一個無國界的世界,每天我們都能通過它獲得來自任何地方任何語言的資訊。

哦,我上面說的不是某國的網際網路。

“編碼歪傳——番外篇” 有 1 則評論

  1. Source: https://www.zhihu.com/question/20167122

    受邀。早知道上得山多終遇老虎,在@梁海

    老兄面前耍Unicode總會有這一天的⋯⋯

    首先,BOM是啥。這個就不解釋了,Wikipedia上很詳細。http://en.wikipedia.org/wiki/Byte_order_mark。

    在網頁上使用BOM是個錯誤。BOM設計出來不是用來支援HTML和XML的。要識別文字編碼,HTML有charset屬性,XML有encoding屬性,沒必要拉BOM撐場面。雖然理論上BOM可以用來識別UTF-16

    編碼的HTML頁面,但實際工程上很少有人這麼幹。畢竟UTF-16這種編碼連ASCII都雙位元組,實在不適用於做網頁。

    其實說BOM是個壞習慣也不盡然。BOM也是Unicode標準的一部分,有它特定的適用範圍。通常BOM是用來標示Unicode純文字位元組流
    的,用來提供一種方便的方法讓文字處理程序識別讀入的.txt檔案是哪個Unicode編碼(UTF-8,UTF-16BE,UTF-16LE)。Windows相對對BOM處理比較好,是因為Windows把Unicode識別程式碼整合進了API裡,主要是CreateFile()。打開文字檔時它會自動識別並剔除BOM。Windows用這個有歷史原因,因為它最初脫胎於多字碼頁的環境。而引入Unicode時Windows的設計者又希望能在使用者不注意的情況下同時相容Unicode和非Unicode(Multiple byte)文字檔,就只能借助這種小trick了。相比之下,Linux這樣的系統在多locale的環境中浸染的時間比較短,再加上社區本身也有足夠的動力輕裝前進(吐槽:微軟對相容性的要求確實是到了非常偏執的地步,任何一點破壞相容性的做法都不允許,以至於很多時候是自己綁住自己的雙手),所以乾脆一步到位進入UTF-8。當然中間其實有一段過渡期,比如從最初全UTF-8的GTK+2.0發佈到基本上所有GTK開發者

    都棄用多locale的GTK+1.2,我印象中至少經歷了三到四年。

    BOM不受歡迎主要是在UNIX環境下,因為很多UNIX程序不鳥BOM。主要問題出在UNIX那個所有指令碼語言
    通行的首行#!標示,這東西依賴於shell解析,而很多shell出於相容的考慮不檢測BOM,所以加進BOM時shell會把它解釋為某個普通字元輸入導致破壞#!標示,這就麻煩了。其實很多現代指令碼語言,比如Python,其直譯器本身都是能處理BOM的,但是shell卡在這裡,沒辦法,只能躺著也中槍。說起來這也不能怪shell,因為BOM本身違反了一個UNIX設計的常見原則,就是文件中存在的資料必須可見。BOM不能作為可見字元被文字編輯器

    編輯,就這一條很多UNIX開發者就不滿意。

    順便說一句,即使指令碼語言能處理BOM,隨處使用BOM也不是推薦的辦法。各個指令碼語言對Unicode的處理都有自己的一套,Python的 # -*- coding: utf-8 -*-,Perl的use utf8,都比BOM簡單而且可靠。另一個好消息是,即使是必須在Windows和UNIX之間切換的朋友也不會悲催。幸虧在UNIX環境下我們還有VIM這種神器,即使遇到BOM擋道,我們也可以通過 set nobomb; set fileencoding=utf8; w 三條命令解決問題。

    最後回頭想想,似乎也真就只有Windows堅持用BOM了。

    P.S.:本問題是自己的第150個回答。突然發現自己回答得很少很少⋯⋯

    P.S. 2:突然想起需要解釋一下為什麼說VIM去除bomb的操作需要在UNIX下完成。因為VIM在Windows環境下有一個奇怪的bug,總是把UTF-16檔案
    識別成二進制檔案,而UNIX(Linux或者Mac都可以)下VIM則無問題

    。這個問題從VIM 6.8一直跟著我到VIM 7.3。目前尚不清楚這是VIM的bug還是我自己那個.vimrc檔案的bug。如有高手解答不勝感激。
    編輯於 2012-04-10 23:42
    知乎使用者
    知乎使用者

    网页编程中用不用bom我就不说什么了,因为软件原因无法使用的就更不能用了。

    最近在学习用cocos2d-x,纯C++的编码,如果代码中有中文等的非ascii字符出现。发现会出错。代码是在mac 下用xcode 写的,放到windows 下用vs 编译。

    最后把所有的源文件转成了带bom的格式后编译通过了,链接失败,这想这个就不是编码的问题了。

    通常情况下,一般都 会认为在写C++代码的时候不要用中文,但是很多时候我们程序员也有想自己看着舒服的时候,为神马就不能写中文了?

    于是在windows 下写了一个helloworld.cpp 类型的文件,输出内容用中文,然后存为utf-8 带bom格式,再把它copy到mac 下用g++ 编译,发现成功通过并且可正常运行,用xcode打开源文件也正常显示。

    所以,这里建议程序要在windows 和 mac 还有linux 上运行的话,源代码最好保存成utf-8 带bom的格式,这样比较通用一些。而用utf-16 无论大端还是小端,g++ 都不认的。或者用utf-8 不带bom格式,然后代码不要出现非ascii 127以后的字符。

    关于说utf-8 不带bom 才是标准的,我想应该是带用个人情绪的说法吧。真正的标准应该是bom是可选的,为什么可选?因为有些时候不带bom会出错,就拿历史较久远的windows来讲吧,很多国家的用户都在用windows ,其文件都是用其本地的ansi 编码来做的,比如大陆的GBK和GB2013,港台的big5,这些编码因为针对当地所用的字符制定的,所以呢,其存储文件较小,所以会大量使用,并且也大量存在着,微软不可能不考虑全球几十亿的用户的文件而盲目地修改解码方式,并且微软也是unicode 制定者之一,所以,带用bom的utf-8也是符合国际标准的。

    或许是因为程序编写者的个人原因,也许是考虑到效率,很多的程序无法正确区分一个utf-8文件是否有bom,所以导致了各种乱码的出现。

    个人不想说哪个是标准,也不想用语言去攻击哪个公司或团体。微软在坚持使用bom上没有错,因为这是在为用户考虑的。也许给我们这些写程序的带来了不便,但是,计算机最广泛的用户不是程序员。
    發佈於 2014-03-11 16:53
    Jim Liu
    Jim Liu

    前端開發話題下的優秀答主

    UTF-8因為它的編碼特性
    ,是位元組序

    無關的,所以不需要BOM。

    我覺得“帶BOM的UTF-8”這個鍋基本上WINDOWS還是要背的,儘管我不太確定“UTF-8檔案是否可以帶BOM”這個問題,但整因為它不需要,於是很多跨平台

    的軟體其實並不支援這種格式。
    編碼歪傳——番外篇

    BOM是什麼,有興趣可以看看我這篇流水賬


    發佈於 2016-05-31 21:22
    知乎使用者
    知乎使用者

    機器學習話題下的優秀答主

    就是帶頭的鵝和去頭的鵝,有些編輯器

    比較傻會把去頭的鵝認成鴨子…
    發佈於 2014-10-21 11:44
    QR Code of Downloading Zhihu App
    下載知乎客戶端
    與世界分享知識、經驗和見解
    相關問題
    nodejs如何將utf-8模式下的字元進行gbk2312的url編碼? 0 個回答
    已經用htonl將變數轉成網路序了,為什麼再此用htonl轉這個變數的時候,結果按理說應該不變吧? 2 個回答
    u-net網路中contact層是什麼意思,copy&crop只是簡單地將兩個矩陣組合在一起嗎? 2 個回答
    在做一個高吞吐量的中文web系統的時候,大家會如何處理utf8存放字串會比其它編碼佔用出50%空間的問題呢? 7 個回答
    相關推薦
    live
    譚浩強《C 程式設計》(第 4 版)章節專項練習及詳解
    0 人讀過​
    閱讀
    live
    Visual Basic 程式設計教學
    24 人讀過​
    閱讀
    live
    編寫高性能的.NET 程式碼
    沃森
    130 人讀過​
    閱讀

    幫助中心
    知乎隱私保護指引
    聯絡我們

    舉報中心
    涉未成年舉報
    網路謠言舉報
    涉企虛假舉報

    關於知乎
    下載知乎
    知乎招聘
    知乎指南
    知乎協議
    京 ICP 證 110745 號 · 京 ICP 備 13052560 號 – 1 · 京公網安備 11010802020088 號 · 京網文[2022]2674-081 號 · 藥品醫療器械網路資訊服務備案(京)網藥械資訊備字(2022)第00334號 · 廣播電視節目製作經營許可證:(京)字第06591號 · 服務熱線:400-919-0001 · Investor Relations · © 2023 知乎 北京智者天下科技有限公司版權所有 · 違法和不良資訊舉報:010-82716601 · 舉報信箱:[email protected]
    本站提供適老化無障礙服務
    登录即可查看 超5亿 专业优质内容
    超 5 千万创作者的优质提问、专业回答、深度文章和精彩视频尽在知乎。

    回覆

發表評論