深入理解-字符編碼ASCII,GB2312,GBK,Unicode,UTF-8

字符編碼

簡介

  1. 起初再考慮寫不寫這篇文章,感覺這篇文章比較枯燥乏味,而且自己感覺也沒理解的太透徹,就把理解的記錄下來,所以這是紀念版的
  2. 前方高能,非戰鬥人員請迅速撤離,我要開始裝逼了。

    Go hard or go home 要麼全力以赴,要麼走人
    No person has the right to rain on your dreams,you do it yourself.
    沒有人有權利給你的夢想潑冷水,只有你自己給自己的夢想潑冷水

    看到這樣的文字是不是很勵志?那換一種方式你還會這樣想嗎? 16進製版:

    476f2068617264206f7220676f20686f6d652089814e485168529b4ee58d742c89814e488d704eba20a4e6f20706572736f6e20206861732074686520726967687420746f207261696e206f6e20796f757220647265616d732c796f7520646f20697420796f757273656c662e206ca167094eba6709674352297ed94f60768468a660f36cfc51b76c342c53ea67094f6081ea5df17ed981ea5df1768468a660f36cfc51b76c34

    然而他的字符編碼是GB2312的,叫我轉化成易懂的字符串,當時我就懵b了。因爲當時我對字符編碼一竅不通,然後就網上,查啊查,最後終於想到了解決方案

幾個值的深思的問題

  1. 什麼是字符?

    字符是各種文字和符號的總稱,包括各個國家文字、標點符號、圖形符號、數字等。

  2. 什麼是字符集?

    字符集是多個字符的集合,字符集種類較多,每個字符集包含的字符個數不同,常見字符集有:ASCII字符集、ISO 8859字符集、GB2312字符集、BIG5字符集、GB18030字符集、Unicode字符集等

  3. 什麼是字符編碼?

    1、 計算機要準確的處理各種字符集文字,需要進行字符編碼,以便計算機能夠識別和存儲各種文字。
    2、 字符編碼(encoding)和字符集不同。字符集只是字符的集合,不一定適合作網絡傳送、處理,有時須經編碼(encode)後才能應用。如Unicode可依不同需要以UTF-8、UTF-16、UTF-32等方式編碼。
    3、字符編碼就是以二進制的數字來對應字符集的字符。 因此,對字符進行編碼,是信息交流的技術基礎。

  4. 概括

    1、使用哪些字符。也就是說哪些漢字,字母和符號會被收入標準中。所包含“字符”的集合就叫做“字符集”。
    2、規定每個“字符”分別用一個字節還是多個字節存儲,用哪些字節來存儲,這個規定就叫做“編碼”。
    3、各個國家和地區在制定編碼標準的時候,“字符的集合”和“編碼”一般都是同時制定的。因此,平常我們所說的“字符集”,比如:GB2312, GBK, JIS 等,除了有“字符的集合”這層含義外,同時也包含了“編碼”的含義。
    4、注意:Unicode字符集有多種編碼方式,如UTF-8、UTF-16等;ASCII只有一種;大多數MBCS(包括GB2312,GBK)也只有一種。

  5. 有趣的例子

    1、在顯示器上看見的文字、圖片等信息在電腦裏面,其實並不是我們看見的樣子,即使你知道所有信息都存儲在硬盤裏,把它拆開也看不見裏面有任何東西,只有些盤片。假設,你用顯微鏡把盤片放大,會看見盤片表面凹凸不平,凸起的地方被磁化,凹的地方是沒有被磁化;凸起的地方代表數字1,凹的地方代表數字0。硬盤只能用0和1來表示所有文字、圖片等信息。
    2、那麼字母”A”在硬盤上是如何存儲的呢?可能小張計算機存儲字母”A”是1100001,而小王存儲字母”A”是11000010,這樣雙方交換信息時就會誤解。比如小張把1100001發送給小王,小王並不認爲1100001是字母”A”,可能認爲這是字母”X”,於是小王在用記事本訪問存儲在硬盤上的1100001時,在屏幕上顯示的就是字母”X”。也就是說,小張和小王使用了不同的編碼表。小張用的編碼表是ASCII,ASCII編碼表把26個字母都一一的對應到2進制1和0上;小王用的編碼表可能是EBCDIC,只不過EBCDIC編碼與ASCII編碼中的字母和01的對應關係不同。一般地說,開放的操作系統(LINUX 、WINDOWS等)採用ASCII 編碼,而大型主機系統(MVS 、OS/390等)採用EBCDIC 編碼。在發送數據給對方前,需要事先告知對方自己所使用的編碼,或者通過轉碼,使不同編碼方案的兩個系統可溝通自如。

  6. 這個例子說明了三點

    1、不管是任何文字圖片等,最後都會以二進制的形式儲存到電腦的磁盤中(比如記事本A.txt,內容爲”ABC”文件,在此磁盤中表現的就是01 01這種二進制形式)
    盤片表面凹凸不平,凸起的地方被磁化,凹的地方是沒有被磁化,凸起的地方代表數字1,凹的地方代表數字0。硬盤只能用0和1來表示所有文字、圖片等信息。是的 很強勢
    2、 任何文件要儲存到電腦中,都會事先進行編碼,然後儲存到電腦的磁盤中,比如A.txt文件,默認編碼爲ANSI編碼,也可以編碼爲UTF-8,然而不同的編碼方式 對應着計算機用一個字節還是多個字節存儲,用哪些字節來存儲。
    3、在雙方數據進行通訊時,要麼就保證發送方和接受方的數據編碼是相同,要麼就是其中一方需要轉碼

  7. 什麼是字節和位?

    字節byte和位bit是電腦裏的數據量單位。
    1.按計算機中的規定,一個英文的字符佔用一個字節,而一個漢字以及漢字的標點符號、字符都佔用兩個字節。
    2.1個字節等於8位 1byte=8bit
    3.1bit在磁盤中以二進制01的形式保存 凸起的地方代表數字1,凹的地方代表數字0

字符編碼種類

ASCII

ASCII碼是西歐編碼的方式,採取7位編碼,所以是2^7=128,共可以表示128個字符,包括34個字符,(如換行LF,回車CR等),其餘94位爲英文字母和標點符號及運算符號等。
ASCII表

重點:

字符集:從符號(NUL=”/0”=“空操作字符”)到“Z”再到“DEL”符號
字符編碼範圍:二進制:00000000——01111111 十進制:0-127
佔用字節:1字節 8bit 盤片儲存方式:凹凹凹凹凹凹凹凹——凸凸凸凸凸凸凸凸

注:NUL:‘\0’是一個ASCII碼爲0的字符,從ASCII碼錶中可以看到ASCII碼爲0的字符是“空操作字符”,它不引起任何控制動作,也不是一個可顯示的字符。

但我們發現ASCII碼是沒有中文編碼的,顯然在天朝是不夠用的,於是GB2312誕生了。

GB2321

GB2312 是對 ASCII 的中文擴展。兼容ASCII。

編碼規定:
編碼小於127的字符與ASCII編碼相同,
特性:兩個大於127的字符連在一起時,就表示一個漢字,前面的一個字節(稱之爲高字節)從0xA1用到0xF7,後面一個字節(低字節)從0xA1到0xFE,這樣我們就可以組合出大約7000多個簡體漢字了。

字符集:從符號(NUL=”/0”=“空操作字符”)到“Z”到“齇”(簡體中文)
字符編碼範圍:16進制:0x0000-(中間有一部分是未使用的)-0xF7FE
佔用字節:英文 1字節 8bit 盤片儲存方式:凹凹凹凹凹凹凹凹——凸凸凸凸凸凸凸凸
中文 2字節 16bit 凹凹凹凹凹凹凹凹凹凹凹凹凹凹凹凹——…

GBK

GBK 兼容ASCLL 兼容 GB2312 是GB2312的擴展
但是中國的漢字太多了,我們很快就就發現有許多人的人名沒有辦法在這裏打出來,不得不繼續把 GB2312 沒有用到的碼位找出來用上。後來還是不夠用,於是乾脆不再要求低字節一定是127號之後的內碼,只要第一個字節是大於127就固定表示這是一個漢字的開始,不管後面跟的是不是擴展字符集裏的內容。結果擴展之後的編碼方案被稱爲 “GBK” 標準,GBK 包括了 GB2312 的所有內容,同時又增加了近20000個新的漢字(包括繁體字)和符號。

Unicode

Unicode是國際組織制定的可以容納世界上所有文字和符號的字符編碼方案。
目前的Unicode字符分爲17組編排,0x0000至0x10FFFF,每組稱爲平面(Plane),而每平面擁有65536個碼位,共1114112個。然而目前只用了少數平面。UTF-8、UTF-16、UTF-32都是將數字轉換到程序數據的編碼方案。

UTF-8

UTF-8以字節爲單位對Unicode進行編碼。從Unicode到UTF-8的編碼方式如下:
UTF-8的特點是對不同範圍的字符使用不同長度的編碼。對於0x00-0x7F之間的字符,UTF-8編碼與ASCII編碼完全相同。UTF-8編碼的最大長度是6個字節。從上表可以看出,6字節模板有31個x,即可以容納31位二進制數字。Unicode的最大碼位0x7FFFFFFF也只有31位。
例1:“漢”字的Unicode編碼是0x6C49。0x6C49在0x0800-0xFFFF之間,使用用3字節模板了:1110xxxx 10xxxxxx 10xxxxxx。將0x6C49寫成二進制是:0110 1100 0100 1001, 用這個比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。
舉一個例子:It’s 知乎日報

你看到的unicode字符集是這樣的編碼表:

I 0049
t 0074
’ 0027
s 0073
0020
知 77e5
乎 4e4e
日 65e5
報 62a5
每一個字符對應一個十六進制數字。

計算機只懂二進制,因此,嚴格按照unicode的方式(UCS-2),應該這樣存儲:

I 00000000 01001001
t 00000000 01110100
’ 00000000 00100111
s 00000000 01110011
00000000 00100000
知 01110111 11100101
乎 01001110 01001110
日 01100101 11100101
報 01100010 10100101
這個字符串總共佔用了18個字節,但是對比中英文的二進制碼,可以發現,英文前9位都是0!浪費啊,浪費硬盤,浪費流量。

怎麼辦?

UTF

UTF-8是這樣做的:

  1. 單字節的字符,字節的第一位設爲0,對於英語文本,UTF-8碼只佔用一個字節,和ASCII碼完全相同;
  2. n個字節的字符(n>1),第一字節的前n位設爲1,第n+1位設爲0,後面字節的前兩位都設爲10,這n個字節的其餘空位填充該字符unicode碼,高位用0補足。

這樣就形成了如下的UTF-8標記位:

高位字節 低位字節 低位字節 低位字節 低位字節 低位字節
0xxxxxxx
110xxxxx 10xxxxxx
1110xxxx 10xxxxxx 10xxxxxx
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
… ….


比如”知”字 在Unicode中佔用兩個字節,那麼第一字節(我叫它高位字節)的前兩位設位1,第三位設爲10,後面低位字節設爲前兩位設爲10, “知”→ 11100111 10011111 10100101
怎麼知道“知”字佔用兩個字節的?首先要知道Unicode字符集中,“知”字的編碼爲77e5,然後轉化爲二進制流01110111 11100101的bit,每8bit等於1byte 所以就佔兩個字節
於是,”It’s 知乎日報“就變成了:
I 01001001
t 01110100
’ 00100111
s 01110011
00100000
知 11100111 10011111 10100101
乎 11100100 10111001 10001110
日 11100110 10010111 10100101
報 11100110 10001010 10100101
和上邊的方案對比一下,英文短了,每個中文字符卻多用了一個字節。但是整個字符串只用了17個字節,比上邊的18個短了一點點。
劇透:一切都是爲了節省你的硬盤和流量。

一圖解憂愁

漢子對照表

1.從這個可以看出,同樣的字符集,但unicode編碼和gbk編碼是不同的。,所以unicode字符集不兼容gbk字符集
2.只要知道unicode字符集的編碼表,就可以用UTF8編碼規則找到UTF-8對應的漢字編碼


解決問題

從上面的內容瞭解了字符編碼以後,以後遇到相關的字符編碼問題的時候至少有解決的思路,而不是一頭霧水

分析

NodeJS服務端環境下
476f2068617264206f7220676f20686f6d652089814e485168529b4ee58d742c89814e488d704eba20a4e6f20706572736f6e20206861732074686520726967687420746f207261696e206f6e20796f757220647265616d732c796f7520646f20697420796f757273656c662e206ca167094eba6709674352297ed94f60768468a660f36cfc51b76c342c53ea67094f6081ea5df17ed981ea5df1768468a660f36cfc51b76c34
容易產生誤區:
這個問題的情況並不是字符亂碼問題,而只是怎樣解析16進制gb2312字符,只是利用了字符編碼的原理。
1.我接受的是gb2312格式的數據,但是這裏並沒有亂碼,因爲服務器發過來的是數字和英文,gb2312是兼容ASCII的。
2.我設置了(接受響應數據編碼格式)response.setEncoding(‘gb2312’);即使我不設置響應格式,nodejs默認識utf-8的,utf-8和gbk都是兼容ASCII,也是就是支持英文和數字

var http=require('http');
var Iconv = require('iconv-lite');//轉碼數據
var GetHttp=function(options,callback){
var AllData="";
try{
var GetReq = http.request(options, function (res) {  
    console.log('STATUS: ' + res.statusCode);
    res.setEncoding('gb2312');  
if(res.statusCode==200){    
    res.on('data', function (chunk) { AllData+=chunk;})
       .on('end',function(){callback(200,AllData);})        
}else{
callback(500,'error');
}
console.log(AllData);
});  
GetReq.on('error',function(err){callback(500,err)});    
GetReq.end();
}catch(error){
callback(500,error);
}
}

exports.GetHttp=GetHttp;

開始問題分析:
1.字符集分析:gb2312支持數字和英文和6000+漢字
2.編碼分析:英文佔一個字節,中文佔兩個字節(這就是問題)

//1.fromCharCode() 可接受一個指定的 Unicode 值,然後返回一個字符串。但我們的數據是gb2312的編碼數據,然而gbk和unicode的編碼方式又不一樣,所以解析出來的數據會亂碼
//2.利用下面的代碼,中文也會亂碼,因爲英文佔1個字節,中文佔2個字節,1個字節是8個二進制流的bit=2個16進制流的bit,而中文=4個16進制流的bit,下面的代碼相當於把1個16進制的數轉爲字符
function HexTostring(s) {
    var r = "";
    for (var i = 0; i < s.length; i += 2) { var sxx = parseInt(s.substring(i, i + 2), 16); r += String.fromCharCode(sxx); }
    return r;
}

這時就要想到,中文漢子對照表:
漢子對照表

解決方案

  1. 首先把漢子編碼對照表存入以存入數據庫(mongodb)

  2. 獲取,並以key=gbk16進制編碼 value=漢子的形式存下來

  var dicUniCodeCN=new Array();
  DBTool.FindData('mongodb://數據庫地址/數據庫名','unicodeCN',{},function(Docs){
    if (Docs.length>0) {
        for (var i = 0; i < Docs.length; i++) {
            dicUniCodeCN[Docs[i].gbk16.toString().toUpperCase()]=Docs[i].CN;
        };
    }

});

3.特性:gb2312的高位字節如果大於127(ASCII),就爲中文,只有gb2312具有這個特性

 var simpleCNStr="";
 for (var j = 0; j < hexData.length; j += 2){
//高位字節>127爲中文
var strHex=hexData.substring(j,j+2);
console.log(parseInt("0x"+strHex,16));

if (parseInt("0x"+strHex,16)>127) {
    strHex=hexData.substring(j,j+4);
    j+=2;
    simpleCNStr+=dicUniCodeCN[strHex];
}else{
    simpleCNStr+=String.fromCharCode(parseInt(strHex,16));
}
}

4.如果想兼容utf-8和unicode和gbk,那麼可以4位16進制的字符截取,如果大於127,那麼默認爲中文,否則就是英文或字符或數字

 var simpleCNStr="";
 for (var j = 0; j < hexData.length; j += 4){
//4位截取,大於127的爲中文
var strHex=hexData.substring(j,j+4);
console.log(parseInt("0x"+strHex,16));

if (parseInt("0x"+strHex,16)>127) {
//不想寫了  
}else{
//待續 你們寫吧...
}
}

題外話-關於parseInt(string, radix)

parseInt("10");         //返回 10
parseInt("19",10);      //返回 19 (10+9)
parseInt("11",2);       //返回 3 (2+1)
parseInt("17",8);       //返回 15 (8+7)
parseInt("1f",16);      //返回 31 (16+15)
parseInt("010");        //未定:返回 10 或 8

這個函數是把數字或進制字符都轉爲10進制的數字,第二個參數radix表示的是第一個參數string的類型(10進制,2進制,8進制,16進制),我之前很白菜的理解爲我想把第一個參數string轉化成16進制。哎,我還是太年輕啊

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章