Unicode 及其编码方案
前言
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+D800
~U+DFFF
:代理区。 -
U+E000
~U+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:
其中:
- 一个像素表示 1 个 Code Point
- 一个小方块表示 2^8 个 Code Point(只为保证视觉一致性,无特殊意义)
- 一个大方块表示一个 Plane,其中包含 2^16 个 Code Point
- 白色表示未用空间
- 蓝色表示已用空间
- 绿色表示自用空间
-
红色区域表示代理区,空间范围
U+D800
~U+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+D800
~U+DFFF
,空间大小 2^11,用于映射 Supplementary Plane 中的所有 Code Point。
Q:代理区空间大小 2^11,如何做到映射空间大小 2^20 的 Supplementary Plane?
A:将U+D800
~U+DFFF
(2^11) 这个范围内的值分为两部分:
U+D800
~U+DBFF
(空间大小 2^10),称为高位(H)U+DC00
~U+DFFF
(空间大小 2^10),称为低位(L)对高位和低位做迪卡尔积,可以得到 2^20 种组合,这样就可以映射 Supplementary Plane 了。
Code Point 编码方法
将 Code Point 编码为 Code Unit。
-
U+0000
~U+FFFF
范围内的 Code Point,直接用 2 个字节表示。 -
U+010000
~U+10FFFF
范围内的 Code Point 通过以下形式编码,再用 4 个字节表示:-
Code Point 减去
0x10000
,获得一个在0x00000
~0xFFFFF
范围中的值,这个值可以转换为 20 位的二进制数。 -
20 位的二进制数的高 10 位与
0xD800
相加,获得高位。 -
20 位的二进制数的低 10 位与
0xDC00
相加,获得低位。 - 高位与低位组合后,刚好可以用 4 个字节表示。
-
Code Point 减去
Code Unit 解码方法
将 Code Unit 解码为 Code Point。
因为有了高低位,解析字节流时就涉及到了预备知识里提到的大小端问题。
在解析使用 UTF-16 大端编码的字节流时,首先判断 Code Unit 是否在 U+D800
~U+DBFF
范围内:
- 如果是,则与其后相邻的 Code Unit 放在一起解码。
- 如果不是,直接解码。
在解析使用 UTF-16 小端编码的字节流时,首先判断 Code Unit 是否在 U+DC00
~U+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+0000
~U+007F
范围内的 Code Point,使用 1 个字节表示,最高位设为 0,低 7 位设为对应的 Code Point。 -
U+0080
~U+10FFFF
范围内的 Code Point,使用 2 ~ 4 个字节表示。设当前所使用的字节数为 n,第一个字节的前高 n 位设为 1,第高 n + 1 位设为 0,后续字节的高 2 位均设为 10。剩余没有提及的二进制位,从低位到高位依次填入对应的 Code Point,空位补 0。
编码方法看似复杂,其实非常简单。以汉字 我 为例:
-
汉字 我 对应的 Code Point 为
U+6211
,转换为二进制110001000010001
。 -
U+6211
在U+0800
~U+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。