详解字符编码与 Unicode

  • 2022 年 9 月 18 日
  • 筆記

人类交流使用 ABC 等字符,但计算机只认识 01。因此,就需要将人类的字符,转换成计算机认识的二进制编码。这个过程就是字符编码。

ASCII

最简单、常用的字符编码就是 ASCII(American Standard Code for Information Interchange,美国信息交换标准代码),它将美国人最常用的 26 个英文字符的大小写和常用的标点符号,编码成 0127 的数字。例如 A 映射成 65 (0x41),这样计算机中就可以用 0100 0001 这组二进制数据,来表示字母 A 了。

ASCII 编码的字符可以分成两类:

  • 控制字符:031127 (0x000x1F0x7F)
  • 可显示字符:32126 (0x200x7E)

具体字符表可以参考:ASCII – 维基百科,自由的百科全书

Unicode

ASCII 只编码了美国常用的 128 个字符。显然不足以满足世界上这么多国家、这么多语言的字符使用。于是各个国家和地区,就都开始对自己需要的字符设计其他编码方案。例如,中国有自己的 GB2312,不够用了之后又扩展了 GBK,还是不够用,又有了 GB18030。欧洲有一系列的 ISO-8859 编码。这样各国人民就都可以在计算机上处理自己的语言文字了。

但每种编码方案,都只考虑了自己用到的字符,没办法跨服交流。如果一篇文档里,同时使用了多种语言的字符,总不能分别指定哪个字符使用了那种编码方式。

如果能统一给世界上的所有字符分配编码,就可以解决跨服交流的问题了,Unicode 就是来干这个事情的。

Unicode 统一编码了世界上大部分的字符,例如将 A 编码成 0x00A1,将 编码成 0x4E2D,将 α 编码成 0x03B1。这样,中国人、美国人、欧洲人,就可以使用同一种编码方式交流了。

一个 Unicode 字符可以使用 U+ 和 4 到 6 个十六进制数字来表示。例如 U+0041 表示字符 AU+4E2D 表示字符 U+03B1 表示字符 α

Unicode 最初编码的范围是 0x00000xFFFF,也就是两个字节,最多 65536 (2^16) 个字符。但随着编码的字符越来越多,两个字节的编码空间已经不够用,因此又引入了 16 个辅助平面,每个辅助平面同样最多包含 65536 个字符。原来的编码范围称为基本平面,也叫第 0 平面。

各平面的字符范围和名称如下表:

平面 字符范围 名称
0 号平面 U+0000U+FFFF 基本多文种平面 (Basic Multilingual Plane, BMP)
1 号平面 U+10000U+1FFFF 多文种补充平面 (Supplementary Multilingual Plane, SMP)
2 号平面 U+20000U+2FFFF 表意文字补充平面 (Supplementary Ideographic Plane, SIP)
3 号平面 U+30000U+3FFFF 表意文字第三平面 (Tertiary Ideographic Plane, TIP)
14 号平面 U+E0000U+EFFFF 特别用途补充平面
15 号平面 U+F0000U+FFFFF 保留作为私人使用区(A 区)(Private Use Area-A, PUA-A)
16 号平面 U+100000U+10FFFF 保留作为私人使用区(B 区)(Private Use Area-B, PUA-B)

每个平面内还会进一步划分成不同的区段。每个平面和区段具体说明参考 Unicode字符平面映射 – 维基百科,自由的百科全书;汉字相关的区段说明参考 中日韩统一表意文字 – 维基百科,自由的百科全书。Unicode 所有字符按平面和区段查找,可以参考 Roadmaps to Unicode;按区域和语言查找可以参考 Unicode Character Code Charts

字符编码的基本概念

“字符编码”是一个模糊、笼统的概念,为了进一步说明字符编码的过程,需要将其拆解为一些更加明确的概念:

字符 (Character)

人类使用的字符。例如:

  • A
  • 等。

编码字符集 (Coded Character Set, CCS)

把一些字符的集合 (Character Set) 中的每个字符 (Character),映射成一个编号或坐标。例如:

  • 在 ASCII 中,把 A 编号为 65 (0x41);
  • 在 Unicode 中,把 编号为 0x4E2D
  • 在 GB2312 中,把 映射到第 54 区第 0 位。

这个映射的编号或坐标,叫做 Code Point。

Unicode 就是一个 CCS。

字符编码表 (Character Encoding Form, CEF)

把 Code Point 转换成特定长度的整型值的序列。这个特定长度的整型值叫做 Code Unit。例如:

  • 在 ASCII 中,0x41 这个 Code Point 会被转换成 0x41 这个 Code Unit;
  • 在 UTF-8 中,0x4E2D 这个 Code Point 会被转换成 0xE4 B8 AD 这三个 Code Unit 的序列。

我们常用的 UTF-8、UTF-16 等,就是 CEF。

字符编码方案 (Character Encoding Scheme, CES)

把 Code Unit 序列转换成字节序列(也就是最终编码后的二进制数据,供计算机使用)。例如 :

  • 0x0041 这个 Code Unit,使用大端序会转换成 0x00 41 两个字节;
  • 使用小端序会转换成 0x41 00 两个字节。

UTF-16 BE、UTF-32 LE 等,就是 CES。


这些概念间的关系如下:

因此,我们说 ASCII 是“字符编码”时,“字符编码”指的是上面从 Character 到字节数组的整个过程。因为 ASCII 足够简单,中间的 Code Point 到 Code Unit,再到字节数组,都是一样的,没必要拆开说。

而我们说 Unicode 是“字符编码”时,“字符编码”其实指的仅是上面的 CCS 部分。

同理,ASCII、Unicode、UTF-8、UTF-16、UTF-16 LE,都可以笼统的叫做“字符编码”,但每个“字符编码”表示的含义都是不同的。可能是 CCS、CEF、CES,也可能是整个过程。

Unicode 转换格式

Unicode 只是把字符映射成了 Code Point (字符编码表,CCS)。将 Code Point 转换成 Code Unit 序列(字符编码表,CEF),再最终将 Code Unit 序列转换成字节序列(字符编码方案,CES),有多种不同的实现方式。这些实现方式叫做 Unicode 转换格式 (Unicode Transformation Format, UTF)。主要包括:

  • UTF-32
  • UTF-16
  • UTF-8

UTF-32

UTF-32 将每个 Unicode Code Point 转换成 1 个 32 位长的 Code Unit。

UTF-32 是固定长度的编码方案,每个 Code Unit 的值就是其 Code Point 的值。例如 0x00 00 00 41 这个 Code Unit,就表示了 0x0041 这个 Code Point。

UTF-32 的一个 Code Unit,需要转换成 4 个字节的序列。因此,有大端序 (UTF-32 BE) 和小端序 (UTF-32 LE) 两种转换方式。

例如 0x00 00 00 41 这个 Code Unit,使用 UTF-32 BE 最终会编码为 0x00 00 00 41;使用 UTF-32 LE 最终会编码为 0x41 00 00 00

UTF-16

UTF-16 将每个 Unicode Code Point 转换成 1 到 2 个 16 位长的 Code Unit。

对于基本平面的 Code Point(0x00000xFFFF),每个 Code Point 转换成 1 个 Code Unit,Code Unit 的值就是其对应 Code Point 的值。例如 0x0041 这个 Code Unit,就表示了 0x0041 这个 Code Point。

对于辅助平面的 Code Point(0x0100000x10FFFF),每个 Code Point 转换成 2 个 Code Unit 的序列。如果还是直接使用 Code Point 数值转换成 Code Unit,就有可能和基本平面的编码重叠。例如 U+010041 如果转换成 0x00010x0041 这两个 Code Unit,解码的时候没办法知道这是 U+010041 一个字符,还是 U+0001U+0041 两个字符。

为了让辅助平面编码的两个 Code Unit,都不与基本平面编码的 Code Unit 重叠,就需要利用基本平面中一个特殊的区段了。基本平面中规定了从 0xD8000xDFFF 之间的区段,是永久保留不映射任何字符的。UTF-16 将辅助平面的 Code Point,编码成一对在这个范围内的 Code Unit,叫做代理对。这样解码的时候,如果解析到某个 Code Unit 在 0xD8000xDFFF 范围内,就知道他不是基本平面的 Code Unit,而是要两个 Code Unit 组合在一起去表示 Code Point。

具体转换方式是:

  1. 将辅助平面的 Code Point 的值 (0x0100000x10FFFF),减去 0x010000,得到 0x000000xFFFFF 范围内的一个数值,也就是最多 20 个比特位的数值
  2. 将前 10 位的值(范围在 0x00000x03FF),加上 0xD800,得到范围在 0xD8000xDBFF 的一个值,作为第一个 Code Unit,称作高位代理或前导代理
  3. 将后 10 位的值(范围在 0x00000x03FF),加上 0xDC00,得到范围在 0xDC000xDFFF 的一个只,作为第二个 Code Unit,称作低位代理或后尾代理

基本平面中的 0xD8000xDBFF0xDC000xDFFF 这两个区段,也分别叫做 UTF-16 高半区 (High-half zone of UTF-16) 和 UTF-16 低半区 (Low-half zone of UTF-16)。

UTF-16 的一个 Code Unit,需要转换成 2 个字节的序列。因此,有大端序 (UTF-16 BE) 和小端序 (UTF-16 LE) 两种转换方式。

例如 0x0041 这个 Code Unit,使用 UTF-16 BE 最终会编码为 0x0041;使用 UTF-16 LE 最终会编码为 0x4100

UTF-8

UTF-8 将每个 Unicode Code Point 转换成 1 到 4 个 8 位长的 Code Unit。

UTF-8 是不定长的编码方案,使用前缀来标识 Code Unit 序列的长度。解码时,根据前缀,就知道该将哪几个 Code Unit 组合在一起解析成一个 Code Point 了。

具体编码方式是:

Code Point 范围 Code Unit 个数 每个 Code Unit 前缀 示例 Code Point 示例 Code Unit 序列
7 位以内 (00xEF) 1 0b0 0b0zzz zzzz 0b0zzz zzzz
8 到 11 位 (0x800x07FF) 2 第一个 0b110,剩下的 0b10 0b0yyy yyzz zzzz 0b110y yyyy 10zz zzzz
12 到 16 位 (0x08000xFFFF) 3 第一个 0b1110,剩下的 0b10 0bxxxx yyyy yyzz zzzz 0b1110 xxxx 10yy yyyy 10zz zzzz
17 到 21 位 (0x1000010FFFF) 4 第一个 0b11110,剩下的 0b10 0b000w wwxx xxxx yyyy yyzz zzzz 0b1111 0www 10xx xxxx 10yy yyyy 10zz zzzz

解码时,拿到每个 Code Unit 的前缀,就知道这是对应第几个 Code Unit:

  • 前缀是 0b0,说明这个 Code Point 是一个 Code Unit 组成
  • 前缀是 0b110,说明这个 Code Point 是两个 Code Unit 组成,后面还会有 1 个 0b10 前缀的 Code Unit
  • 前缀是 0b1110,说明这个 Code Point 是三个 Code Unit 组成,后面还会有 2 个 0b10 前缀的 Code Unit
  • 前缀是 0b11110,说明这个 Code Point 是四个 Code Unit 组成,后面还会有 3 个 0b10 前缀的 Code Unit

UTF-8 的一个 Code Unit,刚好转换成 1 个字节,因此不需要考虑字节序。

参考上表,对于 ASCII 范围内的字符,使用 ASCII 和 UTF-8 编码的结果是一样的。所以 UTF-8 是 ASCII 的超集,使用 ASCII 编码的字节流也可以使用 UTF-8 解码。

UTF-8 与 UTF-16 对比

Code Point 范围 UTF-8 编码长度 UTF-16 编码长度
7 位以内 (0x000xEF) 1 2
8 到 11 位 (0x00800x07FF) 2 2
12 到 16 位 (0x08000xFFFF) 3 2
17 到 21 位 (0x1000010FFFF) 4 4

可以看出只有在 0x000xEF 范围的字符,UTF-8 编码比 UTF-16 短;而在 0x08000xFFFF 范围内,UTF-8 编码是比 UTF-16 长的。

而中文主要在 0x4E000x9FFF,如果写一篇文档,全都是中文,一个英文字母和符号都没有。那使用 UTF-8 编码,可能比 UTF-16 编码还要多占用一半的空间。


相关文章: