算法与数据结构 / regex · Unicode 字符模型与一个自研引擎 / 中文编码:GB2312 / GBK / GB18030 与 Big5 待审核 2 / 36
番外 · Unicode 之前的中文编码

中文编码:GB2312 / GBK / GB18030 与 Big5

字符的编码 讲的是今天怎么把字符变成字节(UTF-8 / UTF-16)。但在 Unicode 普及之前,中文早已要在计算机里落进字节——靠的是一套套国家 / 地区标准编码:大陆的 GB2312 → GBK → GB18030(一脉相承,后者是前者的超集),台 / 港的 Big5(另起一套,收繁体)。它们都把 ASCII 留作单字节、用双字节装汉字,GB18030 还多出 1 / 2 / 4 字节变长。

1 · 同一个字的五种字节

首字符会按它在四套编码里的落点拆解:ASCII、GB2312 区位码、GBK 扩展字,或只有 GB18030 装得下的 4 字节。

图 1-1 · 同一个字在五种编码下各自的字节,以及首字符的逐步拆解。可输入任意字或点预设,「镕」演示 GBK 低字节坑,「㐀」「😀」演示 GB18030 的 4 字节区。

注 · 上图 GB2312 那一列按码位区段判定——某字的 GBK 字节若落在 EUC-CN 区(首字节 0xA1–0xF7、尾字节 0xA1–0xFE)就算「在 GB2312」。对汉字这判定是精确的(GBK 没在汉字区动过 GB2312 的码位);但 GB2312 的符号区原本留有空位,GBK 往里补过个别字符(如欧元 是 GBK 才加的,严格不属 1980 年的 GB 2312–80)。另外现代浏览器与标准里 gb2312 这个 charset 标签实际走 GBK 解码器,解码层面二者已不区分。

2 · GB2312 的区位码

GB2312 把要收的字符排进一张 94 区 × 94 位的方表(区位码):区 01–09 放标点 / 数字 / 字母等图形符号,区 16–55 放 3755 个一级汉字(按拼音排),区 56–87 放 3008 个二级汉字(按部首排)。存进机器时按 EUC-CN 规则,把区号、位号各加 0xA0 得到两个字节——加 0xA0 是为了让两个字节的最高位都为 1,这样它们绝不会和 0x00–0x7F 的单字节 ASCII 撞上,一段混排文本里「这是汉字的字节还是 ASCII」一眼可分。

3 · 谁是谁的超集

把四套编码的字节布局并排,就能看清「GB2312 ⊂ GBK ⊂ GB18030」这条线,以及 Big5 为何与它们互不相通。

表 3-1 · 五种编码的字节范围与收录情况。
编码 首字节 后续字节 字节数 收录
ASCII 00–7F 1 四套编码都把它留作单字节,完全兼容
GB2312(1980) A1–F7 A1–FE 2 简体 6763 汉字 + 682 符号
GBK(1995) 81–FE 40–FE(除 7F 2 全 20902 个 CJK(简 + 繁),GB2312 的超集
GB18030(2000+) 81–FE 2 字节同 GBK;4 字节:30–39 / 81–FE / 30–39 1 / 2 / 4 全部 Unicode,GBK 的超集
Big5(1984) 81–FE 40–7EA1–FE 2 繁体 13053 汉字 + 符号(台 / 港),另起一套

警示 · GBK 打开了「低字节坑」。GB2312 两个字节都不小于 0xA1;GBK 为了塞下两万多字,把尾字节范围下探到 0x40–0x7E——这一段和 ASCII 的字母 / 符号重叠。于是一个 GBK 汉字的第二个字节可能正好是 0x5C\)、0x46F)这类。把字节当 ASCII 逐个处理(比如按 \ 转义、按路径分隔符切分)的程序,就会把半个汉字误读成一个 ASCII 字符,典型的「\ 吃字」乱码。在上面的 lab 里输入「镕」即可看到它的第二个字节。

4 · GB18030 存在的理由

GBK 已经收了两万多汉字,看着够用,但它把容量用尽在双字节的 BMP 汉字上了,装不下两类东西:其一,Unicode 后来在补充平面追加的大批 CJK 扩展字(扩展 B 及以后,> U+FFFF)与 emoji;其二,中国境内的少数民族文字(藏文、蒙古文、维吾尔文、彝文……)以及 Unicode 里其余所有文字。于是需要一套编码同时满足两个看似冲突的要求:向后兼容已经铺开的 GBK 数据,老文件、老系统的字节一个都不用改;同时能无损往返全部 Unicode 码点(U+0000 – U+10FFFF)。

UTF-8 满足第二点,却不满足第一点——它的汉字字节和 GBK 完全不同。GB18030 的解法是:ASCII 仍 1 字节、原 GBK 字符仍用那 2 个字节,凡是这两者装不下的,一律用 4 字节。4 字节区与 Unicode 码空间一一映射,所以它本质是「Unicode 的一种中文味变换格式」,类似 UTF 但字节兼容 GBK。再加上它是强制性国家标准(在中国销售的操作系统 / 软件必须支持),GB18030 就成了既背得动历史包袱、又面向全 Unicode 的那个答案。

建议 · 认 4 字节序列有个简便特征。GB18030 的 4 字节形如 [81–FE] [30–39] [81–FE] [30–39]——第 2、4 个字节恒落在 0x30–0x39,正好是数字 0–9 的字节。补充平面字符(U+10000–U+10FFFF)更是线性映射:U+10000 = 90 30 81 30,一路连续到 U+10FFFF = E3 32 9A 35

5 · 跨编码乱码

GB 系和 Big5 是两套互不相通的码表:同样的字节,按 GBK 解是一个意思,按 Big5 解就是另一批字。一段文本若丢失了「我是什么编码」的声明,收方只能猜——猜错就是满屏乱码。这正是两岸三地早年传文件最常见的麻烦,也是 <meta charset> 与 HTTP Content-Type 必须带 charset 的原因。

图 5-1 · 一段中文的 GBK 字节流分别按 GBK 与 Big5 解码的结果。可换输入观察不同的乱码组合。

6 · 一条时间线

图 6-1 · 从 GB 2312–80 到 GB 18030–2022 的编码沿革,以及 Unicode 1.0 在其中的位置。

注 · 结局是世界收敛到 UTF-8。编码各自为政的代价就是上面那种乱码,以及每个程序都得猜 charset。Unicode 给了所有字符统一的 code point,UTF-8 给了统一的字节方案且向下兼容 ASCII,于是网页、源码、协议几乎全面转向 UTF-8。GB18030 仍在(强制国标、政务系统、需要和 GBK 老数据兼容的场景),但它和 UTF-8 一样,本质都是 Unicode 的一种字节表示——回到 字符的编码:逻辑层永远是那个 U+XXXX,变的只是存储层怎么拆字节。

7 · 参考文献

  1. WHATWG. Encoding Standard. 定义了浏览器实际使用的 gbk / gb18030 / big5 解码表与 gb2312 → gbk 等标签别名——本页的字节就是用它规定的 TextDecoder 反查出来的。encoding.spec.whatwg.org
  2. Wikipedia. GB 18030. 完整的码表布局与版本沿革。en.wikipedia.org
  3. Wikipedia. Big5. 台港繁体编码的布局与厂商扩展。en.wikipedia.org