在百度百科上,对Unicode的介绍是这样的:
“ Unicode(统一码、万国码、单一码)是计算机科学领域里的一项业界标准,包括字符集、编码方案等。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。”
Unicode源于一个很简单的想法:将全世界所有的字符包含在一个集合里,计算机只要支持这一个字符集,就能显示所有的字符,再也不会有乱码了。
它从0开始,为每个符号指定一个编号,这叫做"码点"(code point)。比如,码点U+0639
表示阿拉伯字母Ain
,码点U+0041
表示英语的大写字母A
,码点U+4E25
表示汉字严
。
目前,Unicode的最新版本是7.0版,一共收入了109449个符号,其中的中日韩文字为74500个。但是,这么多字符也不是一次性定义的,Unicode将所有字符进行分区定义,每个区中有65536(2^16)个字符,即成为一个平面,目前一共有17个平面,也就是说,整个Unicode字符集的大小为1114112(2^21)个。
最前面的65536个字符位,称为基本平面(缩写BMP),它的码点范围是从0一直到65535写成16进制就是从U+0000到U+FFFF。所有最常见的字符都放在这个平面,这是Unicode最先定义和公布的一个平面。剩下的字符都放在辅助平面(缩写SMP),码点范围从U+010000一直到U+10FFFF,这也是Unicode的编码规则。
我们说Unicode只是一个字符集,只规定字符的二进制代码编号,没规定字符是如何进行存储,所以这也造成了一些问题。
比如,汉字严
的 Unicode 是十六进制数4E25
,转换成二进制数足足有15位(100111000100101
),也就是说,这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。也就是说,这个符号至少需要2个字节来表示其它更大的符号。
因为需要至少2个字节来表示更大的符号,这就导致了两个问题,第一个是如何区别该编码是Unicode还是ASCII,计算机怎么知道该字符是2个字节还是3个字节甚至更多。第二个问题是,众所周知,英文字母只需要一个字节来进行编码,但是如果用2个字节3个字节甚至更多字节来表示这就会造成相应倍数的存储空间的增加,造成了存储空间上的极大浪费。
所以最后也出现了Unicode的多种存储方式,也就是说有许多种不同的二进制格式来表示Unicode,随着互联网的普及,强烈要求出现一种统一的编码方式。UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式,同时也存在UTF-16以及UTF-32,以下我们会对其分别展开讲述。
UTF-8是一种变长的编码方法,字符长度从1个字节到4个字节不等。越是常用的字符,字节越短,最前面的128个字符,只使用1个字节表示,与ASCII码完全相同。
再次强调一下,UTF-8与Unicode的关系是:UTF-8是Unicode的实现方式之一。
UTF-8是一种边长编码方案,使得在存储上节省了许多空间,因此目前也受到了诸多欢迎,接下来,我们再讲一下UTF-8的编码规则。
UTF-8 的编码规则很简单,只有二条:
0
,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。n
字节的符号(n > 1
),第一个字节的前n
位都设为1
,第n + 1
位设为0
,后面字节的前两位一律设为10
。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。下表总结了编码规则,字母x
表示可用编码的位。
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的特点是对不同范围的字符使用不同长度的编码。对于0x00-0x7F之间的字符,UTF-8编码与ASCII编码完全相同。UTF-8编码的最大长度是4个字节。从上表可以看出,4字节模板有21个x,即可以容纳21位二进制数字。Unicode的最大码位0x10FFFF也只有21位。(百度百科)
跟据上表,解读 UTF-8 编码非常简单。如果一个字节的第一位是0
,则这个字节单独就是一个字符;如果第一位是1
,则连续有多少个1
,就表示当前字符占用多少个字节。
下面,还是以汉字严
为例,演示如何实现 UTF-8 编码。
严
的 Unicode 是4E25
(100111000100101
),根据上表,可以发现4E25
处在第三行的范围内(0000 0800 - 0000 FFFF
),因此严
的 UTF-8 编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx
。然后,从严
的最后一个二进制位开始,依次从后向前填入格式中的x
,多出的位补0
。这样就得到了,严
的 UTF-8 编码是11100100 10111000 10100101
,转换成十六进制就是E4B8A5
。
UTF-16介于UTF-8和UTF-32之间,它同时结合了定长和边长两种编码方式的特点,
它的编码规则很简单:基本平面的字符占用2个字节,辅助平面的字符占用4个字节。也就是说,UTF-16的编码长度要么是2个字节(U+0000到U+FFFF),要么是4个字节(U+010000到U+10FFFF)。
因为UTF-16要不就是2个字节,要不就是4个字节,那么有个问题就是但我们遇到两个字节的时候,怎么知道它本身是一个字符,还是需要和另外两个字节放在一起解读呢?
在基本平面上有一段空白区间U+D800~U+DFFF,它没有用来定义字符编号,而是用来映射辅助平面的字符,也称为“代理区”。
辅助平面的字符共有2^20个(为何是2^20个,待会解释),那么要对应辅助平面的字符就至少需要20个二进制位,UTF-16将这20位拆成两半,前10位映射在U+D800到U+DBFF(空间大小2^10),称为高位(H),后10位映射在U+DC00到U+DFFF(空间大小2^10),称为低位(L)。这意味着,一个辅助平面的字符,被拆成两个基本平面的字符表示。
所以,当我们遇到两个字节,发现它的码点在U+D800到U+DBFF之间,就可以断定,紧跟在后面的两个字节的码点,应该在U+DC00到U+DFFF之间,这四个字节必须放在一起解读。
Unicode码点转成UTF-16的时候,首先区分这是基本平面字符,还是辅助平面字符。如果是前者,直接将码点转为对应的十六进制形式,长度为两字节,若是后者,则需要进行上面所说的切分操作,具体看接下来讲的UTF-16的转码方式。
UTF-16编码以16位无符号整数为单位。我们把Unicode编码记作U。编码规则如下(百度百科):
为什么U'可以被写成20个二进制位?Unicode的最大码位是0x10FFFF,减去0x10000后,U'的最大值是0xFFFFF,所以肯定可以用20个二进制位表示,这就是前面提到的辅助字符总共有2^20个的原因。例如:Unicode编码0x20C30,减去0x10000后,得到0x10C30,写成二进制是:0001 0000 1100 0011 0000。用前10位依次替代模板中的y,用后10位依次替代模板中的x,就得到:1101100001000011 1101110000110000,即0xD843 0xDC30。
按照上述规则,Unicode编码0x10000-0x10FFFF的UTF-16编码有两个WORD,第一个WORD的高6位是110110,第二个WORD的高6位是110111。可见,第一个WORD的取值范围(二进制)是11011000 00000000到11011011 11111111,即0xD800-0xDBFF。第二个WORD的取值范围(二进制)是11011100 00000000到11011111 11111111,即0xDC00-0xDFFF。
所以第一个WORD和第二个WORD的高6位二进制都是根据上面两个取值范围所得出的。
接下来顺便看看怎么由UTF-16编码推导Unicode编码。
如果一个字符的UTF-16编码的第一个WORD在0xDB80到0xDBFF之间,那么它的Unicode编码在什么范围内?我们知道第二个WORD的取值范围是0xDC00-0xDFFF,所以这个字符的UTF-16编码范围应该是0xDB80 0xDC00到0xDBFF 0xDFFF。我们将这个范围写成二进制:
1101101110000000 11011100 00000000 - 1101101111111111 1101111111111111
按照编码的相反步骤,取出高低WORD的后10位,并拼在一起,得到
1110 0000 0000 0000 0000 - 1111 1111 1111 1111 1111
即0xe0000-0xfffff,按照编码的相反步骤再加上0x10000,得到0xf0000-0x10ffff。这就是UTF-16编码的第一个WORD在0xdb80到0xdbff之间的Unicode编码范围,即平面15和平面16。因为Unicode标准将平面15和平面16都作为专用区,所以0xDB80到0xDBFF之间的保留码位被称作高位专用替代。
至此UTF-16基本讲完。
UTF-32是最直观的编码方法,每个码点使用四个字节表示,字节内容一一对应码点。比如,码点0就用四个字节的0表示,码点597D就在前面加两个字节的0。
U+0000 = 0x0000 0000
U+597D = 0x0000 597D
UTF-32的优点在于,转换规则简单直观,查找效率高。缺点在于浪费空间,同样内容的英语文本,它会比ASCII编码大四倍。这个缺点很致命,导致实际上没有人使用这种编码方法,HTML 5标准就明文规定,网页不得编码成UTF-32。
Unicode只是一个符号集,它只规定了符号的二进制代码,并没有规定这个二进制代码应该如何进行存储,简单的讲就是,Unicode为所有字符提供一个唯一的编号,然后UTF-8、UTF-16等只是将字符的Unicode编号编码成相应的二进制代码进行存储或运算,所以相同Unicode编号若用不同的编码方式进行编码,最后会产生不同的二进制代码,这就是有时候文件造成乱码的原因。
参考来源:
Unicode与JavaScript详解(阮一峰)
字符编码笔记:ASCII,Unicode 和 UTF-8(阮一峰)
Unicode(百度百科)