從一個故事開始聊聊字元編碼

恍然小悟發表於2019-01-17

聯通不如移動的故事

在編碼界一直流傳著聯通不如移動的一個故事。。。

請不要誤會,聯通和移動和本篇文章所說的編碼確實沒什麼關係,但請出聯通和移動幫忙做個小實驗,再來仔細說說編碼。

在Windows系統下,在桌面上右鍵新建一個記事本檔案,開啟它輸入“聯通”兩個漢字,Ctrl+S儲存並關閉。

從一個故事開始聊聊字元編碼

雙擊再次開啟它,看到了什麼?奇怪,文字怎麼變成亂碼了?

從一個故事開始聊聊字元編碼

好吧,再次新建一個檔案,這回輸入“移動”儲存再試試。神奇,移動居然完美顯示。

從一個故事開始聊聊字元編碼

好了,不說什麼故事了,這個有趣的現象正是為了聊聊計算機中“編碼”的那些事,之後再解釋為什麼“聯通不如移動”。

聊聊字元編碼的發展史

在計算機中,所有儲存的資料都由二進位制表示。字母、數字、字元這些都不例外,計算機中最小的單位就是二進位制位(0和1),8個位表示一個位元組,因此8個二進位制位就可以排列組合出256種狀態,也就是理論上可以表示出256種字元,而由哪些二進位制位表示哪些字元,這就是由人來決定的了,也就是人們制定出的各種“編碼”。

電腦這種東西最早由老外發明,外國人使用的英語只有26個字母,再加上標點、數字和一些符號也不會太多,因此英文通常用ASCII編碼來表示。

ASCII碼

ASCII碼最開始只在美國使用,組合出的256種狀態中,第0~32中規定了特殊用途,一旦終端、印表機遇上約定好的這些位元組被傳過來時,就要做一些約定的動作,比如遇到0×10, 終端就換行等等。

又把所有的空格、標點符號、數字、大小寫字母分別用連續的位元組狀態表示,一直編到了第 127 號,這樣計算機就可以用不同位元組來儲存英語的文字了。

記得當初學習C語言的時候,就清楚的知道了一些常用的ASCII碼值,比如大寫A是65,小寫a是97等。

從一個故事開始聊聊字元編碼
這128個符號(包括32個不能列印出來的控制符號),只佔用了一個位元組的後面7位,最前面的一位統一規定為0。

英文可以表示了,但是世界上除了英文還有很多語言。我們的中文文字浩如煙海,僅僅靠這8個二進位制位遠遠不夠,怎麼辦?

GB2312

且不說中文,在歐洲有些國家的語言中也有一些特殊的字母,比如俄文希臘文等。於是便使用127號之後的空位繼續表示他們的字母。當然,由於每個國家的語言不同,就越來越亂,比如130在法語中是字母 é,但是在希伯萊語中130卻是他們的字母 ג。

我們的中文就更難辦了,即使把所有的位都用上,也表示不完成千上萬的漢字,於是我們自己也制定了一套中文的編碼GB2312。

中國為了表示漢字,把127號之後的符號取消了,規定:

  • 一個小於127的字元的意義與原來相同,但兩個大於 127 的字元連在一起時,就表示一個漢字;
  • 前面的一個位元組(他稱之為高位元組)從0xA1用到0xF7,後面一個位元組(低位元組)從 0xA1 到 0xFE;
  • 這樣我們就可以組合出大約7000多個(247-161)*(254-161)=(7998)簡體漢字了。
  • 還把數學符號、日文假名和ASCII裡原來就有的數字、標點和字母都重新編成兩個字長的編碼。這就是全形字元,127以下那些就叫半形字元。 把這種漢字方案叫做 GB2312。GB2312 是對 ASCII 的中文擴充套件。

GBK

再後來,發現了GB2312雖然解決了中文編碼的問題,但是仍有不足。

GB2312表示的中文有時不夠,有些字並不是生僻字,但是沒有收錄其中,當時有個小插曲,我當時在高考報名的系統中查詢成績的時候報不出我的名字,只能報出我的姓,正是因為我的名字“玥”字不在GB2312的編碼範圍,因此沒有。

於是乾脆不再要求低位元組一定是 127 號之後的內碼,只要第一個位元組是大於 127 就固定表示這是一個漢字的開始,又增加了近 20000 個新的漢字(包括繁體字)和符號。

這就是更全面的GBK編碼。

Unicode

隨著發展,每個國家都對自己的語言編出一套自己的編碼,真是混亂不堪,我們不知道別人用什麼編碼,別人也不知道我們用什麼編碼,於是標準組織出手了。

ISO標準組織看到了亂象,制定了一套Unicode編碼以解決這種混亂的局面,它的制定簡單粗暴,不是全世界的語言多麼,我乾脆就規定,所有的字元都給我用兩個位元組表示(兩個8位一共16位),對於 ASCII 裡的那些 半形字元,Unicode 保持其原編碼不變,只是將其長度由原來的 8 位擴充套件為16 位,而其他文化和語言的字元則全部重新統一編碼。

從 Unicode 開始,無論是半形的英文字母,還是全形的漢字,它們都是統一的一個字元。同時,也都是統一的兩個位元組。

UTF8

Unicode的制定是在1990年,正式使用在1994年,那個年代在現在來看簡直是遠古時期,那時由於網際網路並不發達並沒有推廣開。

隨著網際網路的發展,為了解決Unicode傳輸問題,於時面向眾多的UTF標準出現了。

  • UTF-8 就是在網際網路上使用最廣的一種 Unicode 的實現方式
  • UTF-8就是每次以8個位為單位傳輸資料
  • 而UTF-16就是每次 16 個位
  • UTF-8 最大的一個特點,就是它是一種變長的編碼方式
  • Unicode 一箇中文字元佔 2 個位元組,而 UTF-8 一箇中文字元佔 3 個位元組
  • UTF-8 是 Unicode 的實現方式之一

因為UTF8是Unicode的實現方式之一,它們之間是互通的,就是說Unicode編碼可以傳換為UTF8,它有一套對應規則:

Unicode符號範圍(16進位制) UTF8編碼(2進位制)
0000 0000-0000 007F 0xxxxxxx
0000 0080-0000 07FF 110xxxxx 10xxxxxx
0000 0800-0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

可以看到,對於單位元組的符號,位元組的第一位設為0,後面7位為這個符號的 Unicode 碼。因此對於英語字母,UTF-8 編碼和 ASCII 碼是相同的(見上面表格的第一行)。

對於n位元組的符號(n>1),第一個位元組的前n位都設為1,第n+1位設為0,後面位元組的前兩位一律設為10。剩下的沒有提及的二進位制位,全部為這個符號的 Unicode 碼。

說的有些抽象,舉個例子吧,比如來了一個漢字,電腦是怎麼知道的它是用UTF8編碼的呢?

因為漢字用三個位元組表示(別再問為什麼用三個位元組表示了,這是規定),因此第一個位元組的前三位都為1,第四位設為0,後面的位都以10開頭,所以它肯定長這個樣子:1110xxxx 10xxxxxx 10xxxxxx。

OK,電腦按照這個規則一看明白了,來的是個漢字!

不如再舉個例子,從Unicode編碼表中查出一個漢字對應的編碼,把它轉換為UTF8試一試,就用我的名字“玥”字吧,它的Unicode編碼為\u73a5

首先第一步把16進位制轉換為2進位制,它的值是111001110100101,那怎麼拆分這個2進位制的值呢?因為UTF8都是後6位為這個字元的Unicode的碼,所以我們從右往左數6位給一一對應上,不足的位補0就好了。

從一個故事開始聊聊字元編碼
這樣就得出了“玥”字的UTF8編碼:11100111 10001110 10100101

作為開發人員完全可以用程式碼實現一下,這裡用node.js真實的實現一下轉碼:

function transferToUTF8(unicode) {
  code = [1110, 10, 10];

  let binary = unicode.toString(2); //轉為二進位制

  code[2] = code[2] + binary.slice(-6); //提取後6位
  code[1] = code[1] + binary.slice(-12, -6); //提取中間6位
  code[0] = code[0] + binary.slice(0, binary.length - 12).padStart(4, '0'); //取剩餘開始的位,不夠補0

  code = code.map(item => parseInt(item, 2)); //把字串轉換為二進位制數值

  return Buffer.from(code).toString(); //利用Buffer轉轉為漢字
}

console.log(transferToUTF8(0x73a5));
複製程式碼

執行結果:

複製程式碼

以上程式碼定義了一個transfer函式,引數接收一個16進位制值,它代表了一個Unicode字元,transfer函式內部先轉換為二進位制,並按照UTF-8的規則轉換為相應的UTF-8編碼,最後,利用node.js的Buffer最終轉碼成漢字,可以看到,已經正確輸出了漢字“玥”。

以上,就是簡單分析了Unicode和UTF-8的轉換關係。

為什麼聯通不如移動?

故事就要講完了,說了這麼多編碼的事現在可以回頭看看開篇為什麼聯通變成了亂碼,因為在Windows的記事本中文預設的儲存編碼為GB2312,通過查詢可以查到漢字“聯”對應的GB2312編碼為\uc1aa,轉換為二進位制是1100000110101010,正好是16位兩個位元組,按8位拆成兩組正好與UTF8的第二種編碼格式對應上了:110xxxxx 10xxxxxx,這樣再次開啟記事本的時候Windows掃描檔案內容,它就會認為這是UTF-8編碼的檔案,而不是GB2312!此時此刻按照UTF-8來解析檔案內容當然出現了亂碼。

這時可以重新另存為檔案,把檔案格式改為GB2312來儲存,現次開啟“聯通”終於顯示了。

這個例子很極端,可以說“聯通”二字的編碼正好是個巧合,但是搞明白了編碼的細節,更有助於我們在開發中遇到問題可以快速理解其實質,並加以解決,在此記下筆記,與大家共同學習提高。

相關文章