從一個小故事聊聊字符編碼那些事

聯通不如移動的故事

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

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

在Windows系統下,在桌面上右鍵新建一個記事本文件,打開它輸入「聯通」兩個漢字,Ctrl+S保存並關閉。java

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

好吧,再次新建一個文件,這回輸入「移動」保存再試試。神奇,移動竟然完美顯示。函數

好了,不說什麼故事了,這個有趣的現象正是爲了聊聊計算機中「編碼」的那些事,以後再解釋爲何「聯通不如移動」。學習

聊聊字符編碼的發展史

在計算機中,全部存儲的數據都由二進制表示。字母、數字、字符這些都不例外,計算機中最小的單位就是二進制位(0和1),8個位表示一個字節,所以8個二進制位就能夠排列組合出256種狀態,也就是理論上能夠表示出256種字符,而由哪些二進制位表示哪些字符,這就是由人來決定的了,也就是人們制定出的各類「編碼」。編碼

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

ASCII碼

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

又把全部的空格、標點符號、數字、大小寫字母分別用連續的字節狀態表示,一直編到了第 127 號,這樣計算機就能夠用不一樣字節來存儲英語的文字了。ip

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


這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來保存,現次打開「聯通」終於顯示了。

這個例子很極端,能夠說「聯通」二字的編碼正好是個巧合,可是搞明白了編碼的細節,更有助於咱們在開發中遇到問題能夠快速理解其實質,並加以解決,在此記下筆記,與你們共同窗習提升。

相關文章
相關標籤/搜索