引言
本文章对结合以下文献对unicode进行深入理解、感谢前辈们的无私奉献!其中特别鸣谢Quinn Note:Unicode的设计和原理这篇文章作者。
1.Quinn Note:Unicode的设计和原理
2.unicode 联盟:unicode12.0文档
3.鹿文鹏、薛若娟:Unicode与UTF-8编码转换方法研究
4.阮一峰:字符编码笔记:ASCII,Unicode 和 UTF-8
5.lzjun:阮一峰的文章有哪些常见性错误
Unicode
Unicode背景就不再阐述了,前面几篇文章都有详细介绍。说白了,Unicode就是把所有符号(各国文字、数学符号、emoji表情等等)统一(标准化)起来,用二进制方式表现,每一个符号对应一个二进制值。
1.码点(code point)
Unicode中码点就是每个符号对应二进制值
中文"好"的码点是十六进制的597D。Unicode表示:U+597D
码点U+0041表示大写拉丁字母A
2.平面映射(plane)
以前Unicode是最大16bit,能表示65536字符。慢慢的各国文字的加入(光我们国家的汉字就好几万),16位完全不够用,因此现在Unicode最大到U+10FFFF(而二进制是10000 11111111 11111111,一共花费了21bit)。也就是说Unicode的值在U+000000到U+10FFFF内。当然这么多符号肯定一些很常用(英文)而一些基本极少使用,所以将Unicode的编码空间划分为17个平面(plane),每个平面包含2^16(65,536)个码位。17个平面的码位可表示为从U+xx0000到U+xxFFFF,其中xx表示十六进制值从0x00到0x10,共计17个平面。
- 第一个平面称为基本多语言平面(Basic Multilingual Plane, BMP),或称第零平面(Plane 0),码点范围从U+000000----U+00FFFF。
- 其他平面称为辅助平面(Supplementary Planes),码点范围从U+010000一直到U+10FFFF。
- 为什么要分区?这块后面说到存储方式就明白了,先记着
BMP平面内,从U+D800到U+DFFF之间的码位区块是永久保留不映射到Unicode字符,也就是说这块区域不用来表示符号,先空着。UTF-16就利用保留下来的0xD800到0xDFFF区段的码位来对辅助平面的字符的码位进行编码,后面介绍UTF-16编码原理时会细讲。
3.如何存储Unicode
有人说,存储还不简单,直接把Unicode的值存储在计算机不就好了。我负责人的告诉你,当然可以。我们来看下面例子
- 'A'的码点是U+65
因为Unicode最大是21位,至少需要4个字节表示全部信息,因此'A'的存储不是直接用一个字节表示01100101存储,而是四个字节:00000000 00000000 00000000 01100101这样。这样的存储方式就叫做UTF-32,每个Unicode码点都用四个字节表示。哇,真的是浪费空间。为了解决这一问题,变长字节编码方式来了——UTF-8。
4.UTF-8
UTF-8的编码规则可以概括为二条:
- 单字节的字符,字节的第一位设为0,后面7位为这个符号的Unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
- 对于n字节(n = 2,3,4)的字符,第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
所以,UTF-8的字符编码区间如下表格,x位是有效的二进制位,它们拼接起来就是对应的Unicode,表中的1和0,只是用于描述当前字符占用几个字节。
- 例子
Tips:计算机中计量基本单位是字节,即8bit
'A'的码点是U+65(01100101)
'中'的码点是U+4E2D(01001110 00101101)通过UTF-32存储
"A中":00000000 00000000 00000000 01100101 00000000 00000000 01001110 00101101存储在计算机硬盘上。
我才不管你多长,反正所有信息我都用4个字节表示。当计算机读取文件信息时,他会以每4个字节为单位读取信息,然后用来在Unicode字符集里面查询对应的码点,转换成符号。虽然很浪费空间,但是方便啊,Unicode对应字符的码点和存储的值是一样大小的,多方便。通过UTF-8存储
根据上面的对应表。
'A'在U+0000——U+007F区间内,所以存储值是:01100101 。发现没有,其实就是他的码点值,没变,其实也是ASCII值。
'中'在U+0800——U+FFFF区间内,所以存储值是:11100100 10111000 10101101。到这里可以发现,对于码点是1个字节可以表示的,那么其UTF-8编码也是1个字节搞定(U+80——U+FF除外),其他n(2,3,4)字节才能表示的码点值在UTF-8中需要n+1字节表示,是不是比UTF-32省空间多了。但是其编码多了写标志信息,如在'中'的UTF-8编码中,使用1110表示我这个码点是需要3个字节编码,后面的每个字节都以10开头,表示这些字节的信息属于上面那个1110头。是不是要麻烦点,但是没关系,这些规则是为了让计算机能正确从UTF-8中读取到正确的Unicode码点值,以获得相应符号。所以累的是计算机,但对于计算机这都不是事。
"A中":01100101 11100100 10111000 10101101存储在计算机硬盘上。在解码的时候,计算机会根据字节前面的标志位是0、110、1110、11110来判断改码点是由几个字节组成,然后把相应后面字节信息转化成码点得到相应符号。这就是变长方式的编码,所以现在最常用的存储方式是UTF-8。
5.UTF-16
从U+0000至U+D7FF以及从U+E000至U+FFFF的码位
这个是BMP平面去掉代理区域U+D800到U+DFFF剩下的区域,UTF-16使用两个字节编码这个范围内的码位,就是其码点就是其UTF-16值。从U+10000到U+10FFFF的码位
这个区域是要用4个字节表示,但是他没有像UTF-8一样使用了标志位,所以我们就无法知道字符之间的边界了,不知道哪里是使用2个字节表示一个字符,哪里是使用4个字节表示一个字符。所以这里就用到前面在BMP(基本面)的代理区。
编码方法:详情查看
辅助平面中的码位,在UTF-16中被编码为32bit,4个字节。先看一下其编码算法,然后再看一个例子。辅助平面的码位(U+10000到U+10FFFF)减去0x10000,得到的值的范围U+0到U+FFFFF,最多占20bit长,我们将20bit长分为两部分,高位的10bit的值(值的范围为0到0x3FF)被加上0xD800得到第一个码元,称作高位代理(highsurrogate),值的范围是0xD800到0xDBFF。低位的10bit的值(值的范围是0到0x3FF)被加上0xDC00得到第二个码元,称作低位代(low surrogate),值的范围是0xDC00到0xDFFF。将上面获得的高位代理和低位代理结合起来得到的四个字节,就是最终的Unicode编码,我们来举一个例子。
例如U+10440编码():
0x10440减去0x10000,结果为0x00440,二进制为0000 0000 0100 0100 0000。分区它的上10位值和下10位值(使用二进制):0000000001 and 000100000。添加0xD800到上值,以形成高位:0xD800 + 0x0001 = 0xD801。添加0xDC00到下值,以形成低位:0xDC00 + 0x0040 = 0xDC40。所以U+10440的UTF-16编码是0xD801 0xDC40。
6.总结
Unicode是字符集,如今最大能表示的字符是2的21次方(除了代理区,稍微少点),他的编码方式一般是UTF-8、UTF-16、UTF-32。最常用的是UTF-8。但是像JAVA中用的是UCS-2的方式,种淘汰的UTF-16编码。
Tips:为什么java里不推荐使用char类型呢?其实,1个java的char字符并不完全等于一个unicode的字符。char采用的UCS-2编码,是一种淘汰的UTF-16编码,编码方式最多有65536种,远远少于当今Unicode拥有11万字符的需求。java只好对后来新增的Unicode字符用2个char拼出1个Unicode字符。导致String中char的数量不等于unicode字符的数量。然而,大家都知道,char在Oracle中,是固定宽度的字符串类型(即所谓的定长字符串类型),长度不够的就会自动使用空格补全。因此,在一些特殊的查询中,就会导致一些问题,而且这种问题还是很隐蔽的,很难被开发人员发现。一旦发现问题的所在,就意味着数据结构需要变更,可想而知,这是多么大的灾难啊。