Java 字符编码(一)Unicode 字符编码
Unicode(http://www.unicode.org/versions/#TUS_Latest_Version) 是一个编码方案,说白了希望给世界上每一种文字系统的每一个字符,都分配一个唯一的整数,这样就不可能有任何冲突了。
一、字符编码规范
1.1 ASCII(American Standard Code for Information Interchange)
美国信息交换标准代码,这是计算机上最早使用的通用的编码方案。那个时候计算机还只是拉丁文字的专利,根本没有想到现在计算机的发展势头,如果想到了,可能一开始就会使用 unicode 了。当时绝大部分专家都认为,要用计算机,必须熟练掌握英文。这种编码占用 7 个 Bit,在计算机中占用一个字节,8 位,最高位没用,通讯的时候有时用作奇偶校验位。因此 ASCII 编码的取值范围实际上是:0x00-0x7f,只能表示 128 个字符。后来发现 128 个不太够用,做了扩展,叫做 ASCII 扩展编码,用足八位,取值范围变成:0x00-0xff,能表示 256 个字符。其实这种扩展意义不大,因为 256 个字符表示一些非拉丁文字远远不够,但是表示拉丁文字,又用不完。所以扩展的意义还是为了下面的 ANSI 编码服务。
1.2 ANSI(American National Standard Institite )
美国国家标准协会,也就是说,每个国家(非拉丁语系国家)自己制定自己的文字的编码规则,并得到了 ANSI 认可,符合 ANSI 的标准,全世界在表示对应国家文字的时候都通用这种编码就叫 ANSI 编码。换句话说,中国的 ANSI 编码和在日本的 ANSI 的意思是不一样的,因为都代表自己国家的文字编码标准。比如中国的 ANSI 对应就是 GB2312 标准,日本就是 JIT 标准,香港,*对应的是 BIG5 标准等等。当然这个问题也比较复杂,微软从 95 开始,用就是自己搞的一个标准 GBK。GB2312 里面只有 6763 个汉字,682 个符号,所以确实有时候不是很够用。GBK 一直能和 GB2312 相互混淆并且相安无事的一个重要原因是 GBK 全面兼容 GB2312,所以没有出现任何冲突,你用 GB2312 编码的文件通过 GBK 去解释一定能获得相同的显示效果,换句话说:GBK 对 GB2312 就是,你有的,我也有,你没得的,我还有!
好了,ANSI 的标准是什么呢,首先是 ASCII 的代码你不能用!也就是说 ASCII 码在任何 ANSI 中应该都是相同的。其他的,你们自己扩展。所以呢,中国人就把 ASCII 码变成 8 位,0x7f 之前我不动你的,我从 0xa0 开始编,0xa0 到 0xff 才 95 个码位,对于中国字那简直是杯水车薪,因此,就用两个字节吧,此编码范围就从 0xA1A1 - 0xFEFE,这个范围可以表示 23901 个汉字。基本够用了吧,GB2312 才 7000 多个呢!GBK 更猛,编码范围是从 0x8140 - 0xFEFE,可以表示 3 万多个汉字。可以看出,这两种方案,都能保证汉字头一个字节在 0x7f 以上,从而和 ASCII 不会发生冲突。能够实现英文和汉字同时显示。
BIG5,香港和*用的比较多,繁体,范围: 0xA140-0xF9FE,0xA1A1-0xF9FE,每个字由两个字节组成,其第一字节编码范围为 0xA1-0xF9,第二字节编码范围为 0x40-0x7E 与 0xA1-0xFE,总计收入 13868 个字 (包括 5401个 常用字、7652 个次常用字、7 个扩充字、以及 808 个各式符号)。
那么到底 ANSI 是多少位呢?这个不一定!比如在 GB2312 和 GBK,BIG5 中,是两位!但是其他标准或者其他语言如果不够用,就完全可能不止两位!
例如:GB18030: GB18030-2000(GBK2K)在 GBK 的基础上进一步扩展了汉字,增加了藏、蒙等少数民族的字形。GBK2K 从根本上解决了字位不够,字形不足的问题。它有几个特点:它并没有确定所有的字形,只是规定了编码范围,留待以后扩充。编码是变长的,其二字节部分与 GBK 兼容;四字节部分是扩充的字形、字位,其编码范围是首字节 0x81-0xfe、二字节 0x30-0x39、三字节 0x81-0xfe、四字节 0x30-0x39。它的推广是分阶段的,首先要求实现的是能够完全映射到 Unicode3.0 标准的所有字形。它是国家标准,是强制性的。
搞懂了 ANSI 的含义,我们发现 ANSI 有个致命的缺陷,就是每个标准是各自为阵的,不保证能兼容。换句话说,要同时显示中文和日本文或者阿拉伯文,就完全可能会出现一个编码两个字符集里面都有对应,不知道该显示哪一个的问题,也就是编码重叠的问题。显然这样的方案不好,所以 Unicode 才会出现!
1.3 MBCS(Multi-Byte Chactacter System(Set))
多字节字符系统或者字符集,基于 ANSI 编码的原理上,对一个字符的表示实际上无法确定他需要占用几个字节的,只能从编码本身来区分和解释。因此计算机在存储的时候,就是采用多字节存储的形式。也就是你需要几个字节我给你放几个字节,比如 A 我给你放一个字节,比如"中“,我就给你放两个字节,这样的字符表示形式就是 MBCS。
在基于 GBK 的 windows 中,不会超过 2 个字节,所以 windows 这种表示形式有叫做 DBCS(Double-Byte Chactacter System),其实算是 MBCS 的一个特例。C 语言默认存放字符串就是用的 MBCS 格式。从原理上来说,这样是非常经济的一种方式。
1.4 CodePage
代码页,最早来自 IBM,后来被微软,oracle,SAP 等广泛采用。因为 ANSI 编码每个国家都不统一,不兼容,可能导致冲突,所以一个系统在处理文字的时候,必须要告诉计算机你的 ANSI 是哪个国家和地区的标准,这种国家和标准的代号(其实就是字符编码格式的代号),微软称为 Codepage 代码页,其实这个代码页和字符集编码的意思是一样的。告诉你代码页,本质就是告诉了你编码格式。
但是不同厂家的代码页可能是完全不同,哪怕是同样的编码,比如, UTF-8 字符编码 在 IBM 对应的代码页是 1208,在微软对应的是 65001,在德国的 SAP 公司对应的是 4110 。所以啊,其实本来就是一个东西,大家各自为政,搞那么多新名词,实在没必要!所以标准还是很重要的!!!
比如 GBK 的在微软的代码页是 936,告诉你代码页是 936 其实和告诉你我编码格式是 GBK 效果完全相同。那么处理文本的时候就不会有问题,不会去考虑某个代码是显示的韩文还是中文,同样,日文和韩文的代码页就和中文不同,这样就可以避免编码冲突导致计算机不知如何处理的问题。当然用这个也可以很容易的切换语言版本。但是这都是治标不治本的方法,还是无法解决同时显示多种语言的问题,所以最后还是都用 unicode 吧,永远不会有冲突了。
1.5 Unicode(Universal Code)
这是一个编码方案,说白了就是一张包含全世界所有文字的一个编码表,只要这个世界上存在的文字符号,统统给你一个唯一的编码,这样就不可能有任何冲突了。不管你要同时显示任何文字,都没有问题。因此在这样的方案下,Unicode 出现了。Unicode 编码范围是:0-0x10FFFF,可以容纳 1114112 个字符,100 多万啊。全世界的字符根本用不完了,Unicode 5.0 版本中,才用了 238605 个码位。所以足够了。
因此从码位范围看,严格的 unicode 需要 3 个字节来存储。但是考虑到理解性和计算机处理的方便性,理论上还是用 4 个字节来描述。
Unicode 采用的汉字相关编码用的是《CJK 统一汉字编码字符集》— 国家标准 GB13000.1 是完全等同于国际标准《通用多八位编码字符集 (UCS)》 ISO 10646.1。《GB13000.1》中最重要的也经常被采用的是其双字节形式的基本多文种平面。在这 65536 个码位的空间中,定义了几乎所有国家或地区的语言文字和符号。其中从 0x4E00-0x9FA5 的连续区域包含了 20902 个来自中国(包括*)、日本、韩国的汉字,称为 CJK (Chinese Japanese Korean) 汉字。CJK 是 《GB2312-80》、《BIG5》 等字符集的超集。
CJK 包含了中国,日本,韩国,越南,香港,也就是 CJKVH。这个在 UNICODE 的 Charset chart 中可以明显看到。 unicode 的相关标准可以从 https://www.unicode.org/standard/standard.html 上面获得。
二、Unicode 中的基本概念
2.1 代码点
Unicode 标准的本意很简单:希望给世界上每一种文字系统的每一个字符,都分配一个唯一的整数,这些整数叫做 代码点(Code Points)。
2.2 代码空间
所有的代码点构成一个 代码空间(Code Space),根据 Unicode 定义,总共有 1,114,112 个代码点,编号从 0x0-0x10FFFF。 换句话说,如果每个代码点都能够代表一个有效字符的话,Unicode 标准最多能够编码 1,114,112,也就是大概 110 多万个字符。最新的 Unicode 标准(7.0)已经给超过 11 万个字符分配了代码点。
2.3 代码平面
Unicode 标准把代码点分成了 17 个代码平面(Code Plane),编号为 #0-#16。每个代码平面包含 65,536(2^16)个代码点(17*65,536=1,114,112)。 其中,Plane#0 叫做基本多语言平面(Basic Multilingual Plane,BMP),其余平面叫做补充平面(Supplementary Planes)。Unicode7.0 只使用了 17 个平面中的 6 个,并且给这 6 个平面起了名字,如下图所示:
下面是这些平面的名字和用途:
-
Plane#0 BMP(Basic Multilingual Plane)
大部分常用的字符都坐落在这个平面内,比如 ASCII 字符,汉字等。 -
Plane#1 SMP(Supplementary Multilingual Plane)
这个平面定义了一些古老的文字,不常用。 -
Plane#2 SIP(Supplementary Ideographic Plane)
这个平面主要是一些BMP中没有包含汉字。 -
Plane#14 SSP(Supplementary Special-purpose Plane)
这个平面定义了一些非图形字符。 Plane#15 SPUA-A(Supplementary Private Use Area A)
Plane#16 SPUA-B(Supplementary Private Use Area B)
2.4 BMP
BMP 是最重要的一个代码平面,大部分常用的字符都定义在这个平面内,如下图所示:
在 BMP 中定义的代码点包括:
-
ASCII
ASCII总共有128个字符,占据了BMP的前128个代码点(上图绿线) -
ISO-8859-1
共256个字符,占据了BMP的前256个代码点(上图绿线+蓝线) -
CJK Unified Ideographs
上图的红色区域(占据BMP大约1/3)定义了两万多个汉字,其中前 20,902 个汉字是按照《康熙字典》里笔画顺序排列的 -
Surrogate Code Points
从 0xD800-0xDBFF 的 1024 个代码点是 High-surrogate 代码点,从 0xDC00-0xDFFF 的 1024 个代码点是 Low-surrogate 代码点。这 2048 个代码点并不是有效的字符代码点,它们是为 UTF 编码保留的。一个 High-surrogate 代码点和一个 Low-surrogate 代码点组成一个代理对(Surrogate Pair),可以在 UTF-16 里编码 BMP 之外的某个代码点(1024^2+65,536=1,114,112)。
三、Unicode 编码方案
之前提到,Unicode 没有规定字符对应的二进制码如何存储。以汉字“汉”为例,它的 Unicode 码点是 0x6c49,对应的二进制数是 110110001001001,二进制数有 15 位,这也就说明了它至少需要 2 个字节来表示。可以想象,在 Unicode 字典中往后的字符可能就需要 3 个字节或者 4 个字节,甚至更多字节来表示了。
这就导致了一些问题,计算机怎么知道你这个 2 个字节表示的是一个字符,而不是分别表示两个字符呢?这里我们可能会想到,那就取个最大的,假如 Unicode 中最大的字符用 4 字节就可以表示了,那么我们就将所有的字符都用 4 个字节来表示,不够的就往前面补 0。这样确实可以解决编码问题,但是却造成了空间的极大浪费,如果是一个英文文档,那文件大小就大出了 3 倍,这显然是无法接受的。
于是,为了较好的解决 Unicode 的编码问题, UTF-8 和 UTF-16 两种当前比较流行的编码方式诞生了。当然还有一个 UTF-32 的编码方式,也就是上述那种定长编码,字符统一使用 4 个字节,虽然看似方便,但是却不如另外两种编码方式使用广泛。
3.1 UTF-8
UTF-8 是一个非常惊艳的编码方式,漂亮的实现了对 ASCII 码的向后兼容,以保证 Unicode 可以被大众接受。
UTF-8 是目前互联网上使用最广泛的一种 Unicode 编码方式,它的最大特点就是可变长。它可以使用 1-4 个字节表示一个字符,根据字符的不同变换长度。编码规则如下:
对于单个字节的字符,第一位设为 0,后面的 7 位对应这个字符的 Unicode 码点。因此,对于英文中的 0 - 127 号字符,与 ASCII 码完全相同。这意味着 ASCII 码那个年代的文档用 UTF-8 编码打开完全没有问题。
对于需要使用 N 个字节来表示的字符(N > 1),第一个字节的前 N 位都设为 1,第 N + 1 位设为0,剩余的 N - 1 个字节的前两位都设位 10,剩下的二进制位则使用这个字符的 Unicode 码点来填充。
编码规则如下:
Unicode编码(十六进制) | UTF-8 字节流(二进制) |
---|---|
000000-00007F | 0xxxxxxx |
000080-0007FF | 110xxxxx 10xxxxxx |
000800-00FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
010000-10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
UTF-8 的特点是对不同范围的字符使用不同长度的编码。对于 0x00-0x7F 之间的字符,UTF-8 编码与 ASCII 编码完全相同。UTF-8 编码的最大长度是 4 个字节。从上表可以看出,4 字节模板有 21 个x,即可以容纳 21 位二进制数字。Unicode 的最大码位 0x10FFFF 也只有 21 位。
例1:“汉”字的 Unicode 编码是 0x6C49。0x6C49 在 0x0800-0xFFFF 之间,使用 3 字节模板:1110xxxx 10xxxxxx 10xxxxxx。将 0x6C49 写成二进制是:0110 1100 0100 1001, 用这个比特流依次代替模板中的 x,得到:11100110 10110001 10001001,即 E6 B1 89。
例2:Unicode 编码 0x20C30 在 0x010000-0x10FFFF 之间,使用 4 字节模板:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx。将 0x20C30 写成 21 位二进制数字(不足 21 位就在前面补 0):0 0010 0000 1100 0011 0000,用这个比特流依次代替模板中的 x,得到:11110000 10100000 10110000 10110000,即 F0 A0 B0 B0。
解码的过程也十分简单:如果一个字节的第一位是 0 ,则说明这个字节对应一个字符;如果一个字节的第一位1,那么连续有多少个 1,就表示该字符占用多少个字节。
3.2 UTF-16
UTF-16 是 Unicode 的一种编码方式,它用两个字节来编码 BMP 里的代码点,用四个字节编码其余平面里的代码点(暂不考虑字节顺序)。由于 BMP 里只有 65535 个代码点,所以直接把代码点转换成 2 个字节就可以了。BMP 之外的平面稍微复杂一点,需要先将代码点转化为一个代理对,然后再转为 4 个字节。
我们把 Unicode 编码记作 U。编码规则如下:
- 如果 U<0x10000,U的 UTF-16 编码就是 U 对应的 16 位无符号整数(为书写简便,下文将 16 位无符号整数记作 WORD)。
- 如果 U≥0x10000,我们先计算 U'=U-0x10000,然后将 U 写成二进制形式:yyyy yyyy yyxx xxxx xxxx,U 的 UTF-16 编码(二进制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx。
为什么 U 可以被写成 20 个二进制位?Unicode 的最大码位是 0x10FFFF,减去 0x10000 后,U 的最大值是 0xFFFFF,所以肯定可以用 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 的 UTF-16 编码与两个 WORD 的 UTF-16 编码区分开来,Unicode 编码的设计者将 0xD800-0xDFFF 保留下来,并称为代理区(Surrogate):
范围 | 说明 | 备注 |
---|---|---|
D800-DB7F | High Surrogates | 高位替代 |
DB80-DBFF | High Private Use Surrogates | 高位专用替代 |
DC00-DFFF | Low Surrogates | 低位替代 |
高位替代就是指这个范围的码位是两个 WORD 的 UTF-16 编码的第一个 WORD。低位替代就是指这个范围的码位是两个 WORD 的 UTF-16 编码的第二个 WORD。
UTF-16 计算规则
假设要编码的补充平面内的代码点为 X,具体的编码过程为:
- X 必定在 0x010000-0x10FFFF 之间
- 将 X 减去 0x010000,得到的数在 0x0-0xFFFFF 之间,正好可以用 20 个 bit 来表示
- 将高位的 10 个 bit 和 0xD800 相加,将地位的 10 个比特和 0xDC00 相加,得到的正好是一个代理对,也就是四个字节
Unicode3.0 中给出了辅助平面字符的转换公式:
High Surrogates:H = Math.floor((c-0x10000) / 0x400)+0xD800
Low Surrogates:L = (c - 0x10000) % 0x400 + 0xDC00
3.3 UTF-32
UTF-32 编码以 32 位无符号整数为单位。Unicode 的 UTF-32 编码就是其对应的 32 位无符号整数。
3.4 字节序
字节序有两种,分别是“大端”(Big Endian, BE)和“小端”(Little Endian, LE)。
根据字节序的不同,UTF-16可被实现为UTF-16LE或UTF-16BE,UTF-32可被实现为UTF-32LE或UTF-32BE。例如:
Unicode编码 | UTF-16LE | UTF-16BE | UTF32-LE | UTF32-BE |
---|---|---|---|---|
0x006C49 | 49 6C | 6C 49 | 49 6C 00 00 | 00 00 6C 49 |
0x020C30 | 43 D8 30 DC | D8 43 DC 30 | 30 0C 02 00 | 00 02 0C 30 |
Unicode 标准建议用 BOM(Byte Order Mark)来区分字节序,即在传输字节流前,先传输被作为 BOM 的字符“零宽无中断空格”。这个字符的编码是 FEFF,而反过来的 FFFE(UTF-16)和 FFFE0000(UTF-32)在 Unicode 中都是未定义的码位,不应该出现在实际传输中。
下表是各种 UTF 编码的 BOM:
UTF编码 | Byte Order Mark (BOM) |
---|---|
UTF-8 without BOM | 无 |
UTF-8 with BOM | EF BB BF |
UTF-16LE | FF FE |
UTF-16BE | FE FF |
UTF-32LE | FF FE 00 00 |
UTF-32BE | 00 00 FE FF |
参考:
- 《Unicode.org》:http://www.unicode.org/versions/#TUS_Latest_Version
- 《Unicode》:https://blog.csdn.net/wm_1991/article/details/52230716
- 《Unicode的流言终结者和编码大揭秘》:https://blog.csdn.net/soonfly/article/details/51161771
- 《从字节理解Unicode(UTF8/UTF16)》:https://www.cnblogs.com/zizifn/p/4716712.html
- 《UTF-8、UTF-16、UTF-32 编码》:https://blog.csdn.net/guxiaonuan/article/details/78678043
- 《百度百科Unicode》:https://baike.baidu.com/item/Unicode/750500?fr=aladdin
- 《Unicode character table》:https://unicode-table.com/en/
每天用心记录一点点。内容也许不重要,但习惯很重要!