从一个小故事聊聊字符编码那些事
聯通不如移動的故事
在編碼界一直流傳著聯通不如移動的一個故事。。。請不要誤會,聯通和移動和本篇文章所說的編碼確實沒什么關系,但請出聯通和移動幫忙做個小實驗,再來仔細說說編碼。
在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,它有一套對應規則:
| 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'); //取剩余開始的位,不夠補0code = 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來保存,現次打開“聯通”終于顯示了。
這個例子很極端,可以說“聯通”二字的編碼正好是個巧合,但是搞明白了編碼的細節,更有助于我們在開發中遇到問題可以快速理解其實質,并加以解決,在此記下筆記,與大家共同學習提高。
總結
以上是生活随笔為你收集整理的从一个小故事聊聊字符编码那些事的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: P678-vect2.cpp
- 下一篇: 基于IP访问控制的局限性