前面谈到不少的 Unicode, 但一直没有系统地谈及 Unicode 的方方面面, 所以本篇文章专门谈谈 Unicode, 当然了, Unicode 是一个庞大的主题, 这里也是拣些重要的方面谈谈而已, 免不了挂一漏万.
什么是 Unicode?
按 Unicode 官方的说法, Unicode 是 Unicode Standard(Unicode标准)的简写, 所以 Unicode 即是指 Unicode 标准.
按 wiki 的说法, 它是一个计算机工业标准(a computing industry standard).
下图来自 http://www.unicode.org/standard/WhatIsUnicode.html 中的截图, 在这里我把中文和英文的合在一起
这样一个所谓的一个唯一的数字在 Unicode 中就叫做 码点.
Unicode 中的码点是什么?
字符集通常又叫 编码字符集(coded charset), 这里的 coded
与 字符集编码(charset encoding) 中的 encoding
是不同的.
一个是 code, 一个是 encode, 翻译时都可以译成"编码", 但把 coded charset 译成 编号字符集 也许更不易引发误解.
- 码点(Code Point) 即是这里的 code, 表示的是一种抽象的数字编号.
- UTF-X 则是最终的 encoding.
这点如不明白, 仍请参见 字符集与编码(二)--编号 vs 编码 .
码点的表示形式与范围是?
U+[XX]XXXX
是码点的表示形式, X 代表一个十六制数字, 可以有 4-6 位, 不足 4 位前补 0 补足 4 位, 超过则按是几位就是几位.
以下是码点的一些具体示例:
- U+0048
- U+4F60
- U+1D11E
注: 最后一个是5位的码点.
有人可能以为码点只有 4 位, 并常常将它与 UTF-16 的编码搞混, 这些都是对码点的误解.
它的范围目前是 U+0000 ~ U+10FFFF, 理论大小为 10FFFF+1=11000016. 后一个 1 代表是 65536, 因为是 16 进制, 所以前一个 1 是后一个 1 的 16 倍, 所以总共有1×16+1=17 个的 65536 的大小, 粗略估算为 17×6万=102万, 所以这是一个百万级别的数.
准确的值是 1114112, 一般记为 111 万左右即可.
16 进制的 110000 写成二进制是 100010000000000000000, 是一个 21 位的二进制数, 我们知道 210=K, 220=K×K=M, 即百万级别, 所以 221 理论上限是两百万左右.
100010000000000000000 大小基本上由第一个 1 决定, 所以也就一百万左右, 从这里也可印证前面的估算.
按照 Unicode 官方的说法, 码点范围就这些了, 以后也不会再扩充了.
为了更好分类管理如此庞大的码点数, 把每 65536 个码点作为一个 平面, 总共 17 个平面.
平面, BMP, SP
什么是平面?
由前面可知, 码点的全部范围可以均分成 17 个 65536 大小的部分, 这里面的每一个部分就是一个 平面(Plane). 编号从 0 开始, 第一个平面称为 Plane 0.
下图来自http://rishida.net/docs/unicode-tutorial/part2
什么是 BMP?
第一个平面即是 BMP(Basic Multilingual Plane 基本多语言平面), 也叫 Plane 0, 它的码点范围是 [U+0000 ~ U+FFFF]. 这也是我们最常用的平面, 日常用到的字符绝大多数都落在这个平面内.
上图中第一个花花绿绿的平面就是 BMP.
UTF-16 只需要用两字节编码此平面内的字符.
很多人错误地把 UTF-16 当成定长两字节看待, 但只要处理的字符都在这一平面内, 一般也不会遇到什么问题.
什么是增补平面?
后续的 16 个平面称为 SP(Supplementary Planes). 显然, 这些码点已经是超过 U+FFFF 的了, 所以已经超过了 16 位编码空间的理论上限, 对于这些平面内的字符, UTF-16 采用了四字节编码.
注: 其中很多平面还是空的, 还没有分配任何字符, 只是先规划了这么多.
另: 有些还属于私有的, 如上图中的最后两个 Private Use Planes, 在此可自定义字符.
另: BMP 中也保留了部分区域供自定义字符使用.
鸟瞰 BMP 字符集
Unicode 的字符如此之多, 即使是最常用的 BMP, 它的码点空间也有 6 万多, 如果把这些字符都放到一张图片上, 会是什么情况呢? GNU Unifont 就制作了一张这样的图片. 见http://unifoundry.com/pub/unifont-7.0.03/unifont-7.0.03.bmp
提示: 打开它需要一点时间, 它的像素是 4000×4000 这个级别!
下图是它的一个缩略版本:
这是一个 256×256=65536 的表格, 横向纵向以 16 进制算都是从 00~FF(0~255).
CJK 统一汉字
你可能已经注意到上图中间一大片的区域, 没错, 它就是我们的汉字, 在 Unicode中, 称为 CJK 统一汉字(CJK: Chinese, Japanese, and Korean, 中日韩). 我们可以局部放大看一下:
正则表达式 [\u4E00-\u9FA5] 来匹配中文的问题在哪?
你可能在不少地方见过这种写法, 严格来说这只是 Unicode 最主要的一段中文区域.
你只要稍加计算就可知这一段大小不过是两万多一点, \u4E00-\u9FA5(19968-40869), 中文怎么可能只有这两万多字呢?
这里的"天字第一号"字 4E00 是哪个字呢? 请看上面的图中左边第 4E
开头的行, 它就是"一"字, 我们还可以看到它上面还有不少偏门的汉字, 这就是后来增补的汉字了. 所以严格来说, 这个上限是不准确的. 那么它的下限又是否准确呢? 下面是 Word 的一个插入符号功能的一个截图:
可以看到 9FA5 后面也还有不少的汉字, 它们中间又还夹杂着一些符号, 所以想正确地表示 Unicode 中的汉字还是个不小的挑战.
应该说, Unicode 处在不断发展中, 它有一百多万的空间, 目前也只是定义了十万左右的字符, 还会不断增加, 汉字自然也有可能增加, 所以汉字的范围实际上是动态的, 变化的.
当然了, 常用的基本落在了这一范围内, 而事实上已经包含了许多的不常用汉字, 毕竟连只有 6 千多字的 GB2312 中都含有大量的不常用汉字.
在要求不那么严格的应用中, 按以上范围去判断基本也 OK, 而汉字这一概念实际上也没有准确定义, 比方说上图中一些"偏旁部首", 这些是"汉字"吗?
代理区
你可能还注意到前面的 BMP 缩略图中有一片空白, 这白花花一片亮瞎了我们的猿眼的是啥呢? 正如标题已经告诉你的, 这就是所谓的 代理区(Surrogate Area) 了.
可以看到这段空白从 D8~DF. 其中前面的红色部分 D800–DBFF 属于 高代理区(High Surrogate Area), 后面的蓝色部分 DC00–DFFF 属于 低代理区(Low Surrogate Area), 各有四行, 大小均为 4×256=1024.
关于代理区的相关用途, 我们在讲到 UTF-16 编码时再说.
还可以看到在它之前是韩文的区域, 之后 E0 开始到 F8 的则是属于私有的(private), 可以在这里定义自己专用的字符.
前面说到 17 个平面的最后两个是私有平面, 这里的 U+E000 ~ U+F8FF 则是 BMP 平面中的私有区域(Private Use Area).
至此我们对 Unicode 的码点, 平面都有了一定的了解, 但我们还没有触及一个重要的方面, 那就是码点到最终编码的转换, 在 Unicode 中, 这称为 UTF.
什么是 UTF?
UTF 即是 Unicode 转换格式(Unicode (or UCS) Transformation Format).
关于 UCS: Universal Character Set(统一字符集), 也称 ISO/IEC 10646 标准, 不那么严格的情况下, 可以认为它和"Unicode字符集"这一概念是等价的. 如有兴趣的可以自行搜索了解.
码点如何转换成 UTF 的几种形式呢? 我想这是大家很关心的问题, 再发一次前面的一个图:
让我们先从最简单的 UTF-32 说起.
UTF-32
我们说码点最大的 0x10FFFF 也就 21 位, 而 UTF-32 采用的定长四字节则是 32 位, 所以它表示所有的码点不但毫无压力, 反而绰绰有余, 所以只要把码点的表示形式以前补 0 的形式补够 32 位即可. 这种表示的最大缺点是占用空间太大.
再来看稍复杂一点的 UTF-8.
UTF-8
UTF-8 是变长的编码方案, 可以有 1, 2, 3, 4 四种字节组合. 在前面的 定长与变长 篇章我们提到 UTF-8 采用了高位保留方式来区别不同变长, 如下:
如上, 彩色的表示是保留的固定位, X 表示是有效编码位.
单字节最高位都是 0, 多字节的最高位都是 1.
多字节方面, 更具体的讲, N 字节模式, 首字节以 "N 个 1 再加 0 " 打头, 后跟 "N-1" 个以 "10" 打头的字节.
码点与字节如何对应?
哪些码点用哪种变长呢? 可以先把码点变成二进制, 看它有多少有效位(去掉前导0)就可以确定了.
- 一字节有效编码位有 7 位, 27=128, 码点 U+0000 ~ U+007F(0~127)使用一字节.
一字节留给了 ASCII, 所以 UTF-8 兼容 ASCII.
- 二字节有效编码位只有 5+6=11 位, 最多只有 211=2048 个编码空间, 所以数量众多的汉字是无法容身于此的了. 码点 U+0080 ~ U+07FF(128~2047)使用二字节.
注意: 这里码点从 128~2047, 因为去掉了一字节的码点, 所以不会占满 2048 个编码空间, 是有冗余的, 但你不能把适用于一字节的码点放到这里来编码. 下同.
- 三字节模式可看到光是保留位就达到 4+2+2=8 位, 相当于一字节, 所以只剩下两字节 16 位有效编码位, 它的容量实际也只有 65536. 码点 U+0800~U+FFFF(2048~65535)使用三字节编码.
我们前面说到, 一些汉字字典收录的汉字达到了惊人的 10 万级别. 基本上, 常用的汉字都落在了这三字节的空间里, 这就是我们常说的汉字在 UTF-8 里用三字节表示.
当然了, 这么说并不严谨, 如果这 10 万的汉字都被收录进来的话, 那些偏门的汉字自然只能被挤到四字节空间上去了.
- 四字节的可以看到它的有效位是 3+6+6+6=21 位, 前面说到最大的码点 10FFFF 也是 21 位, U+FFFF 以上的增补平面的字符都在这里来表示.
按照 UTF-8 的模式, 它还可以扩展到 5 字节, 乃至 6 字节变长, 但 Unicode 说了码点就到 10FFFF, 不扩充了, 所以 UTF-8 最多到四字节就足够了.
码点到 UTF-8 如何转换?
那么具体是如何转换呢, 其实不难, 来看一个汉字"你"(U+4F60)的转换示意, 如下图所示:
上图显示了一有效位为 15 位的码点到三字节转换的一个基本原理, 我们还可看到原来 4F60 中的一头一尾的两个 4 和 0 在转换后还存在于最终的三字节结果中.
UTF-8 三字节模式固定了 1110 的开头模式, 所以多数汉字总是以 1110 开头, 换成 16 进制形式, 1110 就是字母 E.
如果看到一串的 16 进制有如下的形式: EX XX XX EX XX XX… 每三个三个字节前面都是 E 打头, 那么它很可能就是一串汉字的 UTF-8 编码了.
其它变长字节转换道理也类似, 其中分组从低位开始, 高位如不足则补零. 这里就不再示例了.
最后来看最复杂的 UTF-16, 在此之前我们先要理解代理区与代理对等概念.
UTF-16
UTF-16 是一种变长的 2 或 4 字节编码模式. 对于 BMP 内的字符使用 2 字节编码, 其它的则使用 4 字节组成所谓的代理对来编码.
什么是代理区?
在前面的鸟瞰图中, 我们看到了一片空白的区域, 这就是所谓的 代理区(Surrogate Area) 了, 代理区是 UTF-16 为了编码增补平面中的字符而保留的, 总共有 2048 个位置, 均分为 高代理区(D800–DBFF) 和 低代理区(DC00–DFFF) 两部分, 各 1024 大小.
这两个区一横一纵组成一个二维的表格, 共有 1024×1024=210×210=24×216=16×65536 个位置, 所以它恰好可以表示增补的 16 个平面中的所有字符.
当然了, 说恰好是不对的, 显然代理区就是冲着表示增补平面来设计的, 或者至少它们是一起考虑的.
下面的图片来自 wiki:
什么是代理对?
一个高代理区(即上图中的 Lead(头), 行)加一个低代理区(即上图中的 Trail(尾), 列)的编码组成一对即是一个 代理对(Surrogate Pair), 必须是这种先高后低的顺序, 如果出现两个高, 两个低, 或者先低后高, 都是非法的.
在图中可以看到一些转换的例子, 如:
- (D800 DC00)—> U+10000, 左上角, 第一个增补字符
- (DBFF DFFF)—> U+10FFFF, 右下角, 最后一个增补字符
码点到 UTF-16 如何转换?
分成两部分:
- BMP 中直接对应, 无须做任何转换;
- 增补平面 SP 中, 则需要做相应的计算.
其实由上图中的表也可看出增补平面中, 码点就是从上到下, 从左到右排列过去的, 所以只需做个简单的除法, 拿到除数和余数即可确定行与列.
拿到一个码点, 先减去 1000016, 再除以 40016(=102410)就是所在行了, 余数就是所在列了, 再加上行与列所在的起始值, 就得到了代理对了.
- Lead = (码点 - 1000016) ÷ 40016 + D800
- Trail = (码点 - 1000016) % 40016 + DC00
下面以前面的 U+1D11E 具体示例了代理对的计算:
- Lead = (1D11E - 1000016) ÷ 40016 + DB00 = D11E ÷ 40016 + D800 = 34 + D800 = D834
- Trail = (1D11E - 1000016) % 40016 + DC00 = D11E % 40016 + DC00 = 11E + DC00 = DD1E
所以, 码点 U+1D11E 对应的代理对即是 D834 DD1E.
注意: 以上计算方式仅用于说明转换原理, 不代表实际采用的计算方式.
一个码点减去 1000016 后实际最多只有 20 位, 再除以40016(=210=100000000002), 这个除数实际是一个二进制整数, 相当于十进制中整十整百的数.
所以结果实际上低 10 位上的就是余数, 而高 10 位(或者不到 10 位)上的就是商, 可以通过更为快速的 移位 操作实现.
举个十进制的例子, 就好比是 "1234÷100=12...34", 你都不需要拿笔去算.
应该说, 代理区的设计是有效率上的考虑的, 如果我们要做转换, 应该考虑是否有系统 API 可供调用, 而不要自行去实现.
关于 Unicode 的基本知识, 就讲到这里.