计算机只认 0 和 1 ,所有的影像和字符最终都会转换成计算机能够认识的二进制。一个二进制位(bit)可以表示两种状态 0 和 1 ,一个字节(byte)由八个二进制位组成,所以一个字节一共可以表示256( 2^8 )种状态。
在谍战剧里,我们经常看到这样一个桥段,特工人员,千辛万苦拿到一条信息,打开一看是一串数字,然后赶紧跑到一个秘密地方,拿出一个密码本(也可能是一本唐诗选),按照一定规则(只有自己人知道),比如第一个数字表示页数,第二个数字表示行数,第三个数字表示第几个字,逐一将信息翻译出来。如果这个过程中用了错误的密码本,或者不知道规则,那么将会解码失败。
作为一个程序员,告诉你一些编码知识作为一个程序员,告诉你一些编码知识
计算机的编解码过程跟上面的过程是一样一样的。
计算机只认 0 和 1 ,所有的影像和字符最终都会转换成计算机能够认识的二进制。一个二进制位(bit)可以表示两种状态 0 和 1 ,一个字节(byte)由八个二进制位组成,所以一个字节一共可以表示256( 2^8 )种状态。如果我们规定每种状态代表一个字符,那么一个字节就可以表达出 256 个字符。
ASCII
计算机是由美国人发明的,所以在最初设计编码的时候,就只考虑了英文的编码。英文字符很少,加上一些特殊字符,一共也就100个左右,确切的说是128个。这样的话用一个字节进行编码就完全够了,不仅够用了,而且还富裕出一位,即第一位一直没有参与编码,统一定为 0 。这就是所谓的 ASCII 编码。在 ASCII 编码中,空格 SPACE 是 32 (二进制 00100000 ),大写的字母 A 是 65 (二进制 01000001 )。
非ASCII
随着计算机的普及,欧洲也开始普及计算机,欧洲人发现 ASCII 规定的 128 个字符不能满足他们的使用,比如,在法语中,字母上方有注音符号,就无法用 ASCII 码表示。于是,一些欧洲国家就决定,把字节中闲置的第一位编入新的符号。比如,法语中的 é 的编码为 130 (二进制 10000010 )。这样一来,这些欧洲国家使用的编码体系,最多可以表示 256 个符号。这就是大家经常见到的 ISO-8859-1 编码,也叫 Latin1 编码。
中文编码
随着计算机的普及,国人也开始使用计算机,但是发现按照之前的编码方式,根本就没有汉字什么事儿,也就是计算机根本没办法认识汉字。
GB2312
为了能够让计算机认识汉字,我们决定对汉字进行编码,本着敢想敢干的精神,我们规定用两个字节表示一个汉字。
具体规则是这样的:一个小于 127 的字节代表的意义与原来的 ASCII 相同,但两个大于 127的字节连在一起时,就表示这是一个汉字,前面的一个字节称为高字节,后面一个字节称为低字节,这样我们就可以组合出 6763 个简体汉字。这就是大家常说的 GB2312 编码。
GBK
很显然 GB2312 编码的 6763 个汉字,并不能适应所有的使用场景,比如“喆”字就不再其中,于是在 GB2312 的基础上又进行了新的扩展,规定只要第一个字节是大于 127 的就OK,至于第二个字节是大于 127 还是小于 127 都无所谓了。经过这样的改动之后,收录的汉字及符号就可以达到 2W 多个,这就是我们常说的 GBK 编码。
再后来,人们继续对第二个字节进行扩展,发展出了 GB18030 编码,比 GBK 又多出了一些字符编码。
至此,所有的汉字编码都是用两个字节表示的,但是英文是用一个字节表示。上了一些年纪的程序员都体验过,一个汉字算两个英文字符的经历。
BIG-5
上面提到的都是简体中文编码,虽然 GBK 及 GB18030 包含了部分繁体字,但是也不全面,于是台湾同胞就发了专门支持繁体字的 Big5 编码,也就是大家经常说的大五码。
一个小问题
不知道大家有没有注意到一个问题,在单字节编码的时候,对于那些大于 127 小于 256 的编码,在不同的国家代表的字母很可能不一样。比如, 130 在法语编码中代表了 é ,在希伯来语编码中却代表了字母 Gimel (ג) ,在俄语编码中又会代表另一个符号。在汉字的双字节编码中也存在这样的问题,比如 BIG5 编码跟 GBK 编码都是双字节编码,但是代表的汉字却不一样。
这就相当于,同样一串二进制数值,A特工组织按照他们的规则解析出来可能是“你好”,而B特工组织按照他们的规则解析出来可能是“滚蛋”。特工组织之间的翻译标准不一样是相当有必要的,但是计算机的编码规则如果各不相同就比较麻烦了。比如你跟台湾的志玲姐姐聊天,志玲姐姐用 BIG5 编码给你发了一封信,然后你用 GBK 去解码,……,也许就没有然后了。
Unicode
为了解决上面的问题,有个叫 ISO 的国际标准组织,决定放弃所有区域性编码,如 BIG5 , GBK 等,重新制定一个新的编码,这个编码集将包含所有字符的编码,这样大家就都统一了,这套编码的英文全称“Universal Multiple-Octet Coded Character Set”,简称UCS, 俗称 “Unicode“。 Unicode 的出现相当于秦始皇对度量衡跟货币进行了统一。
Unicdoe 按照日常字符的使用频繁度划分了 17 个平面,编号为 0-16 , 0 号平面称为基本多语言平面(Basic Multilingual Plane,简称 BMP ),包含了日常使用最频繁的字符,编码范围从 0000 到 FFFF ,这样该平面可以表示 2^16=65536 个字符;其它平面的编码范围也是从 0000 到 FFFF ,所以其它平面也可以编码 65535 个字符,这样 17 个平面一共可以编码 17×65,536 = 1,114,112 个符号。
我们最常用的 Unicode 编码使用的是多语言平面的编码,即所有字符都用两个字节进行编码(其它平面可能需要三个或四个字节)。举个例子比如中国的'中'字 Unicode 码是 4E2D ,小写'a'的 Unicode 码是 0061 .
这里面存在两个问题,如果所有英文字符都是按照 Unicode 编码,那么会出现浪费存储空间的问题。明明一个字节可以搞定的事情,偏偏要用两个字节。
第二个问题就是计算机如何知道这是 Unicode 编码还是 ASCII 编码,也就是 2 个字节表示的一个字符,还是 2 个字符呢。
UTF
UTF 的全称是 Unicode Transformation Format ,也就是 Unicode 的转换格式。上面提到了,如果直接使用 Unicode 码进行存储会存在浪费空间的问题,而 UTF-8 的出现就是为了解决该问题, UTF-8 使用变长的方式存储 Unicode 码,也就是英文字符继续使用一个字节进行存储,但是汉字要使用 3 个字节。那么 UTF-8 是如何做到的呢。
首先,对于单字节的符号,字节的第一位设为 0 ,后面 7 位为这个符号的 Unicode 码。因此对于英语字母, UTF-8 编码和 ASCII 码是相同的。
其次,对于 n 字节的符号( n > 1 ),第一个字节的前 n 位都设为 1 ,第 n + 1 位设为 0 ,后面字节的前两位一律设为 10 。剩下的没有提及的二进制位,全部为这个符号的 Unicode码。
下表总结了编码规则,字母 x 代表可用的编码位。
Unicode符号范围(十六进制) UTF-8编码方式(二进制)<>pre
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
根据上表,对 UTF-8 编码进行解读会发现,如果一个字节的第一位是 0 ,则这个字节单独就是一个字符;如果第一位是 1 ,则连续有多少个 1 ,就表示当前字符占用多少个字节。
举个例子
假设“hello世界”这样一个字符串,他们的 Unicode 的编码分别是
1h--0068
2e--0065
3l--006C
4l--006C
5o--006F
6世--4E16
7界--754C
按照 UTF-8 的编码规则可以得到如下 UTF-8 编码
1h--01101000
2e--01100101
3l--01101100
4l--01101100
5o--01101111
6世--11100100-10111000-10010110
7界--11100111-10010101-10001100
可以看到用 UTF-8 编码之后,英文字符占用一个字节,而汉字占用了三个字节,一共需要 11个字节,而如果直接存储 Unicode 码则需要 14 个字节。 UTF-8 编码对于英文来说节省了很大空间,但是对于中文来说增加了空间。
Little endian 和 Big endian
上面提到 Unicode 是用两个字节表示字符,如果第一个字节在前,就是"大端方式"(Big endian),第二个字节在前就是"小端方式"(Little endian)。'世'字的 Unicode 码是 4E16 ,一个字节是 4E ,一个字节是 16 , 存储的时候如果 4E 在前就是大端存储,如果是 16 在前就是小端存储。
那么计算机是怎么知道一个文件是采用哪种编码方式呢?
Unicode 规范定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做"零宽度非换行空格"(zero width no-break space),用 FEFF 表示。这正好是两个字节,而且 FF 比 FE 大 1 。
如果一个文本文件的头两个字节是 FE FF ,就表示该文件采用大头方式;如果头两个字节是 FF FE ,就表示该文件采用小头方式。
总结
UTF-8 编码是基于 Unicode 字符集的一种编码实现。现在几乎所有的编程语言和操作系统都支持 Unicode 编码,使用 Unicode 编码之后,再也不会出现上文提到的一个汉字等于两个英文字符的尴尬局面。
GBK , BIG5 等都属于区域性编码只能在固定范围内使用,比如 GBK 只适合在简体中文环境使用,虽然 GBK 相比于 UTF-8 更节省空间,但现在全世界都变成地球村了,所以还是建议大家都使用 UTF-8 编码。
ANSI :在 window 下,如果我们用记事本打开文档,经常会见到 ANSI 编码方式,这是 Windows 默认的编码方式。对于英文文档采用 ASCII 编码,对于简体中文文档采用 GB2312 编码(只针对 Windows 简体中文版,如果是繁体中文版会采用 Big5 码)。