字符的编码格式有很多种,互联网上广泛存在着不同种类编码方式编码的文本文件。肯定有很多人在编程的时候遇到过读取文件出现乱码的问题,这很大程度上就是文件编码格式和读取时使用的编码格式不一致造成的,所以就经常需要根据文本编码类型来选择相应的编码格式读取文件。
字符编码是计算机技术的基石,所以了解编码格式的一些皮毛还是十分重要的。我再网络上查阅了相关的几篇文章,总结了主流编码格式 ASCII、UTF-8、Unicode 的相关知识点信息。
字节与编码
在了解不同的编码格式之前,我们还需要知道计算机是如何进行存储和编码的。
计算机内部处理和存储信息时,所有的信息都可以描述为一串特定的二进制信息流,其最小的单位为一个比特 $bit$ ,每一个比特都存在 $0$ 和 $1$ 两种状态;而 8 个比特便是一个字节 $byte$,可以表达出 $2^8=256$ 种状态。通过读取不同的状态,计算机就可以执行对应的操作,而这个把特定的一个状态对应到特定操作上的这个过程,就是编码。
下面贴一个比较官方的描述:
编码是信息从一种形式或格式转换为另一种形式的过程,也称为计算机编程语言的代码简称编码。用预先规定的方法将文字、数字或其它对象编成数码,或将信息、数据转换成规定的电脉冲信号。
所以,有了编码,我们就可以通过这样的方式写一套编码表,将不同的字符用对应的字节码来表示,从而让计算机“读懂”文字,并且显示在屏幕上,形成这一篇文章(笑)。
ASCII 编码
ASCII 码是最早的编码方式之一,早在上个世纪 60 年代,美国就制定了相关的一套字符编码表,并一直沿用至今。
ASCII 码中一共规定了 128 个字符,每一个字符通过 1 个字节来表示,但这 128 个符号只占用了一个字节的后 7 位,最前一位规定为 0。
对于英语国家来说,128 个字符就足够使用了,但是表示其他的语言是远远不够的——比如法语、希腊语等。(因为当时主要是在欧洲国家和北美洲国家使用,所以没有考虑到庞大的汉字系统)
因此后来 ASCII 码的标准被广泛使用,为了适应非英语语系国家以及数学家(误)的使用需求,码表还需要添加其他的非英语字符和符号,于是之前空出来的 1 比特就派上了用场,扩展 ASCII 问世。
非 ASCII 编码
不同的国家有不同的字母,128 个字符是囊括不完的。所以实际上高位的 128 个字符在不同的国家编码对应着不同的符号。比如 ASCII 码中,130 号字符在法语里是 é,而在希伯来语里是 ג,在其他的语言又会是另一个符号。
但是无论怎么改,都是高 128 位(128-255)不同,而低 128 位(0-127)所表示的符号全是一样的。
汉字由于是另一个语系,使用的符号十分之多,汉字就 10 万往上语法符号还和英文不一样,所以肯定需要更更多地字节去编码表示。例如简体中文常见的编码方式是 GB2312,每一个汉字需要两个字节来表示,理论上最多能够表示 $2^8\times2^8=65536$ 个符号。
Unicode 编码
于是,为了解决这个问题,就有一个组织跳了出来,给了一个字符编码的终极方案——Unicode,目的就是为了囊括这个世界上所有的字符编码,然后进行统一标准。
Unicode 如今的规模可以容纳 100 多万个符号,无论啥语言都能够一碗水端平,全给编码出来,比如 \u4e2d\u6587 表示的就是“中文”。字符编码问题解决了,但却引来了另一个问题——空间问题。
Unicode 虽好,但每一个字符所占用的空间过大(4 byte)。如果我只写了一篇英语的文章,那么用 Unicode 编码后的文本大小会是 ASCII 码的 4 倍之多,而信息是一模一样的。所以这对于那个存储非常贵的年代是难以接受的,Unicode 的发展也受到了很大的阻力,直到互联网的兴起。
UTF-8 编码
互联网让全世界的人能够互相交流,不同的语种之间的互动和信息存储也就顺势产生了对统一编码方式的强烈渴求,于是 UTF-8,一种基于 Unicode 优化的编码格式出现了。
UTF-8 是目前网络上使用最为普遍的一种编码方式,除此之外还有 UTF-16 和 UTF-32,但是基本不使用。并且 UTF-8 知识 Unicode 的实现方式之一,而非代替者。
UTF-8 是一种变长的编码方式,它可以使用 1~4 个字节表示一个符号,根据不同的符号而变化字节长度。编码规则也十分简单:
-
对于单字节的符号,字节的第一位设为 0,后面 7 位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
-
对于n字节的符号($n > 1$),第一个字节的前 $n$ 位都设为1,第 $n + 1$ 位设为0,后面字节的前两位一律设为 10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
例子:
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
严的 Unicode 是 4E25
(100111000100101
),根据上表,可以发现 4E25 处在第三行的范围内(0000 0800 - 0000 FFFF
),因此严的 UTF-8 编码需要三个字节,即格式是 1110xxxx 10xxxxxx 10xxxxxx
。然后,从严的最后一个二进制位开始,依次从后向前填入格式中的 x,多出的位补 0。这样就得到了严的 UTF-8 编码 11100100 10111000 10100101
,转换成十六进制就是 E4B8A5
。
Little endian 和 Big andian
从上个例子继续,在存储严的 Unicode 码时,需要分为两个字节 4E 和 25 来存储,那么存储顺序就有 4E 在前,25 在后的 Big endian 方式,和25 在前,4E 在后的 Little endian 方式。
这两个古怪的名称来自英国作家斯威夫特的《格列佛游记》。在该书中,小人国里爆发了内战,战争起因是人们争论,吃鸡蛋时究竟是从大头(Big-endian)敲开还是从小头(Little-endian)敲开。为了这件事情,前后爆发了六次战争,一个皇帝送了命,另一个皇帝丢了王位。
因此在计算机中,第一个字节在前就是 Big endian 方式,第一个字节在后就是 Little endian 方式。可计算机怎么知道我们使用的什么存储方式呢?
Unicode 中有这样一个规范,每一个文件的最前面加入一个表示编码顺序的字符,这个字符的名字是“零宽度非换行空格”(zero width no-break space),用 FEFF
表示。这正好是两个字节,而且 FF
比 FE
大 1。
因此,如果一个文本文件的头两个字节是FE FF
,就表示该文件采用Big endian 方式;如果头两个字节是FF FE
,就表示该文件采用Little endian 方式。