在谍战剧里,我们经常看到这样一个桥段,特工人员,千辛万苦拿到一条信息,打开一看是一串数字,然后赶紧跑到一个秘密地方,拿出一个密码本(也可能是一本唐诗选),按照一定规则(只有自己人知道),比如第一个数字表示页数,第二个数字表示行数,第三个数字表示第几个字,逐一将信息翻译出来。如果这个过程中用了错误的密码本,或者不知道规则,那么将会解码失败。
计算机的编解码过程跟上面的过程是一样一样的。
计算机只认 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编码方式(二进制) |
---|---|
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
码)。