返回列表 發帖

[教學] 編碼歪傳——番外篇 (上集)

本帖最後由 Laputa 於 2023-8-2 11:25 編輯

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

Blog版 :https://laputa.eu.org/archives/82

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

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

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

一個位元組序列並不是合法的UTF-8字元,比如以11111110開頭的位元組序列。
一個位元組序列碰巧符合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的encodeURI和encodeURIComponent的時候則總是會使用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聯盟,做了這麼偉大的事情將全世界的語言文字統一收錄和編碼,而這當中包括了那麼多我們根本沒聽說過的奇怪的語言文字。正是因為他們的努力奠定了網際網路是一個無國界的世界,每天我們都能通過它獲得來自任何地方任何語言的資訊。

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

RE: 編碼歪傳——番外篇 (下集)

本帖最後由 Laputa 於 2023-8-2 09:51 編輯

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”這個問題,但整因為它不需要,於是很多跨平台

的軟體其實並不支援這種格式。

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

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

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

比較傻會把去頭的鵝認成鴨子…

TOP

返回列表