作为一个程序员,你应该知道的编码知识

485 阅读11分钟

原创:花括号MC(微信公众号:huakuohao-mc)。关注JAVA基础编程及大数据,注重经验分享及个人成长。

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

计算机的编解码过程跟上面的过程是一样一样的。

计算机只认 01,所有的影像和字符最终都会转换成计算机能够认识的二进制。一个二进制位(bit)可以表示两种状态 01,一个字节(byte)由八个二进制位组成,所以一个字节一共可以表示256(2^8)种状态。如果我们规定每种状态代表一个字符,那么一个字节就可以表达出 256 个字符。

ASCII

计算机是由美国人发明的,所以在最初设计编码的时候,就只考虑了英文的编码。英文字符很少,加上一些特殊字符,一共也就100个左右,确切的说是128个。这样的话用一个字节进行编码就完全够了,不仅够用了,而且还富裕出一位,即第一位一直没有参与编码,统一定为 0。这就是所谓的 ASCII 编码。在 ASCII 编码中,空格SPACE32(二进制 00100000 ),大写的字母 A65 (二进制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

上面提到的都是简体中文编码,虽然 GBKGB18030 包含了部分繁体字,但是也不全面,于是台湾同胞就发了专门支持繁体字的 Big5 编码,也就是大家经常说的大五码。

一个小问题

不知道大家有没有注意到一个问题,在单字节编码的时候,对于那些大于 127 小于 256 的编码,在不同的国家代表的字母很可能不一样。比如,130 在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。在汉字的双字节编码中也存在这样的问题,比如BIG5编码跟GBK 编码都是双字节编码,但是代表的汉字却不一样。

这就相当于,同样一串二进制数值,A特工组织按照他们的规则解析出来可能是“你好”,而B特工组织按照他们的规则解析出来可能是“滚蛋”。特工组织之间的翻译标准不一样是相当有必要的,但是计算机的编码规则如果各不相同就比较麻烦了。比如你跟台湾的志玲姐姐聊天,志玲姐姐用BIG5 编码给你发了一封信,然后你用 GBK 去解码,……,也许就没有然后了。

Unicode

为了解决上面的问题,有个叫ISO 的国际标准组织,决定放弃所有区域性编码,如BIG5GBK 等,重新制定一个新的编码,这个编码集将包含所有字符的编码,这样大家就都统一了,这套编码的英文全称“Universal Multiple-Octet Coded Character Set”,简称UCS, 俗称 “Unicode“。Unicode 的出现相当于秦始皇对度量衡跟货币进行了统一。

Unicdoe 按照日常字符的使用频繁度划分了17 个平面,编号为0-160 号平面称为基本多语言平面(Basic Multilingual Plane,简称 BMP ),包含了日常使用最频繁的字符,编码范围从0000FFFF,这样该平面可以表示2^16=65536 个字符;其它平面的编码范围也是从0000FFFF,所以其它平面也可以编码 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 007F0xxxxxxx
0000 0080-0000 07FF110xxxxx 10xxxxxx
0000 0800-0000 FFFF1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

根据上表,对UTF-8 编码进行解读会发现,如果一个字节的第一位是 0,则这个字节单独就是一个字符;如果第一位是 1,则连续有多少个 1,就表示当前字符占用多少个字节。

举个例子 假设“hello世界”这样一个字符串,他们的 Unicode 的编码分别是

h--0068
e--0065
l--006C
l--006C
o--006F--4E16--754C

按照 UTF-8 的编码规则可以得到如下UTF-8 编码

h--01101000
e--01100101
l--01101100
l--01101100
o--01101111--11100100-10111000-10010110--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 表示。这正好是两个字节,而且FFFE1

如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。

总结

UTF-8 编码是基于Unicode 字符集的一种编码实现。现在几乎所有的编程语言和操作系统都支持 Unicode 编码,使用Unicode 编码之后,再也不会出现上文提到的一个汉字等于两个英文字符的尴尬局面。

GBKBIG5 等都属于区域性编码只能在固定范围内使用,比如GBK 只适合在简体中文环境使用,虽然 GBK 相比于UTF-8 更节省空间,但现在全世界都变成地球村了,所以还是建议大家都使用UTF-8 编码。

ANSI:在window下,如果我们用记事本打开文档,经常会见到ANSI 编码方式,这是Windows 默认的编码方式。对于英文文档采用ASCII编码,对于简体中文文档采用 GB2312 编码(只针对 Windows 简体中文版,如果是繁体中文版会采用 Big5 码)。


推荐阅读

手把手教你搭建一套ELK日志搜索运维平台

教你如何学习Java的 NIO

这也许就是产品和开发互撕的本质原因吧

·END·
 

花括号MC

Java·大数据·个人成长

微信号:huakuohao-mc