Unicode 及其编码方案

published

前言

Unicode 标准有上千页,还有几十页的补充附录、报告和注解。想要深入了解 Unicode,确实要下些功夫。

不过,本文不准备深入地讲述 Unicode 相关的细节,只准备简要讲述 Unicode 编码相关的内容,以满足日常编程中处理 Unicode 字符编码的需求。

预备知识

字符

各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。

编码模型

  • Character Repertoire:字符组成的列表。
  • Coded Character Set(CCS):将字符与唯一数值的关联起来的映射。其中的唯一数值称为 Code Point。

Unicode 是这一层的概念。

  • Character Encoding Form(CEF):将由一个或多个 Code Unit(定长的 Bit 序列)组成的序列与 Code Point 关联起来的映射。

UTF-8 / UTF-16 / UTF-32 是这一层的概念。

  • Character Encoding Scheme(CES):将 Code Unit 与 Octet 关联起来的映射。CEF 是为了满足基于 Octet 的文件系统的存储需求和基于 Octet 的网络的传输需求。

Octet 是什么?Octet 表示 8 Bit 的二进制流。那为什么不用 Byte?Byte 不一定是 8 Bit,只是现代计算机的事实标准使用 8 Bit 来代表一个 Byte。在很多技术规格文献中,为了避免产生歧义,更倾向于使用 Octet 这个术语来强调 8 Bit 的二进制流。

更多细节可以阅读 WIKIPEDIA 中关于字符编码的解释

大小端

大小端的说法源自《格列佛游记》 鸡蛋通常一端大一端小,小人国的人们对于剥蛋壳时应从哪一端开始剥起有着不一样的看法。

同样,人们对于传输多字节数据时,是先传高位字节(大端)还是先传低位字节(小端)也有着不一样的看法,这就是「大小端」的由来。

对于多字节数据来说,如果先操作高位字节,则称为大端模式;反之,则称为小端模式。

Unicode

Unicode - A computing industry standard for providing a unique code point for each character.

Unicode 中的 Code Point

预备知识中提到了 Code Point 的概念,这里不再解释。

Unicode 中的 Code Point 通常使用 U+Hex 的形式表示,比如:

  • 拉丁字母 A 的 Code Point 为 U+0041
  • 希腊字母 θ 的 Code Point 为 U+03B8
  • 中文单字 我 的 Code Point 为 U+6211

Unicode 中的 Plane

Plane 译为平面。

根据字符的使用频率,Unicode 被划分为了 17 个 Plane,每个 Plane 中包含 2^16 个 Code Point。

通过 0 ~ 16 这 17 个十进制数为 Plane 编号:

  • Basic(空间范围:U+0000 ~ U+FFFF):
    • Plane 0 - Basic Multilingual Plane(BMP)。其中包含绝大多数现代字符,比如拉丁文、斯拉夫文、希腊文、汉字(中国),日文、朝鲜文、阿拉伯文、希伯来文、梵文(印度)等。
    • U+D800U+DFFF:代理区。
    • U+E000U+F8FF:未被使用,留给第三方定义私有字符,以避免和 Unicode 冲突。
  • Supplementary(空间范围:U+010000 ~ U+10FFFF):
    • Plane 1 - Supplementary Multilingual Plane(SMP)。其中包含历史上的文字,比如苏美尔楔形文字、埃及象形文字、Emoji 以及其他符号。
    • Plane 2 - Supplementary Ideographic Plane(SIP)。其中包含不常用的和历史遗留的汉字字符。
    • Plane 3 ~ 13 - 未被使用。
    • Plane 14 - Supplement Special-purpose Plane(SSP)。其中包含少量用于格式化的字符。
    • Plane 15 - Supplement Private Use Area Plane A(SPUA-A)。未被使用,留给第三方定义私有字符,以避免和 Unicode 冲突。
    • Plane 16 - Supplement Private Use Area Plane B(SPUA-B)。未被使用,留给第三方定义私有字符,以避免和 Unicode 冲突。

下图为 Unicode Plane 的整体布局,从左到右,从上至下,编号依次为 0 ~ 16:

Unicode Plane

其中:

  • 一个像素表示 1 个 Code Point
  • 一个小方块表示 2^8 个 Code Point(只为保证视觉一致性,无特殊意义)
  • 一个大方块表示一个 Plane,其中包含 2^16 个 Code Point
  • 白色表示未用空间
  • 蓝色表示已用空间
  • 绿色表示自用空间
  • 红色区域表示代理区,空间范围 U+D800U+DFFF(描述 UTF-16 编码方案的时候会提及)

Unicode 编码方案

Unicode 只是定义了一个庞大的、全球通用的字符集,并为每个字符规定了唯一确定的 Code Point,而 Unicode 字符如何存取,Unicode 是不关心的。

为了解决 Unicode 字符编码的问题,引入了 Unicode 编码方案。Unicode 编码方案中比较流行的是 Unicode Transformation Formats(UTF)。

UTF 会以数字作为后缀,如 UTF-8 / UTF-16 / UTF-32。其中的数字表示 Code Unit 的大小,也就是 Code Unit 对应的 Bit 序列的长度(在预备知识中提到,Code Unit 是定长的 Bit 序列)。对 UTF-8 来言,Code Unit 的大小是 8;对 UTF-16 而言,Code Unit 的大小是 16;诸如此类。

UTF-32

UTF-32 中,每个 Code Point 使用 4 个字节表示,字节内容与 Code Point 一一对应。这是最简单直接的编码方案。

举例:

字符 Code Point 编码
A U+0041 0x0000 0041
θ U+03B8 0x0000 03B8
U+6211 0x0000 6211
𠀾 U+2003E 0x0002 003E

但是,使用四个字节来存储单个字符会极大的浪费存储空间。由于这个缺点,这种编码方案并不常用。

UTF-16

UTF-16 中,每个 Code Point 使用 2 个字节或 4 个字节表示。

Code Point 范围 占用字节数量
U+0000 - U+FFFF 2
U+010000 - U+10FFFF 4

UTF-16 编码方案中,涉及到一个概念 —— 代理区。代理区位于 BMP 中,空间范围 U+D800U+DFFF,空间大小 2^11,用于映射 Supplementary Plane 中的所有 Code Point。

Q:代理区空间大小 2^11,如何做到映射空间大小 2^20 的 Supplementary Plane?
A:将 U+D800U+DFFF(2^11) 这个范围内的值分为两部分:

  • U+D800U+DBFF(空间大小 2^10),称为高位(H)
  • U+DC00U+DFFF(空间大小 2^10),称为低位(L)

对高位和低位做迪卡尔积,可以得到 2^20 种组合,这样就可以映射 Supplementary Plane 了。

Code Point 编码方法

将 Code Point 编码为 Code Unit。

  • U+0000U+FFFF 范围内的 Code Point,直接用 2 个字节表示。
  • U+010000U+10FFFF 范围内的 Code Point 通过以下形式编码,再用 4 个字节表示:
    1. Code Point 减去 0x10000,获得一个在 0x000000xFFFFF 范围中的值,这个值可以转换为 20 位的二进制数。
    2. 20 位的二进制数的高 10 位与 0xD800 相加,获得高位。
    3. 20 位的二进制数的低 10 位与 0xDC00 相加,获得低位。
    4. 高位与低位组合后,刚好可以用 4 个字节表示。

Code Unit 解码方法

将 Code Unit 解码为 Code Point。

因为有了高低位,解析字节流时就涉及到了预备知识里提到的大小端问题。

在解析使用 UTF-16 大端编码的字节流时,首先判断 Code Unit 是否在 U+D800U+DBFF 范围内:

  • 如果是,则与其后相邻的 Code Unit 放在一起解码。
  • 如果不是,直接解码。

在解析使用 UTF-16 小端编码的字节流时,首先判断 Code Unit 是否在 U+DC00U+DFFF 范围内:

  • 如果是,则与其后相邻的 Code Unit 放在一起解码。
  • 如果不是,直接解码。

UTF-8

UTF-8 中,每个 Code Point 使用 1 ~ 4 个字节表示。

Code Point 范围 占用字节数量 Code Point 位数 字节 1 字节 2 字节 3 字节 4
U+0000 - U+007F 1 7 0xxxxxxx
U+0080 - U+07FF 2 11 110xxxxx 10xxxxxx
U+0800 - U+FFFF 3 16 1110xxxx 10xxxxxx 10xxxxxx
U+010000 - U+10FFFF 4 21 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

UTF-8 是一种变长的编码方法,字符长度从 1 个字节到 4 个字节不等。越是常用的字符,字节越短,最前面的 128 个字符,只使用 1 个字节表示,与 ASCII 码完全相同。由于 UTF-8 很节省空间,所以它成为了互联网上最常见的网页编码。

Code Point 编码方法

将 Code Point 编码为 Code Unit。

  • U+0000U+007F 范围内的 Code Point,使用 1 个字节表示,最高位设为 0,低 7 位设为对应的 Code Point。
  • U+0080U+10FFFF 范围内的 Code Point,使用 2 ~ 4 个字节表示。设当前所使用的字节数为 n,第一个字节的前高 n 位设为 1,第高 n + 1 位设为 0,后续字节的高 2 位均设为 10。剩余没有提及的二进制位,从低位到高位依次填入对应的 Code Point,空位补 0。

编码方法看似复杂,其实非常简单。以汉字 为例:

  • 汉字 对应的 Code Point 为 U+6211,转换为二进制 110001000010001
  • U+6211U+0800U+FFFF 间,使用 3 个字节表示,具体格式为 1110xxxx10xxxxxx10xxxxxx
  • 从低位到高位依次填入对应的 Code Point,空位补 0。得到对应 UTF-8 编码 111001101000100010010001,转换为十六进制为 E6 88 91

Code Unit 的解码方法

将 Code Unit 解码为 Code Point。

  • Code Unit 最高位为 0,直接解码。
  • Code Unit 最高位为 1,统计连续的 1 的数量,以获得需要组合解码的 Code Unit 数量。根据 Code Unit 数量读取所需 Code Unit,截取 Code Unit 中的有效位,进行连接,以获取对应 Code Point。

参考