从一个小故事聊聊字符编码那些事
聯(lián)通不如移動(dòng)的故事
在編碼界一直流傳著聯(lián)通不如移動(dòng)的一個(gè)故事。。。請(qǐng)不要誤會(huì),聯(lián)通和移動(dòng)和本篇文章所說的編碼確實(shí)沒什么關(guān)系,但請(qǐng)出聯(lián)通和移動(dòng)幫忙做個(gè)小實(shí)驗(yàn),再來仔細(xì)說說編碼。
在Windows系統(tǒng)下,在桌面上右鍵新建一個(gè)記事本文件,打開它輸入“聯(lián)通”兩個(gè)漢字,Ctrl+S保存并關(guān)閉。
雙擊再次打開它,看到了什么?奇怪,文字怎么變成亂碼了?
好吧,再次新建一個(gè)文件,這回輸入“移動(dòng)”保存再試試。神奇,移動(dòng)居然完美顯示。
好了,不說什么故事了,這個(gè)有趣的現(xiàn)象正是為了聊聊計(jì)算機(jī)中“編碼”的那些事,之后再解釋為什么“聯(lián)通不如移動(dòng)”。
聊聊字符編碼的發(fā)展史
在計(jì)算機(jī)中,所有存儲(chǔ)的數(shù)據(jù)都由二進(jìn)制表示。字母、數(shù)字、字符這些都不例外,計(jì)算機(jī)中最小的單位就是二進(jìn)制位(0和1),8個(gè)位表示一個(gè)字節(jié),因此8個(gè)二進(jìn)制位就可以排列組合出256種狀態(tài),也就是理論上可以表示出256種字符,而由哪些二進(jìn)制位表示哪些字符,這就是由人來決定的了,也就是人們制定出的各種“編碼”。
電腦這種東西最早由老外發(fā)明,外國(guó)人使用的英語(yǔ)只有26個(gè)字母,再加上標(biāo)點(diǎn)、數(shù)字和一些符號(hào)也不會(huì)太多,因此英文通常用ASCII編碼來表示。
ASCII碼
ASCII碼最開始只在美國(guó)使用,組合出的256種狀態(tài)中,第0~32中規(guī)定了特殊用途,一旦終端、打印機(jī)遇上約定好的這些字節(jié)被傳過來時(shí),就要做一些約定的動(dòng)作,比如遇到0×10, 終端就換行等等。
又把所有的空格、標(biāo)點(diǎn)符號(hào)、數(shù)字、大小寫字母分別用連續(xù)的字節(jié)狀態(tài)表示,一直編到了第 127 號(hào),這樣計(jì)算機(jī)就可以用不同字節(jié)來存儲(chǔ)英語(yǔ)的文字了。
記得當(dāng)初學(xué)習(xí)C語(yǔ)言的時(shí)候,就清楚的知道了一些常用的ASCII碼值,比如大寫A是65,小寫a是97等。
這128個(gè)符號(hào)(包括32個(gè)不能打印出來的控制符號(hào)),只占用了一個(gè)字節(jié)的后面7位,最前面的一位統(tǒng)一規(guī)定為0。
英文可以表示了,但是世界上除了英文還有很多語(yǔ)言。我們的中文文字浩如煙海,僅僅靠這8個(gè)二進(jìn)制位遠(yuǎn)遠(yuǎn)不夠,怎么辦?
GB2312
且不說中文,在歐洲有些國(guó)家的語(yǔ)言中也有一些特殊的字母,比如俄文希臘文等。于是便使用127號(hào)之后的空位繼續(xù)表示他們的字母。當(dāng)然,由于每個(gè)國(guó)家的語(yǔ)言不同,就越來越亂,比如130在法語(yǔ)中是字母 é,但是在希伯萊語(yǔ)中130卻是他們的字母 ?。
我們的中文就更難辦了,即使把所有的位都用上,也表示不完成千上萬的漢字,于是我們自己也制定了一套中文的編碼GB2312。
中國(guó)為了表示漢字,把127號(hào)之后的符號(hào)取消了,規(guī)定:
- 一個(gè)小于127的字符的意義與原來相同,但兩個(gè)大于 127 的字符連在一起時(shí),就表示一個(gè)漢字;
- 前面的一個(gè)字節(jié)(他稱之為高字節(jié))從0xA1用到0xF7,后面一個(gè)字節(jié)(低字節(jié))從 0xA1 到 0xFE;
- 這樣我們就可以組合出大約7000多個(gè)(247-161)*(254-161)=(7998)簡(jiǎn)體漢字了。
- 還把數(shù)學(xué)符號(hào)、日文假名和ASCII里原來就有的數(shù)字、標(biāo)點(diǎn)和字母都重新編成兩個(gè)字長(zhǎng)的編碼。這就是全角字符,127以下那些就叫半角字符。
把這種漢字方案叫做 GB2312。GB2312 是對(duì) ASCII 的中文擴(kuò)展。
GBK
再后來,發(fā)現(xiàn)了GB2312雖然解決了中文編碼的問題,但是仍有不足。
GB2312表示的中文有時(shí)不夠,有些字并不是生僻字,但是沒有收錄其中,當(dāng)時(shí)有個(gè)小插曲,我當(dāng)時(shí)在高考報(bào)名的系統(tǒng)中查詢成績(jī)的時(shí)候報(bào)不出我的名字,只能報(bào)出我的姓,正是因?yàn)槲业拿帧矮h”字不在GB2312的編碼范圍,因此沒有。
于是干脆不再要求低字節(jié)一定是 127 號(hào)之后的內(nèi)碼,只要第一個(gè)字節(jié)是大于 127 就固定表示這是一個(gè)漢字的開始,又增加了近 20000 個(gè)新的漢字(包括繁體字)和符號(hào)。
這就是更全面的GBK編碼。
Unicode
隨著發(fā)展,每個(gè)國(guó)家都對(duì)自己的語(yǔ)言編出一套自己的編碼,真是混亂不堪,我們不知道別人用什么編碼,別人也不知道我們用什么編碼,于是標(biāo)準(zhǔn)組織出手了。
ISO標(biāo)準(zhǔn)組織看到了亂象,制定了一套Unicode編碼以解決這種混亂的局面,它的制定簡(jiǎn)單粗暴,不是全世界的語(yǔ)言多么,我干脆就規(guī)定,所有的字符都給我用兩個(gè)字節(jié)表示(兩個(gè)8位一共16位),對(duì)于 ASCII 里的那些 半角字符,Unicode 保持其原編碼不變,只是將其長(zhǎng)度由原來的 8 位擴(kuò)展為16 位,而其他文化和語(yǔ)言的字符則全部重新統(tǒng)一編碼。
從 Unicode 開始,無論是半角的英文字母,還是全角的漢字,它們都是統(tǒng)一的一個(gè)字符。同時(shí),也都是統(tǒng)一的兩個(gè)字節(jié)。
UTF8
Unicode的制定是在1990年,正式使用在1994年,那個(gè)年代在現(xiàn)在來看簡(jiǎn)直是遠(yuǎn)古時(shí)期,那時(shí)由于互聯(lián)網(wǎng)并不發(fā)達(dá)并沒有推廣開。
隨著互聯(lián)網(wǎng)的發(fā)展,為了解決Unicode傳輸問題,于時(shí)面向眾多的UTF標(biāo)準(zhǔn)出現(xiàn)了。
- UTF-8 就是在互聯(lián)網(wǎng)上使用最廣的一種 Unicode 的實(shí)現(xiàn)方式
- UTF-8就是每次以8個(gè)位為單位傳輸數(shù)據(jù)
- 而UTF-16就是每次 16 個(gè)位
- UTF-8 最大的一個(gè)特點(diǎn),就是它是一種變長(zhǎng)的編碼方式
- Unicode 一個(gè)中文字符占 2 個(gè)字節(jié),而 UTF-8 一個(gè)中文字符占 3 個(gè)字節(jié)
- UTF-8 是 Unicode 的實(shí)現(xiàn)方式之一
因?yàn)閁TF8是Unicode的實(shí)現(xiàn)方式之一,它們之間是互通的,就是說Unicode編碼可以傳換為UTF8,它有一套對(duì)應(yīng)規(guī)則:
| 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 |
可以看到,對(duì)于單字節(jié)的符號(hào),字節(jié)的第一位設(shè)為0,后面7位為這個(gè)符號(hào)的 Unicode 碼。因此對(duì)于英語(yǔ)字母,UTF-8 編碼和 ASCII 碼是相同的(見上面表格的第一行)。
對(duì)于n字節(jié)的符號(hào)(n>1),第一個(gè)字節(jié)的前n位都設(shè)為1,第n+1位設(shè)為0,后面字節(jié)的前兩位一律設(shè)為10。剩下的沒有提及的二進(jìn)制位,全部為這個(gè)符號(hào)的 Unicode 碼。
說的有些抽象,舉個(gè)例子吧,比如來了一個(gè)漢字,電腦是怎么知道的它是用UTF8編碼的呢?
因?yàn)闈h字用三個(gè)字節(jié)表示(別再問為什么用三個(gè)字節(jié)表示了,這是規(guī)定),因此第一個(gè)字節(jié)的前三位都為1,第四位設(shè)為0,后面的位都以10開頭,所以它肯定長(zhǎng)這個(gè)樣子:1110xxxx 10xxxxxx 10xxxxxx。
OK,電腦按照這個(gè)規(guī)則一看明白了,來的是個(gè)漢字!
不如再舉個(gè)例子,從Unicode編碼表中查出一個(gè)漢字對(duì)應(yīng)的編碼,把它轉(zhuǎn)換為UTF8試一試,就用我的名字“玥”字吧,它的Unicode編碼為\u73a5
首先第一步把16進(jìn)制轉(zhuǎn)換為2進(jìn)制,它的值是111001110100101,那怎么拆分這個(gè)2進(jìn)制的值呢?因?yàn)閁TF8都是后6位為這個(gè)字符的Unicode的碼,所以我們從右往左數(shù)6位給一一對(duì)應(yīng)上,不足的位補(bǔ)0就好了。
這樣就得出了“玥”字的UTF8編碼:11100111 10001110 10100101
作為開發(fā)人員完全可以用代碼實(shí)現(xiàn)一下,這里用node.js真實(shí)的實(shí)現(xiàn)一下轉(zhuǎn)碼:
function transferToUTF8(unicode) {code = [1110, 10, 10];let binary = unicode.toString(2); //轉(zhuǎn)為二進(jìn)制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'); //取剩余開始的位,不夠補(bǔ)0code = code.map(item => parseInt(item, 2)); //把字符串轉(zhuǎn)換為二進(jìn)制數(shù)值return Buffer.from(code).toString(); //利用Buffer轉(zhuǎn)轉(zhuǎn)為漢字 }console.log(transferToUTF8(0x73a5));運(yùn)行結(jié)果:
玥以上代碼定義了一個(gè)transfer函數(shù),參數(shù)接收一個(gè)16進(jìn)制值,它代表了一個(gè)Unicode字符,transfer函數(shù)內(nèi)部先轉(zhuǎn)換為二進(jìn)制,并按照UTF-8的規(guī)則轉(zhuǎn)換為相應(yīng)的UTF-8編碼,最后,利用node.js的Buffer最終轉(zhuǎn)碼成漢字,可以看到,已經(jīng)正確輸出了漢字“玥”。
以上,就是簡(jiǎn)單分析了Unicode和UTF-8的轉(zhuǎn)換關(guān)系。
為什么聯(lián)通不如移動(dòng)?
故事就要講完了,說了這么多編碼的事現(xiàn)在可以回頭看看開篇為什么聯(lián)通變成了亂碼,因?yàn)樵赪indows的記事本中文默認(rèn)的保存編碼為GB2312,通過查詢可以查到漢字“聯(lián)”對(duì)應(yīng)的GB2312編碼為uc1aa,轉(zhuǎn)換為二進(jìn)制是1100000110101010,正好是16位兩個(gè)字節(jié),按8位拆成兩組正好與UTF8的第二種編碼格式對(duì)應(yīng)上了:110xxxxx 10xxxxxx,這樣再次打開記事本的時(shí)候Windows掃描文件內(nèi)容,它就會(huì)認(rèn)為這是UTF-8編碼的文件,而不是GB2312!此時(shí)此刻按照UTF-8來解析文件內(nèi)容當(dāng)然出現(xiàn)了亂碼。
這時(shí)可以重新另存為文件,把文件格式改為GB2312來保存,現(xiàn)次打開“聯(lián)通”終于顯示了。
這個(gè)例子很極端,可以說“聯(lián)通”二字的編碼正好是個(gè)巧合,但是搞明白了編碼的細(xì)節(jié),更有助于我們?cè)陂_發(fā)中遇到問題可以快速理解其實(shí)質(zhì),并加以解決,在此記下筆記,與大家共同學(xué)習(xí)提高。
總結(jié)
以上是生活随笔為你收集整理的从一个小故事聊聊字符编码那些事的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: P678-vect2.cpp
- 下一篇: 基于IP访问控制的局限性