字符编码

字符编码可以说是一个被人忽视却很重要的知识,因为我们只是大概知道个所以然,但是又不能准确了解字符编码的原理,举个例子,为什么会出现乱码,emoji又是怎么回事呢,包括我在内也只是大体上了解,所以今天打算一探究竟,做个总结。

基础概念

字符编码中有两个东西,一是字符,二是编码,一个中文汉字代表一个字符,一个英文字母也代表一个字符。当许多字符堆在一块的时候就形成了字符集,比如英语、法语、汉语等。字符集由三个重要部分组成:字库表编码字符集字符编码

字库表里面记录了对应语言的所能表述的所有字符,编码字符集是表示字符在字库表里的位置的概念,比如苹果在汉语字库表的第1234页,在法语字库表的第3000页,请把苹果想象成一个字符,这里面的1234和3000就是一个编码值(code point),而字符编码是表述字符集转换成实际存储数据010101010的过程。 我们肯定听说过ASCII,UTF8,GB2312,Unicode编码,为啥会有这么多编码呢???

ASCII编码

因为计算机是美国人发明的,美国人只需要用26个字符和常见的标点符号就可以把所有的意思表述完毕了,因此发明了ASCII编码(美国信息交换标准代码)。

ASCII

可以看出它只包含了128个字符,老美够抠的,用7位二进制数表示,由于计算机1个字节是由8个二进制位组成的,因此ASCII编码中的字符只需用一个字节就可以表示完毕,单字节存储到计算机中,00000000-01111111,0x00-0x7F。

非ASCII编码

可以想象,我们中国人肯定是不够用的,光是汉字就有5-6万个,经常使用的汉字至少也得5000个,因此我们发明了GB2312(中华人民共和国国家标准简体中文字符集),GB2312标准共收录6763个汉字,其中一级汉字3755个,二级汉字3008个;同时收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个字符。与此同时像港台的BIG5,GB2312的扩充版本GBK都是采用说双字节存储。双字节是变长的,比如有些简单字符用单字节存储,有些复杂字符用双字节存储,同时设计初衷希望兼容ASCII编码,带来的缺点是会存在空字位,造成资源浪费。

Unicode

注意这里我没有说它是编码,因为Unicode设计初衷是为了制定一个统一规则定义字符,它是一个很庞大的字符集,从它的字面就可以看出,统一的,宇宙的。 目前最新的版本为2016年6月21日公布的9.0.0[2],已经收入超过十万个字符,只要不是自己意淫出来的,在Unicode里都可以找到对应的字符,以及其所属code point(编码值)。 Unicode编号从0000到10FFFF(17个Plane),每个Plane包括256*256个字符,第一个Pane称为基本多文种平面(Basic Multiple Plane)简称BMP,其他的平面称为辅助平面(Supplementary Plane)。 Unicode规定每个符号用3个或者4个字节存储,可以想象无论编号低或高都用4字节存储,这会造成很大的存储浪费,因此并没有成为一个标准的字符编码,但是为日后的UTF8做了充足的准备。

UTF8编码

UTF8编码是Unicode的一种实现方式,每个字符的长度是变长的,可以提高存储的利用率。UTF8的编码规则如下:

  • 对于单字节的字符,字节的第一个字位是0,后面的7位是这个字符对应的Unicode码。因此对于英文字符来说,其UTF8编码和ASCII编码是一样的。
  • 对于n字节符号,第一个字节的前n位是1,第n+1位是0,后面字节的前两位是10,剩下的字位全部为这个符号的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的话就代表这个字节就是一个字符,很可能是一个简单的英语字符。如果第一位是1的话,则连续有多少个1,就代表该字符有多少个字节。

编码出现的问题

乱码的出现

是由于内容在存储到硬盘中和从硬盘中读取出来所使用的字符集不一致或者不兼容导致的。由于字符集不一样,那么每一个字符对应在不同的字符集位置不一样,因此会出现乱码。总结为存、取方式不一样。

大端小端字节序问题

有一次在做项目时候,需要把数据库中的timestamp类型的数据取出来并转换为对应的Int64,C#程序中获取到的timestamp类型变成了byte[]类型,在转换成Int64之前需要将byte[]颠倒顺序,然后再调用BitConvert.ToInt64()方法。所以我好奇为什么需要将字节数组颠倒顺序,原来这和大端小端存储顺序有关。 Unicode对于一个字符采用多字节编码,存储数据到计算机内存中会有字节序问题。

  • 大端存储(Big Endian): 内存中的低地址存储高字节位,高地址存储低字节位,符合人的主观想法逻辑。
  • 小端存储(Little Endian): 内存中的低地址存储低字节位,高地址存储高字节位,跟人的主观意识相反。

注意:对于大小端存储是指对于一个整数或者一个字符(包括数字、字母、汉字)对应的字节数组的存储顺序,不要想象成整个文本串的顺序,是整个文本串中的每一个字符的对应的码元的存储顺序。

比如一个Int类型整数123456789,十六进制表达方式为:07-5B-CD-15。如果小端存储,在内存储中排序为:0x15 0xCD 0x5B 0x07 (低位在前),大端存储,在内存中排序为:0x07 0x5B 0xCD 0x15 (高位在前),高位对应byte[]中的第0个字节。

为啥会有不同的排序呢?因为这和计算机的CPU有关系,对于Intel系列-Windows/Linux X86/X64的CPU采用Little Endian方式存储,而对于IBM 370和摩托罗拉微处理器都是用Big Endian方式存储,JAVA编译的程序使用Big Endian。 对于小段储存,一个好处就是可以直接添加到内存的尾部,无需经过顺序颠倒,因此相比大端存储更加高效。

C#中Encoding.Default.GetBytes()可以直接得到字符串的字节数组,而且其字节数组就是一个大端排序。而用BitConvert.GetBytes()却得到一个整数的小段排序字节数组,这是为什么呢? 因为对于使用UTF8编码的字符串,都是使用单个字节表示数字的,每个字节都有意义,当读取字节时候一定会从表示长度的那个字节开始读,无论正着存还是倒着存,队头队尾是确定的。 而使用对于数字来说,可能使用1个字节,2个字节或者多个字节存储,因此会有字节序问题,需要区分大小端,否则正着读是一个结果,倒着读又是一个结果。

参考资料