字符编码表(CEF:Character Encoding Form)
编码字符集只定义了字符与码点的映射 ,并没有规定码点的字节表示方式。由于 1 个字节可以表示 256 个编号,足以容纳 ASCII 字符集,因此 ASCII 编码的规则很简单:直接将码点值用 uint8 表示即可 。对于 Unicode 字符集,容纳 2^21 至少需要 3 字节。可以采用类似 ASCII 的编码规则: 直接将编码点值用 uint32 表示即可,这正是 UTF-32 编码 。
这种一刀切的定长编码方式虽然简单粗暴,弊端也很明显: 对于纯英文文本,UTF-32 编码空间占用将是 ACSII 编码的 4 倍 ,造成极大的空间浪费,几乎没什么人用。有没有更优雅的解决方案?当然,这就是 UTF-8 和 UTF-16,两种当前比较流行的 Unicode 编码方式。
UTF-8
历史的经验,成功的设计往往具有包容性。UTF-8 是一个典型,漂亮的实现了 对 ASCII 码的向后兼容 ,以保证可以被大众接受。UTF-8 是目前互联网上使用最广泛的一种 Unicode 编码方式,它的最大特点就是可变长,随码点变换长度(从 1 字节到 4 字节)。text
大道至简,优雅的设计一定是简单的,UTF-8 的编码规则也诠释了这一点。编码规则如下:
<=127(U+7F)
的码点采用单字节编码,与 ASCII 保持一致;\>127(U+7F)
的码点采用 N 字节(N 属于 2,3,4)编码,首字节的前 N 位为 1,第 N+1 位为 0,剩余 N-1 个字节的前两位都为 10,剩下的二进制位使用字符的码点来填充。
其中(U+7F) 表示 Unicode 的十六进制码点值,即 127。如果觉得编码规则抽象,结合下表更加清晰:
Unicode 码点范围 | 码点数量 | UTF-8 编码格式 |
---|---|---|
0000 0000 ~ 0000 007F | 2^7 | 0xxxxxxx |
0000 0080 ~ 0000 07FF | 2^11 - 2^7 | 110xxxxx 10xxxxxx |
0000 0800 ~ 0000 FFFF | 2^16 - 2^11 | 1110xxxx 10xxxxxx 10xxxxxx |
0001 0000 ~ 0010 FFFF | 2^20 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
举个例子,如“汉”的 Unicode 码点是 U+6C49(110 1100 0100 1001),根据上表可得需要 3 字节编码,填充码点值后得到 0xE6 0xB7 0x89(11100110 10110001 10001001)。
根据编码规则,解码也很简单,关键是如何判断连续的字节数: 首字节连续 1 的个数即为字节数 。
需要一提的是, 在 MySQL 中,utf8 是“虚假的 utf8” ,最大只支持 3 个字节,如果建表时选择 CHARSET=utf8,会导致很多特殊字符和 emoji 表情都无法插入。 utf8mb4 才是“真正的 utf8” ,mb4 即 most bytes 4。为什么 MySQL 中 utf8 最大只支持 3 字节?历史原因,在 MySQL 刚开发那会儿,Unicode 空间只有 2^16,Unicode 委员会还在做 “65535 个字符足够全世界用了”的美梦呢。
UTF-16
在 C/C++ 中遇到的 wchar_t 类型或 Java 中的 char 类型,这些类型占内存两个字节,因为 Unicode 中常用的字符都处于[U+0, U+FFFF](基本平面)的范围之内,因此 两个字节几乎可以覆盖大部分的常用字符 ,这正是 UTF-16 编码的一个前提。
相比 UTF-32 与 UTF-8, UTF-16 编码是一个折中:小于(U+FFFF)2^16 的码点(基本平面)使用 2 字节编码,大于(U+FFFF)2^16 的码点(辅助码点)使用 4 字节编码 。由于基础平面空间会占用 2 字节的所有比特位,无法像 UTF-8 那样留有“10”前缀。那么问题来了: 当遇到两个节时,如何判断是 2 字节编码还是 4 字节编码?
UTF-16 的编码的另一个前提: 在基本平面内,****[U+D800, U+DFFF]****是一个空段(空间大小为 2^11) ,这些码点不对应任何字符。因此,这个空段可以用来映射辅助平面的字符。
辅助平面容量为 2^20,至少需要 20 个二进制位,UTF-16 将这 20 个二进制位分成两半,前 10 位映射在 U+D800 到 U+DBFF(空间大小 2^10),称为 高位 (H),后 10 位映射在 U+DC00 到 U+DFFF(空间大小 2^10),称为 低位 (L)。
映射方式采用线性映射。Unicode3.0 中给出了辅助平面字符的转换公式:
H = Math.floor((c-0x10000) / 0x400) + 0xD800
L = (c - 0x10000) % 0x400 + 0xDC00
也就是说,一个辅助平面的码点,被拆成两个基本平面的空段码点表示。如果双字节的值在[U+D800, U+DBFF]中,则要和后续相邻的双字节一同解码。具体编码规则为:
<= (U+FFFF)
的码点采用双字节编码,直接将码点使用 uint16 表示;\> (U+FFFF)
的码点采用 4 字节编码,作差计算码点溢出值,将溢出值用 uint20 表示后,前 10 位映射到[U+D800, U+DBFF]
,后 10 位映射到[U+DC00, U+DFFF]
;
小结: 定长编码的优点是转换规则简单直观,查找效率高,缺点是空间浪费,以及不可扩展。如果 Unicode 字符集进一步扩充,UTF-16 和 UTF-32 都将不可用,而 UTF-8 具有更强的可扩展性。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论