0%

再探编码

前言

计算机发展本身就是多元化的,而且标准或者说字符集具有地域性,再加上计算机系统的历史遗留性(没错,那边的 Windows 说的就是你),我觉得编码一直是一个很难的问题。

在我的博客还是 WordPress 的时候我曾经写过一篇讲编码的文章,但是实际上那时候我也是一知半解,写出来的东西也是浅薄无趣的。

然而学CS,一知半解是最恐怖的,正好最近就在各种踩坑,所以不如一次弄明白算了,也算是解决长久以来的一块心病了。

所以本文会尝试去理清各种字符集以及编码之间的关系,如果有任何错误还请指出。

字符集

字符集简单来说就是一个映射,通常情况下是一个整数到某个特定的字符的双射,比如在 ASCII 字符集中,'A' 对应的是 65, 反过来 65 也对应的是 'A'。

字符集概念很简单,但是有两个问题。

  • 我知道一个字符集中 65 对应 'A',反之亦然,那我可以确定它是 ASCII 吗?
  • 我在 C++ 中写了 char a = 'A' 然后发现变量 a 的值真的是 65,那是不是所有字符集中字符都是由它相对应的整数表示?

对于第一个问题,答案当然是否。因为字符集一般都是向前兼容的,比如 Unicode 中 'A' 也是 65。

对于第二个问题,答案也是否。因为当字符集太大的时候,如果都这样表示,那么会浪费相当大的空间,我们在后面的 UTF 会看到如何解决这个问题。

简单概括下,字符集就是字符和整数的映射,它不代表计算机内部的实际编码表示。

ASCII

ASCII 全称 American Standard Code for Information Interchange,它只有 8bit 长,但是实际上编码只用了 7bit,还有一位一般是用来奇偶校验(因为很多电子通信也用 ASCII)。

具体的 ASCII 表这里不再给出,只强调一点就是绝大部分字符集和编码方式都是兼容 ASCII 的。

Extend ASCII(扩展 ASCII)

当计算机走向世界的时候,人们首先发现的问题就是 ASCII 不够用了。

但是正如我之前说过的,编码具有很强的历史性,改变编码方式很难一次到位,所以扩展 ASCII 就出现了,它的做法很简单:把原来 ASCII 中不参与编码的最高位拿出来用于编码,这样原来 ASCII 范围 0~127 就变成了扩展 ASCII 的0~255。

但是这里有个问题是扩展 ASCII 不像 ASCII 那样只有一个标准,也就是说扩展 ASCII 实际上有很多种,不过最后被采用的是 ISO 8859,由于 Windows 中实现要比最终标准化早一些,所以 Windows 中对应的 Windows-1252 实际上是 ISO 8859 的超集。

到这里问题还不大,但是 Windows 为自己埋下了祸根。

ANSI

首先这里要纠正一个错误的认知:没有任何一种编码方式/字符集叫做 ANSI。

ANSI 已经很累了,不想再背锅了。

ANSI 全称是 American National Standards Institute,是一个标准化组织,比如上面的 ISO 8859 就是它跟 ISO 一起提出来的。

那么当我们在谈 ANSI 编码的时候,我们在谈什么?

Windows Code Page(Windows 代码页)

实际上,一般说 ANSI 编码实际上想提到的就是 Windows 代码页。

换句话说,我们在说 ANSI 编码的时候,很大可能想表达的是某个国家特定的字符集和它所代表的代码页。

比如中文系统使用的 GBK 在 Windows 中的代码页是 cp936,一般我们在中国说 ANSI 编码可能指的就是 GBK,而上面的 Windows-1252 就是代码页 cp1252,表示的就是相应的扩展 ASCII。

但是代码页可以说是 Windows 中最致命的残留之一,这个问题直到 Windows10 1803 才有了一定程度的解决,但是由于 Windows 高度的向前兼容性,这个问题恐怕在不久的将来还会一直存在,除非 NT 推倒重来。

这里引用 cppreference 的一段话来说明这个问题

wchar_t - type for wide character representation (see wide strings). Required to be large enough to represent any supported character code point (32 bits on systems that support Unicode. A notable exception is Windows, where wchar_t is 16 bits and holds UTF-16 code units) It has the same size, signedness, and alignment as one of the integer types, but is a distinct type.

这里 wchar_t 在 Windows 上只有 16bit 的根源就在于代码页的设计。

在微软最初的设计里,代码页只有两种 Single-Byte Character Set(SBCS) 和 Double-Byte Character Set(DBCS),比如上面的 cp1252 就是一个 SBCS,中文系统使用的 cp936 就是一个 DBCS。

当然现在微软也有 UTF-8 编码的代码页,不过那都是后话了。

GB(国标)

GB2312

扩展 ASCII 还是太少了,比如中文,255 个字怎么可能够用嘛,隔壁同源的日文也是同理。

所以国家这时候就开始着手设计自己的字符集了,第一个国标就是 GB2312。

GB2312 采用两个字节编码,收录了绝大多数常用的汉字。

GBK

但是 GB2312 只收录了约 7000 个汉字,甚至连朱镕基的“镕”都没有,而且台湾港澳台使用的字体也没有收录,所以就有了 GBK,即 “GB2312扩”。

它仍然采用两个字节编码,兼容 GB2312 的同时收录了日本、台湾和韩国等通用字符集的汉字。

Windows 中的 936 号代码页实际上几乎就是 GBK。

但是随着计算机技术的发展,GBK 仍然无法满足需要,而且 GBK 实际上并不是国家标准,所以后来接盘的是 GB18030,这里我们先停下来,看一看隔壁的 Unicode。

Unicode

随着 Internet 的发展,编码的问题愈发突出,如何让文本能正确的显示在显示器上成了一个难题,Unicode 就是在这种背景下出现的。

最新的 Unicode 标注一共收录了约 14 万个字符,基本涵盖了世界上绝大数语言。

从 Unicode 起,另外一个问题提了出来:有了字符集,如何编码让它更适合计算机使用或者网络传输呢?

之前的单字节和双字节编码因为占用空间少,所以直接不加任何编码就可以使用,但是 Unicode 可是有 14 万个,如果还采用定长编码的话至少需要 18bit 才能保证一一对应,这样的话对于之前 ASCII 中就有的字符来说有 11bit 就被浪费了,显然是不合理的。

所以这里字符集和编码方式就必须分开考虑了,这也是为什么会有 Unicode Transformation Format(UTF) 一说了。

简单来说,Unicode 还是那个 Unicode,但是编码方式会有很多种。

UTF-7

UTF-7 已经不属于 Unicode 标准了,但是它还属于 RFC 标准。

简单来说,UTF-7 对于 ASCII 不做改变,但是对于非 ASCII 字符采用 base64 编码后在首位加上 + 和 - 用于区分,这样编码出的字符串中所有字符就是标准 ASCII 了。

如果做过 XSS 或者 SQL 注入的应该对 UTF-7 有印象。

UTF-8

UTF-8 是最常用的编码,也是 Web 中几乎统治性的编码方式。

UTF-8 是一种变长编码方式,它兼容 ASCII 最小可以用 1byte 表示,同时最多可以用 4byte 表示所有的 Unicode 字符。

UTF-8 编码方式其实很好理解,对于某个字符首先取出它的二进制表示:

  • 如果小于等于 7 位(ASCII),那么直接表示为 0xxxxxxx
  • 如果小于等于 11 位但是大于 7 位,那么表示为 110 xxxxx 10 xxxxxx
  • 如果小于等于 16 位但是大于 11 位,那么表示为 1110 xxxx 10 xxxxxx 10 xxxxxx
  • 如果小于等于 21 位但是大于 16 位,那么表示为 11110 xxx 10 xxxxxx 10 xxxxxx 10 xxxxxx

刚才提到过,最新的 Unicode 标准只要 18bit 就可以全部表示,所以 UTF-8 目前是可以表示 Unicode 全部字符的。

这里举个例子,中文的“中”字,它的 Unicode 为