字符编码
简介
- 起初再考虑写不写这篇文章,感觉这篇文章比较枯燥乏味,而且自己感觉也没理解的太透彻,就把理解的记录下来,所以这是纪念版的
Go hard or go home 要么全力以赴,要么走人
No person has the right to rain on your dreams,you do it yourself.
没有人有权利给你的梦想泼冷水,只有你自己给自己的梦想泼冷水
看到这样的文字是不是很励志?那换一种方式你还会这样想吗? 16进制版:
476f2068617264206f7220676f20686f6d652089814e485168529b4ee58d742c89814e488d704eba20a4e6f20706572736f6e20206861732074686520726967687420746f207261696e206f6e20796f757220647265616d732c796f7520646f20697420796f757273656c662e206ca167094eba6709674352297ed94f60768468a660f36cfc51b76c342c53ea67094f6081ea5df17ed981ea5df1768468a660f36cfc51b76c34
然而他的字符编码是GB2312的,叫我转化成易懂的字符串,当时我就懵b了。因为当时我对字符编码一窍不通,然后就网上,查啊查,最后终于想到了解决方案
几个值的深思的问题
- 什么是字符?
字符是各种文字和符号的总称,包括各个国家文字、标点符号、图形符号、数字等。
- 什么是字符集?
字符集是多个字符的集合,字符集种类较多,每个字符集包含的字符个数不同,常见字符集有:ASCII字符集、ISO 8859字符集、GB2312字符集、BIG5字符集、GB18030字符集、Unicode字符集等
- 什么是字符编码?
1、 计算机要准确的处理各种字符集文字,需要进行字符编码,以便计算机能够识别和存储各种文字。
2、 字符编码(encoding)和字符集不同。字符集只是字符的集合,不一定适合作网络传送、处理,有时须经编码(encode)后才能应用。如Unicode可依不同需要以UTF-8、UTF-16、UTF-32等方式编码。
3、字符编码就是以二进制的数字来对应字符集的字符。 因此,对字符进行编码,是信息交流的技术基础。
- 概括
1、使用哪些字符。也就是说哪些汉字,字母和符号会被收入标准中。所包含“字符”的集合就叫做“字符集”。
2、规定每个“字符”分别用一个字节还是多个字节存储,用哪些字节来存储,这个规定就叫做“编码”。
3、各个国家和地区在制定编码标准的时候,“字符的集合”和“编码”一般都是同时制定的。因此,平常我们所说的“字符集”,比如:GB2312, GBK, JIS 等,除了有“字符的集合”这层含义外,同时也包含了“编码”的含义。
4、注意:Unicode字符集有多种编码方式,如UTF-8、UTF-16等;ASCII只有一种;大多数MBCS(包括GB2312,GBK)也只有一种。
- 有趣的例子
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 编码。在发送数据给对方前,需要事先告知对方自己所使用的编码,或者通过转码,使不同编码方案的两个系统可沟通自如。
- 这个例子说明了三点
1、不管是任何文字图片等,最后都会以二进制的形式储存到电脑的磁盘中(比如记事本A.txt,内容为"ABC"文件,在此磁盘中表现的就是01 01这种二进制形式)
盘片表面凹凸不平,凸起的地方被磁化,凹的地方是没有被磁化,凸起的地方代表数字1,凹的地方代表数字0。硬盘只能用0和1来表示所有文字、图片等信息。是的 很强势
2、 任何文件要储存到电脑中,都会事先进行编码,然后储存到电脑的磁盘中,比如A.txt文件,默认编码为ANSI编码,也可以编码为UTF-8,然而不同的编码方式 对应着计算机用一个字节还是多个字节存储,用哪些字节来存储。
3、在双方数据进行通讯时,要么就保证发送方和接受方的数据编码是相同,要么就是其中一方需要转码
- 什么是字节和位?
字节byte和位bit是电脑里的数据量单位。
1.按计算机中的规定,一个英文的字符占用一个字节,而一个汉字以及汉字的标点符号、字符都占用两个字节。
2.1个字节等于8位 1byte=8bit
3.1bit在磁盘中以二进制01的形式保存 凸起的地方代表数字1,凹的地方代表数字0
字符编码种类
ASCII
ASCII码是西欧编码的方式,采取7位编码,所以是2^7=128,共可以表示128个字符,包括34个字符,(如换行LF,回车CR等),其余94位为英文字母和标点符号及运算符号等。
重点:
字符集:从符号(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是这样做的:
- 单字节的字符,字节的第一位设为0,对于英语文本,UTF-8码只占用一个字节,和ASCII码完全相同;
- n个字节的字符(n>1),第一字节的前n位设为1,第n+1位设为0,后面字节的前两位都设为10,这n个字节的其余空位填充该字符unicode码,高位用0补足。
高位字节 | 低位字节 | 低位字节 | 低位字节 | 低位字节 | 低位字节 |
---|---|---|---|---|---|
0xxxxxxx | |||||
110xxxxx | 10xxxxxx | ||||
1110xxxx | 10xxxxxx | 10xxxxxx | |||
11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | ||
111110xx | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | |
1111110x | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx |
... .... |
这样就形成了如下的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;
}
这时就要想到,中文汉子对照表:
解决方案
首先把汉子编码对照表存入以存入数据库(mongodb)
获取,并以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进制。哎,我还是太年轻啊