字元編碼詳解:從ASCII到UTF-8
為什麼你的文字有時會變成問號和亂碼。一份關於字元編碼的實用指南。
你開啟一個檔案,看到的是ü而不是u。或者資料庫在應該顯示名字的地方回傳????。又或者收到一封郵件,主旨裡散布著=?UTF-8?B?。
歡迎來到字元編碼問題的精彩世界。
簡短的歷史
電腦儲存的是數字,不是字母。所以必須有人決定哪個數字代表哪個字母。1960年代,ASCII將0-127的編號分配給英文字母、數字和基本符號。字母「A」是65。空格是32。很簡單。
但ASCII只涵蓋128個字元。對英文夠用。對德語變音符號、日語漢字、阿拉伯文字,或者人類實際使用的成千上萬其他字元,就不夠了。
Unicode之前的混亂
不同地區發明了各自的編碼。西歐有ISO-8859-1。日本有Shift-JIS。俄羅斯有KOI8-R。中國有GB2312。每種在自己的生態系統內都運作良好。一旦混用,一切都崩潰了。
用一種編碼儲存、用另一種編碼開啟的檔案會產生亂碼——那種你可能見過的錯誤字元大雜燴。UTF-8檔案被當作ISO-8859-1讀取時,cafe會變成café。
Unicode解決了對映問題
Unicode給每個字元一個唯一的編號(稱為「碼位」)。拉丁字母A是U+0041。雪人是U+2603。每個表情符號、每種文字系統、每個數學符號都有自己的碼位。超過15萬個字元,還在持續增長。
但Unicode只是對映。它不說明如何將這些數字儲存為位元組。那是編碼的工作。
UTF-8:勝出的編碼
UTF-8是網際網路大部分儲存Unicode文字的方式。關鍵技巧:每個字元使用可變數量的位元組。
- ASCII字元(英文字母、數字):每個1位元組
- 歐洲帶重音字元:每個2位元組
- 亞洲字元(CJK):每個3位元組
- 表情符號和稀有符號:每個4位元組
這意味著UTF-8中的英文文字與ASCII完全相同。舊系統不會出問題。但你仍然可以表示世界上任何字元。
目前超過98%的網站使用UTF-8。編碼之戰已經結束,UTF-8獲勝了。
UTF-8 vs UTF-16 vs UTF-32
UTF-8: 可變寬度(1-4位元組)。對英文為主的文字高效。Web標準。
UTF-16: 可變寬度(2或4位元組)。JavaScript、Java和Windows內部使用。每個字元至少2位元組,所以對ASCII文字效率較低。
UTF-32: 固定寬度(每字元4位元組)。簡單但浪費。很少用於儲存或傳輸。
JavaScript的string.length計算的是UTF-16程式碼單元,不是字元。所以"😀".length回傳2,不是1。
當編碼出問題時
用錯誤的編碼讀取檔案。 位元組本身沒問題,但被錯誤地解釋了。解決方案:開啟時指定正確的編碼。
資料庫字元集不匹配。 應用程式傳送UTF-8,但資料庫欄位設定為latin1。ASCII範圍外的字元被損壞。解決方案:將資料庫設為utf8mb4(MySQL中不是utf8,那個只處理3位元組字元)。
HTTP缺少charset標頭。 如果伺服器不傳送Content-Type: text/html; charset=utf-8,瀏覽器只能猜。有時猜錯。
實用建議
始終使用UTF-8。 除非有非常具體的理由不用,UTF-8是所有場景的正確選擇。
明確宣告編碼。 HTML中:<meta charset="utf-8">。HTTP中:Content-Type: text/html; charset=utf-8。不要讓系統去猜。
注意字串長度。 在JavaScript中,字元計數遇到表情符號和組合字元時會變得複雜。使用Array.from(str).length或Intl.Segmenter API來取得準確計數。
注意BOM。 位元組順序標記(U+FEFF)有時出現在UTF-8檔案開頭。它不可見但可能導致解析器出錯。某些編輯器會默默添加它。
字元編碼不是什麼令人興奮的話題,但理解它能省下數小時的除錯時間。到處使用UTF-8,明確宣告它,當你看到亂碼時,就會準確知道該去哪裡找原因。