My avatar, a blue cat cartoon picture

彻底理解 ASCII Unicode UTF-8 UTF-32 是什么以及区别与联系



BasicComputerKnowledge

1. 什么是 ASCII,它是如何出现的?

计算机中的数据都是 0 和 1,不管是在 RAM 还是 ROM 中。我们常用的十进制的数字会转化成二进制保存,比如 26 会转化成二进制数 11010。

那么英文字符(英文字母与英文符号)呢?

解决方法是一个约定好的在字符和数字间的映射。最流行的是 ASCII:

图 1
图 1

有了它,西方基础的字符都可以映射成数字了,在 0-127 之间。即 ASCII 可以表达 128 个不同的字符。

例如 Hello 转换为为二进制的过程(叫做编码 Encoding):

图 2
图 2

最后把这些二进制数字拼接起来存储即可。每一个字符都会转换成一个 8 位的二进制数字,称为 1 字节(8 bits,1 byte)。每个字符占 1 个字节。

2. 什么是 Unicode,它是如何出现的?它与 ASCII 的区别与联系是什么?

除了英文字符,其他的文字和符号怎么办?例如中文有几万个汉字,而不止是 26 个字母,用 ASCII 肯定无法表示。

Unicode 标准就出来了,包含 100 多种语言中的超过 10 万个独特字符。中文等语言,emoji 表情等都可以用它表示。

Unicode 比 ASCII 复杂很多,在描述 Unicode 时,我们把描述 ASCII 时用的字符(character)改为字素(grapheme)。一个字素就是一个单字,是人类书写系统的单一单位,例如 "y","你","あ","ية" 都是一个字素。英文单词不是字素,因为它们能够被拆分成多个字母,而每个字母是一个字素

为了表示字素,我们可以引入编码点(Code Point)一个或多个编码点组合在一起表示一个字素。

例如 "d" 和 "你" 都可以用一个编码点表示:

图 3
图 3

更复杂的字素,例如带音调的字母,可以有两种方式表示:

图 4
图 4

第二种方式显然是把 e 与音调符号拼接起来。

现在讨论的是如何将字素映射到编码点,这里有更多的例子:

图 5
图 5

每一个编码点都有一个名字和一个。这个值可以转换成二进制的字节,这就是编码(Encoding)

例如 d 编码点的名字就是 Latin Small Letter D,值是 100。

UTF-8、UTF-32 等是编码策略,它们与 ASCII 的区别

ASCII 可以直接把值转换为二进制:

图 6
图 6

Unicode 有很多个编码策略,它们各有优劣。

UTF-32

比如 UTF-32,它将每一个编码点的值编码为 4 个字节,即 32 位。所以这个策略的名字叫 UTF-32。

图 7
图 7

这很像 ASCII,区别就是 ASCII 将一个 ASCII 值编码为 1 个字节,但 UTF-32 将一个编码点的值编码为 4 个字节,多占了 4 倍空间。

这是一个 Unicode 字符串 "Hello!👍🏿" 编码为 UTF-32 的例子:为了简洁,该图将 4 字节的二进制转换成 16 进制了。

图 8
图 8

这种编码的优点是每一个编码点都占同样大的字节,无论这个编码点的值是什么。

图 9
图 9

例如前 4 个字素 "Hell",H 的编码从是从第 0 个字节到第 3 个字节,e 的是从第 4 个字节到第 7 个字节,等等。可以直接从编码的结果中找到各个字素的索引位置。

缺点也很明显,这种编码方式很浪费存储空间:

图 10
图 10

这是 "Hello world!" 字符串分别在 ASCII 和 UTF-32 编码下的编码结果,UTF-32 是 ASCII 结果的 4 倍大小。特别是这种纯英文字符,完全可以兼容 ASCII 的字符串,看图里有多少 00 就知道有多么浪费了。无论每个编码点的值是大是小,均占 4 个字节。

UTF-8

UTF-8 并不是把每个编码点编码为 8 位,而是编码为 8 - 32 位,即 1 到 4 个字节。编码点的值很小的时候,只编码为 1 个字节,大的时候就编码为 2-4 个字节,可以节省大量空间。

图 11
图 11

像 "d" 这种简单的西方字母,UTF-8 和 ASCII 编码的结果是相同的,因为它的 Unicode 的值与它的 ASCII 的值是相同的,都是 100,所以对值的编码结果也是相同的。

这有一个很大的兼容性优势,旧的 ASCII 程序可以读简单字符的 UTF-8 编码,甚至不需要知道它是 UTF-8 编码。

UTF-8 也有缺点,每个编码点编码结果可能不一样长,字节数不同,就很难在编码结果中找到每个编码点的索引。

图 12
图 12

这对性能有一些影响,但是通常不用担心。

UTF-8 是 Unicode 被采用最多的编码策略。

有时候你可能觉得这对其它语言不太公平,但是这问题不大,例如一个阿拉伯语的网页,它的 HTML 代码中大部分还是英文,UTF-8 编码后的大部分字素都只占 1 个字节,所以总体上并不会多消耗太多流量和存储空间。

图 13
图 13

总结

稍微复习一下,一个字素(grapheme)是人类手写的一个单位,由一个或多个编码点(code point)组成。然后使用几种编码方案中的一种对每个编码点进行编码。例如使用 UTF-8,每个编码点会被编码成 1-4 个字节。

图 14
图 14

Unicode 数据比 ASCII 数据复杂很多:

图 15
图 15

如果把 UTF-8 编码结果当作 ASCII 或 UTF-32 的编码结果来解析(即使用 ASCII 或 UTF-32 策略来解码),就会造成我们常见的乱码

图 16
图 16

我们可以总结成几条:

1. 我们必须知道原始的编码规则才能把字节解码为字素

2. 在 Unicode 中,1 个字素 ≠ 一个编码点 ≠ 1 个字节

例如在 Python 中,直接把 👍 作为一个字符串,会是这样:

图 17
图 17

它的长度是 4 个字符,每一个字符都是一个两位 16 进制的编码。这叫做"不可感知 Unicode 的函数(Unicode-unaware function)"。

但如果这样写,在字符串前加一个 u

图 18
图 18

字符串的长度就是 1 了,这被叫做"可感知 Unicode 的函数(Unicode-aware function)",因为它计算字符串长度时计算的是 Unicode 字符个数。

这个字素只有一个编码点,再看一个由两个编码点组成的字素👍🏿:

图 19
图 19

这个字符串的长度就为 2 了。

在各种编程语言中很容易混淆,容易出问题,因为各种编程语言对字符串的处理方式都是不同的,例如:

图 20
图 20

这段 python 程序定义了一个 truncate_with_ellipsis() 方法,输入一个字符串和 max_length 参数,如果字符串的长度超过了 max_length,就截取到 max_length-3 的位置,再补一个省略号。

Python 中在字符串前加 u 即可定义 Unicode 的字符串。在 Python 中,是否是 Unicode 字符串直接影响了这个函数执行的结果。

可以看到 👍 在 Python Unicode 字符串中占 1 个长度,👍🏿 占两个。

而在我常用的 C# 中:

图 21
图 21

可以发现 👍 在字符串中占 2 个长度,👍🏿 占 4 个长度,汉字"一"占 1 个长度。

C# 中默认是 Unicode 字符串,但为什么与 Python 中的 Unicode 字符串中各个字素所占的长度不一样呢?这就是 Unicode 各个编码策略不同导致的。

C# 中 string 的 Length 属性返回的是字符串的字符个数(一个字符就是一个 character,是一个 Unicode 字符),而 .NET 字符串是 UTF-16 编码,所以 Length 实际返回的是 UTF-16 编码后的 Unicode 字符个数。

看一下它们在 go 中有多长:

图 22
图 22

在 go 中,字母占 1 个长度,👍 占 4 个长度,👍🏿 占 8 个长度,汉字"一"占 3 个长度。go 中字符串的 len() 方法返回值是字符串在 UTF-8 编码下的字节数。

对日常使用多种编程语言的开发者来说,不搞清楚编码的这些逻辑,真的很搞人的心态。

为了防止读完文章后在各种编码和字节长度中迷迷糊糊,这个网页给我们 👍 在各种编码策略下的结果:https://www.fileformat.info/info/unicode/char/1f44d/index.htm

如何理解应试教育中教的 ASCII 与 Unicode 与 UTF-8

在应试教育中,我们常常被告知什么字符代码,字符编码,接连便是难懂的话,什么“字符集”,什么“编码方案”之类,引得众人都哄笑起来,店内外充满了快活的空气。

从应试教育的方向来解释,需要被编码的文本,例如 1234abcd!@#$一二三四👍,为了把最早期的英文字母,数字,英文符号等字符映射到二进制 0 和 1,就有了 ASCII,它是字符集到 0 和 1 之间的映射关系表,它能支持 128 个字符,很够用。

看 ASCII 的表就知道,它就是给了各个字符一个序号(索引号),规定这个序号就是这个字符。我们把这个号称为"字符代码"。Unicode 的字符代码就是编码点(Code Point)的值。

后面各个国家的各个语言都需要映射到 0 和 1,就有了各自的规则,比如中文的 GB2312,中日韩汉字繁体字的 GBK,它们跟 ASCII 一样,都是映射规则。最后 ISO 看不下去了,搞出了 Unicode,搞了个包含所有字素的 Unicode 字符集,还规定了一套编码(Encoding)规范。编码是把字素转换为 0 和 1 的过程,是把"字符代码"编码为"字符编码"的过程。

ASCII 与 Unicode 都是"字符代码",ASCII 的编码策略也是 ASCII,ASCII 中字符代码和字符编码是一致的。Unicode 的编码策略就比较多了,UTF-8、UTF-16 等都是。

参考资料 https://www.youtube.com/watch?v=ut74oHojxqo