完全弄懂 Unicode 编码

完全弄懂 Unicode 编码

 

今天,在学习 Node.js 中的 Buffer 对象时,注意到它的 alloc 和 from 方法会默认用 UTF-8 编码,在数组中每位对应 1 字节的十六进制数。想到了之间学习 ES6 时关于字符串的 Unicode 表示法,忽然就很想知道 UTF-16 是如何进行编码的,我尝试将一些汉字转换成二进制数,而后简单的按 2 个字节一组转换成十六进制,发现对于那些码点较大的汉字,结果并不只仅是简单的二进制转十六进制。因而,我开始在网上找资料,决心完全弄明白 Unicode 编码。javascript

ASCII码

在学校学 C 语言的时候,了解到一些计算机内部的机制,知道全部的信息最终都表示为一个二进制的字符串,每个二进制位有 0 和 1 两种状态,经过不一样的排列组合,使用 0 和 1 就能够表示世界上全部的东西,感受有点中国“太极”的感受——“太极生两仪,两仪生四象,四象生八卦”。java

在计算机种中,1 字节对应 8 位二进制数,而每位二进制数有 0、1 两种状态,所以 1 字节能够组合出 256 种状态。若是这 256 中状态每个都对应一个符号,就能经过 1 字节的数据表示 256 个字符。美国人因而就制定了一套编码(其实就是个字典),描述英语中的字符和这 8 位二进制数的对应关系,这被称为 ASCII 码。数组

ASCII 码一共定义了 128 个字符,例如大写的字母 A 是 65(这是十进制数,对应二进制是0100 0001)。这 128 个字符只使用了 8 位二进制数中的后面 7 位,最前面的一位统一规定为 0。post

历史问题

英语用 128 个字符来编码彻底是足够的,可是用来表示其余语言,128 个字符是远远不够的。因而,一些欧洲的国家就决定,将 ASCII 码中闲置的最高位利用起来,这样一来就能表示 256 个字符。可是,这里又有了一个问题,那就是不一样的国家的字符集可能不一样,就算它们都能用 256 个字符表示全,可是同一个码点(也就是 8 位二进制数)表示的字符可能可能不一样。例如,144 在阿拉伯人的 ASCII 码中是 گ,而在俄罗斯的 ASCII 码中是 ђ。学习

所以,ASCII 码的问题在于尽管全部人都在 0 - 127 号字符上达成了一致,但对于 128 - 255 号字符上却有不少种不一样的解释。与此同时,亚洲语言有更多的字符须要被存储,一个字节已经不够用了。因而,人们开始使用两个字节来存储字符。ui

各类各样的编码方式成了系统开发者的噩梦,由于他们想把软件卖到国外。因而,他们提出了一个“内码表”的概念,能够切换到相应语言的一个内码表,这样才能显示相应语言的字母。在这种状况下,若是使用多语种,那么就须要频繁的在内码表内进行切换。编码

Unicode

最终,美国人意识到他们应该提出一种标准方案来展现世界上全部语言中的全部字符,出于这个目的,Unicode诞生了。spa

Unicode 固然是一本很厚的字典,记录着世界上全部字符对应的一个数字。具体是怎样的对应关系,又或者说是如何进行划分的,就不是咱们考虑的问题了,咱们只用知道 Unicode 给全部的字符指定了一个数字用来表示该字符。code

对于 Unicode 有一些误解,它仅仅只是一个字符集,规定了符合对应的二进制代码,至于这个二进制代码如何存储则没有任何规定。它的想法很简单,就是为每一个字符规定一个用来表示该字符的数字,仅此而已。对象

Unicode 编码方案

以前提到,Unicode 没有规定字符对应的二进制码如何存储。以汉字“汉”为例,它的 Unicode 码点是 0x6c49,对应的二进制数是 110110001001001,二进制数有 15 位,这也就说明了它至少须要 2 个字节来表示。能够想象,在 Unicode 字典中日后的字符可能就须要 3 个字节或者 4 个字节,甚至更多字节来表示了。

这就致使了一些问题,计算机怎么知道你这个 2 个字节表示的是一个字符,而不是分别表示两个字符呢?这里咱们可能会想到,那就取个最大的,假如 Unicode 中最大的字符用 4 字节就能够表示了,那么咱们就将全部的字符都用 4 个字节来表示,不够的就往前面补 0。这样确实能够解决编码问题,可是却形成了空间的极大浪费,若是是一个英文文档,那文件大小就大出了 3 倍,这显然是没法接受的。

因而,为了较好的解决 Unicode 的编码问题, UTF-8 和 UTF-16 两种当前比较流行的编码方式诞生了。固然还有一个 UTF-32 的编码方式,也就是上述那种定长编码,字符统一使用 4 个字节,虽然看似方便,可是却不如另外两种编码方式使用普遍。

UTF-8

UTF-8 是一个很是惊艳的编码方式,漂亮的实现了对 ASCII 码的向后兼容,以保证 Unicode 能够被大众接受。

UTF-8 是目前互联网上使用最普遍的一种 Unicode 编码方式,它的最大特色就是可变长。它可使用 1 - 4 个字节表示一个字符,根据字符的不一样变换长度。编码规则以下:

  1. 对于单个字节的字符,第一位设为 0,后面的 7 位对应这个字符的 Unicode 码点。所以,对于英文中的 0 - 127 号字符,与 ASCII 码彻底相同。这意味着 ASCII 码那个年代的文档用 UTF-8 编码打开彻底没有问题。

  2. 对于须要使用 N 个字节来表示的字符(N > 1),第一个字节的前 N 位都设为 1,第 N + 1 位设为0,剩余的 N - 1 个字节的前两位都设位 10,剩下的二进制位则使用这个字符的 Unicode 码点来填充。

编码规则以下:

Unicode 十六进制码点范围 UTF-8 二进制
0000 0000 - 0000 007F 0xxxxxxx
0000 0080 - 0000 07FF 110xxxxx 10xxxxxx
0000 0800 - 0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
0001 0000 - 0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

根据上面编码规则对照表,进行 UTF-8 编码和解码就简单多了。下面以汉字“汉”为利,具体说明如何进行 UTF-8 编码和解码。

“汉”的 Unicode 码点是 0x6c49(110 1100 0100 1001),经过上面的对照表能够发现,0x0000 6c49 位于第三行的范围,那么得出其格式为 1110xxxx 10xxxxxx 10xxxxxx。接着,从“汉”的二进制数最后一位开始,从后向前依次填充对应格式中的 x,多出的 x 用 0 补上。这样,就获得了“汉”的 UTF-8 编码为 11100110 10110001 10001001,转换成十六进制就是 0xE6 0xB7 0x89

解码的过程也十分简单:若是一个字节的第一位是 0 ,则说明这个字节对应一个字符;若是一个字节的第一位1,那么连续有多少个 1,就表示该字符占用多少个字节。

UTF-16

在了解 UTF-16 编码方式以前,先了解一下另一个概念——"平面"。

在上面的介绍中,提到了 Unicode 是一本很厚的字典,她将全世界全部的字符定义在一个集合里。这么多的字符不是一次性定义的,而是分区定义。每一个区能够存放 65536 个(2^16)字符,称为一个平面(plane)。目前,一共有 17 个(2^5)平面,也就是说,整个 Unicode 字符集的大小如今是 2^21

最前面的 65536 个字符位,称为基本平面(简称 BMP ),它的码点范围是从 0 到 2^16-1,写成 16 进制就是从 U+0000 到 U+FFFF。全部最多见的字符都放在这个平面,这是 Unicode 最早定义和公布的一个平面。剩下的字符都放在辅助平面(简称 SMP ),码点范围从 U+010000 到 U+10FFFF。

基本了解了平面的概念后,再说回到 UTF-16。UTF-16 编码介于 UTF-32 与 UTF-8 之间,同时结合了定长和变长两种编码方法的特色。它的编码规则很简单:基本平面的字符占用 2 个字节,辅助平面的字符占用 4 个字节。也就是说,UTF-16 的编码长度要么是 2 个字节(U+0000 到 U+FFFF),要么是 4 个字节(U+010000 到 U+10FFFF)。那么问题来了,当咱们遇到两个字节时,究竟是把这两个字节看成一个字符仍是与后面的两个字节一块儿看成一个字符呢?

这里有一个很巧妙的地方,在基本平面内,从 U+D800 到 U+DFFF 是一个空段,即这些码点不对应任何字符。所以,这个空段能够用来映射辅助平面的字符。

辅助平面的字符位共有 2^20 个,所以表示这些字符至少须要 20 个二进制位。UTF-16 将这 20 个二进制位分红两半,前 10 位映射在 U+D800 到 U+DBFF,称为高位(H),后 10 位映射在 U+DC00 到 U+DFFF,称为低位(L)。这意味着,一个辅助平面的字符,被拆成两个基本平面的字符表示。

所以,当咱们遇到两个字节,发现它的码点在 U+D800 到 U+DBFF 之间,就能够判定,紧跟在后面的两个字节的码点,应该在 U+DC00 到 U+DFFF 之间,这四个字节必须放在一块儿解读。

接下来,以汉字"𠮷"为例,说明 UTF-16 编码方式是如何工做的。

汉字"𠮷"的 Unicode 码点为 0x20BB7,该码点显然超出了基本平面的范围(0x0000 - 0xFFFF),所以须要使用四个字节表示。首先用 0x20BB7 - 0x10000 计算出超出的部分,而后将其用 20 个二进制位表示(不足前面补 0 ),结果为0001000010 1110110111。接着,将前 10 位映射到 U+D800 到 U+DBFF 之间,后 10 位映射到 U+DC00 到 U+DFFF 便可。U+D800 对应的二进制数为 1101100000000000,直接填充后面的 10 个二进制位便可,获得 1101100001000010,转成 16 进制数则为 0xD842。同理可得,低位为 0xDFB7。所以得出汉字"𠮷"的 UTF-16 编码为 0xD842 0xDFB7

Unicode3.0 中给出了辅助平面字符的转换公式:

H = Math.floor((c-0x10000) / 0x400)+0xD800 L = (c - 0x10000) % 0x400 + 0xDC00 

根据编码公式,能够很方便的计算出字符的 UTF-16 编码。

相关文章
相关标签/搜索