標籤:

編碼歪傳

繼續上一篇。

身為一名Web開發者,這一篇將介紹一下在Web應用當中常會出現編碼問題的地方。文中經常會亂用「字符集」和「編碼」,不過看明白了第一篇的話相信你不會混淆概念,而且我個人覺得這兩個概念很多時候混淆也無妨……

概念

出於把問題描述得稍微清楚一點的目的,我打算先把我們的概念進行一下定義。

一般而言我們常遇到亂碼的場景有這樣兩種:

  1. 作為寫入端,我應該用什麼編碼來存儲/傳輸?
  2. 作為讀出端,我應該用什麼編碼來消費我所收到的位元組流?

因為我覺得絕大多數具體場景都可以歸納成上述兩種,所以這樣應該可以簡化一下問題。

程序內部處理

現代編程語言一般都內建字元串作為自帶的數據類型,一門強大且又實用的編程語言通常來說都有高效的字元串實現以及大量配套的字元串處理函數。

在上一篇中我們有順帶提到,UTF-16因為是一種在處理效率和存儲空間之間比較平衡的,同時編碼空間又足夠大的編碼方式,在一些編程語言當中被採用來當作字元串的內部編碼。比如C#、Java(可能因JVM/JDK而異)。

一般而言編程的String類型編碼都是固定的,但是通常會提供豐富的編碼轉換函數。一種(我認為)比較可靠的方式是:String用固定編碼方式實現,以使得標準的字元串函數能夠只關注一種編碼,從而保證它的正確性,也能夠最大程度地針對性優化;而通過使用類庫來將String轉換為特定編碼的位元組流,或將位元組流以特定編碼轉換成String。

反過來看,像PHP里的字元串就比較糙,它的編碼有很大問題,如果一個字元串是多位元組的(通過上一篇我們了解到除了ASCII以外基本上常用的編碼都是多位元組的),處理它就要用mb_xxxx系列的函數。這對編程是一種負擔,因為這樣就意味著String類型對字元的抽象力度不足,還是得花很多精力去關注字元串的編碼。對於PHP的程序一個辦法就是在整個程序內部統一編碼,同時基於此選擇好使用那一組字元串處理函數(作為項目規範),避免程序內部關心編碼的問題,只把編碼暴露在與外界交換數據的地方。

存儲/傳輸

管你是什麼程序,程序所生成的東西總要被消費才有意義(不然就變成烤機程序了)。Web應用里最常見的兩種對程序結果的消費方式,一是把它存儲(資料庫、文件)起來,二是把它傳輸給用戶(瀏覽器)以供展現。

當需要存儲/傳輸文本的時候,就需要高度關心字元編碼了。

存儲

很多人遇到的問題是把用戶表單提交的東西寫進MySQL裡面以後亂碼了,這個問題一些可能的原因有:

  1. 提交內容的字元編碼
  2. 服務端程序(如PHP)內部使用的編碼
  3. MySQL傳輸時候使用的編碼
  4. MySQL資料庫聲明和使用的字符集

第1點下一步會更詳細的展開。

第2點在上文當中有一定介紹了,PHP程序所接收的位元組流被當作字元串看待後,我們的程序必須要選擇合適的字元串處理函數,結果才會是對的。比如一個截斷程序要能正確處理多位元組編碼,如果把多位元組編碼切斷成「半個字元」嚴重的時候甚至會造成PHP出core。

第3點就是PHP中常見的mysqli_set_charset所覆蓋的範圍,沒錯,因為MySQL其實是服務,所以這個存儲其實也是傳輸。

第4點就是在建庫建表的時候選的那個字符集和編碼。

這當中的重點的就是2需要對1的編碼有預期,正確的把1的位元組流解析出來,轉換成程序內部字元串實現所使用的編碼,套用正確的演算法,接下來與MySQL驅動和服務之間使用雙方預期的編碼,最終以資料庫定義的時候所聲明的字符集保存下來。

傳輸

一個HTTP請求發出的時候,用戶代理(UserAgent,通常是瀏覽器)可以通過HTTP Request Header中的Accept-Charset欄位來顯式聲明預期返回的編碼,這是一種協商手段。現在的瀏覽器都很流弊,啥編碼都能解析,於是直接懶得發這個,言下之意就是服務端給返回什麼就消化什麼。

對於服務端而言,如果收到的請求指定了Accept-Charset那麼應該按照請求者的預期來決定響應內容的編碼,如果沒有指定,則可以「自由發揮」,這種時候理論上說你用什麼編碼都可以,但最終都必須通過某種手段告訴請求者響應內容是什麼編碼。

方式1:使用HTTP Response Header中Content-Type來給響應內容聲明編碼。比如Content-Type: text/html; charset=UTF-8。這裡有個小插曲,在IE6(沒記錯的話)里用Ajax請求的時候如果Response寫的是小寫utf-8就會跪,必須要大寫。別問我為什麼知道,說起來都是淚,那是一個風雨交加的深夜……

方式2:通過HTML頁面頭部的<meta charset="xxx">標籤來給頁面聲明編碼。如果Response Header里不寫編碼,瀏覽器就會嘗試找這個標籤,然後將接下來的內容以這個編碼解讀。這就是為什麼我們提倡將<meta charset="xxx">寫在<title>標籤之前的原因,如果<title>出現在此之前,它裡面的字元就不知道該用什麼編碼來解讀了,直觀的說就是可能造成title亂碼。

一旦決定了編碼,服務端程序就會將字元以該種編碼最終寫入位元組流,傳給客戶端。

那如果兩種方式都用了,口徑卻不一致會怎麼辦?首先當然是給開發者賞兩耳光,然後有興趣的可以做做實驗看看不同的瀏覽器會有什麼不同的兼容策略。

用戶提交內容

上面有說表單提交也有個編碼的問題,其實包括Ajax請求等,只要是客戶端向服務端發送內容,都一樣,但通過上面的例子我想你已經明白了,這完全是鏡像的,這次瀏覽器扮演著信息的生產者的角色,本質是完全一樣的。

消費

給你一本書,你怎麼知道它是中文版還是英文版?「我靠,它用英文寫的就是英文版,用中文寫的就是中文版啊。」

人類的大腦簡直聰明得要命了,這種問題根本不需要動腦子,計算機就要笨多了。其實並不是計算機笨,而是這個問題在計算機的領域裡面太難了。比如上一篇文章說到GB2312是兼容ASCII的,那麼如果收到的內容前幾個位元組是3C 68 74 6D 6C 3E也就是<html>的ASCII編碼,也許臆想它是ASCII的,於是後面出現的雙位元組字元可能就會遭殃了。UTF-8有一個很不錯的性質是它比較容易識別,但是也有錯誤率和效率問題。所以這些你猜來我猜去的不靠譜的倒霉事情就只讓它出現在男女情愛當中吧不要來污染我們純凈的計算機世界了好嗎。

上面一節當中有說到,一個靠譜的信息生產者,會在給你傳遞信息的時候協商或聲明編碼。身為一個合格的信息消費者,瀏覽器可以通過這些聲明來選擇正確的編碼,解讀位元組流。

瀏覽器也是個程序,於是它內部也會有字元串實現,也許它用自帶字元串的語言實現的,也許它用自己實現的字元串(如C/C++),不管怎樣,有了明確的編碼,瀏覽器都能夠將所獲得的位元組流轉換成自己所使用的內部編碼。

事已至此,似乎只要生產者靠譜,消費者要注意的問題就非常少了。在服務端我們小心翼翼地處理那麼多環節的編碼問題,到了瀏覽器好像已經完事兒了。不管這之前有再多波折,瀏覽器內部各種對字元的處理再多,基本上都不會有編碼的問題了,簡直太沒勁了,於是這裡稍微發散思維一下。

接下來瀏覽器就需要把字元顯示出來,我們考慮瀏覽器通過操作系統給它提供的API。API要麼規定編碼要麼協商/聲明編碼對吧,如果是前者,瀏覽器需要把自己內部用的編碼轉換成API所預期的編碼,然後調用API——在這個場景裡面,瀏覽器又從信息的消費者變成生產者了對吧,而這次操作系統是消費者。

然後我們假設操作系統將會用某種字體渲染這段字元,字體文件內部一般都對每個字元進行編號,現代的字體一般都會用Unicode,沒錯,我們又回到了字符集的概念。操作系統將字元編碼還原到字符集當中的字元編號(顯然對於變長位元組編碼這個過程要一些運算),在字體文件內通過編號查到這個字元,一個設計良好的字體可能對同一個字元會設計了多個字形(Glyph),比如Regular體一個、粗體一個、斜體一個,甚至還有更多更多,比如組合字元、一些特殊規則下的變形字元,不展開討論。

這些渲染規格都是在API里指定好的,然後就用對應的字形來進行渲染。渲染字形這事兒還不是一個簡單的事情,字體分點陣的、矢量的(甚至圖片的?),不同的渲染引擎,例如Windows上的GDI、DirectWrite、第三方的GDI++、MacType,還有OSX的渲染引擎,Linux不同的桌面系統的渲染引擎,在最終把字形繪製成像素點的演算法上有細節區別。

上面說的還只是渲染單個字元的時候的問題,在此之前還要做文字的排版啊什麼的,哪怕看起來很小一件事情也夠人鑽大半輩子了。我的天,人類為了在計算機上展示文字到底下了多少功夫?

好的好的,剛才似乎發散的太多了,就此打住,總之就瀏覽器而言對於一個HTML頁面的消費差不多是可以理解了。

階段性小結

把亂碼的問題從一個信息的生產者和消費者兩個角度來看,中間所經歷的哪些環節涉及到編碼,哪些環節涉及到編碼的協商與聲明,就明確多了。上面的例子其實很容易就可以舉一反三。

於是一些常見的諸如「PHPMyAdmin里看是正常的,頁面上是亂碼」或者「頁面上是正常的,PHPMyAdmin里看著是亂碼」這種問題可能會是哪些個環節闖的禍心裡就已經有譜了。對於各種介面,比如與MySQL通訊,比如與後端之間的介面,如何協商/聲明編碼,什麼時候需要轉換編碼,心裏面也有譜了。

預報

呵呵呵呵,這次的內容雖然沒那麼理論,但是還是太簡單了嘛,看到亂碼就查編碼唄你當我是傻X呢。

這時候也有觀眾吐槽:「那麼各種程序當中用的編碼比如URL Encode、Base64又是些啥玩意啊老濕?」

也有好奇心過盛的觀眾要問:「問號和方塊是怎麼回事?屯屯屯燙燙燙錕斤拷又是些什麼鬼呢老濕?」

對於上面的問題我只想說四個字:請聯繫我請看終篇:《編碼歪傳——番外篇》


推薦閱讀:

業力與大腦神經編碼記憶 腦可塑性-大腦皮層增大-記憶學習經歷固化 大腦重構 關鍵期MeCP2蛋白 中風大腦修復
應該學習編碼的5個理由
URL編碼與解碼
iOS KVO crash 自修復技術實現與原理解析

TAG:編碼 |