简单聊聊Unicode
# 17.简单聊聊Unicode
接下来我们简单讲讲,Unicode的原理,例如,Unicode的设计思路,Unicode和UTF的关系等。
# 简单的字符编码模型
一个字符,在计算机中如何存储,是涉及到很多部分的。例如,一个字符集有多少个字符、如何编号和转成几个字节存储(也就是如何编码)等等,我们称这套机制为:字符编码模型(Character Encoding Model)。
前面我们说过ASCII这种简单字符集的编码思路:就是列出所有字符后,编号,然后编号的二进制数就是编码。即使是后面的GB系列编码,或者其他ANSI编码系列,其编码思路都是比较简单的
换句话说,在传统的字符编码模型中,基本上都是将字符集里的字符进行编号(字符编号转化为二进制数后一般不超过一个字节),然后该字符编号就是字符的编码。字符集和字符编码可以认为是等价的,并不需要进行严格区分
# Unicode的设计思路
Unicode等现代字符编码模型,并没有采用ANSI这种简单字符集的思路,采用了一个全新的思路。这个思路讲字符集和字符编码的概念,细致的分解为如下几个方面:
- 一套字符集有什么字符
- 这些字符的编号是什么
- 这些编号的规则
- 这些编号如何转为字节序列
- 在某些特殊的传输环境中(例如Email),如何将字节序列进行适应性编码处理
现代字符编码模型之所以要分解为这么几个方面,其核心思想是创建一个能够用不同方式来编码的通用字符集。
这意味着,同一个字符集,可以通用于不同的编码方式;也就是说,可以采用不同的编码方式来对同一个字符集进行编码。字符集与编码方式之间的关系可以是一对多的关系(而ANSI的字符集中,字符集和编码方式是唯一的)
更进一步而言,在传统字符编码模型中,字符编码方式与字符集是紧密结合在一起的;
而在现代字符编码模型中,字符编码方式与字符集脱钩了。用软件工程的专业术语来说,就是将之前紧密耦合在一起的字符编码方式与字符集解耦了。
可以把字符集和编码,跟接口及接口实现做个对比:
从这里可以很清楚地看到,
- 编码是依赖于字符集的,就像代码中的接口实现依赖于接口一样;
- 一个字符集可以有多个编码实现,就像一个接口可以有多个实现类一样。
# 现代字符编码模型
在Unicode Technical Report (UTR统一码技术报告)《UNICODE CHARACTER ENCODING MODEL (opens new window)》中,现代字符编码模型分为了5个层次,我们这里仅仅做简单介绍
- 第1层 抽象字符表ACR(Abstract Character Repertoire抽象字符清单):明确字符的范围,即确定支持哪些字符,可以简单理解为无序的字符集合。
- 第2层 编号字符集CCS(Coded Character Set):用数字给抽象字符表ACR中的字符进行编号。
- 第3层 字符编码方式CEF(Character Encoding Form字符编码形式、字符编码格式、字符编码规则):这一层主要是决定用几个字节存储字符,也就是如何将数字编号转为二进制。在之前简单的字符编码模型中,直接将数字编号转为二进制;但这样有缺点,我们后续展开。
- 第4层 字符编码模式CES(Character Encoding Scheme):可以简单理解为如何将多个字节存储到计算机中。因为我们用了多个字节,计算机如何知道几个字节代表一个字符呢?各个字符之间如何分隔、各个字节之间的先后顺序怎么定?
- 第5层 传输编码语法TES(Transfer Encoding Syntax):将字节序列作进一步的适应性编码处理。例如,在网络中的一些协议如何传输、如何压缩等等。
补充知识点:
对于第一层来说,字符表可以是封闭的(即字符范围是固定的),即除非创建一个新的标准,否则不允许添加新的字符,比如ASCII字符表和ISO/IEC 8859系列都是这样的例子;
字符表也可以是开放的(即字符范围是不固定的),即允许不断添加新的字符,比如Unicode字符表,目前有很多字符源源不断的加进来。
对于第二层来说,举个例子,汉字“严”的Unicode是十六进制数4E25,转换成二进制数足足有15位(0100 1110 0010 0101),也就是说这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。
对于第三层来说,汉字“严” 到底是用几个字节来存储呢?
对于第四层,汉字“严”的字节应该怎么排序?大端还是小端?字符之间如何分隔开?计算机如何知道3个字节表示一个字符,而不是1个,4个字节表示一个字符?
如果要在网络中传输汉字“严”(例如email),这么多个字节怎么传送?怎么压缩?
如果读者不清楚大端和小端的概念,请先看看阮一峰老师的博客:字节序探析:大端与小端的比较 - 阮一峰的网络日志 (opens new window),讲的非常透彻。
# Unicode的编码方案
前面我们说过,在现代字符编码模型中,可以采用不同的编码方式来对同一个字符集进行编码。而Unicode就有多种编码方式,称为UTF,全称Unicode Transformation Formats。
UTF 系列编码方案有UTF-8、UTF-16、UTF-32,均是由 Unicode 编码方案衍变而来,以适应不同的数据存储或传递。
换句话说,UTF8,UTF16和UTF32都是Unicode的实现,可以理解为将某个具体的Unicode字符,转为具体的某种格式(例如3个字节存储的格式,4个字节存储的格式)
例如汉字“严”的Unicode是十六进制数4E25, 在各种编码方式下的编码方案为:
编码 | hex | dec (bytes) | dec | binary |
---|---|---|---|---|
UTF-8 | E4 B8 A5 | 228 184 165 | 14989477 | 11100100 10111000 10100101 |
UTF-16BE | 4E 25 | 78 37 | 20005 | 01001110 00100101 |
UTF-16LE | 25 4E | 37 78 | 9550 | 00100101 01001110 |
UTF-32BE | 00 00 4E 25 | 0 0 78 37 | 20005 | 00000000 00000000 01001110 00100101 |
UTF-32LE | 25 4E 00 00 | 37 78 0 0 | 625868800 | 00100101 01001110 00000000 00000000 |
以上编码方式参考:严 - 中日韩象形文字: U+4E25 - Unicode 字符百科 (opens new window)
补充:
- 在Unicode标准中,码点采用了十六进制书写,并加上前缀U+,例如U+0041就是拉丁字母A的码点
- 码点(Code Point)在Unicode字符集中,每个字符映射成一个数字,这个数字被称为相应字符的码点。例如“严”字在 Unicode 中对应的码点是 U+0x4E25。
- 码元(Code Unit)是指一个已编码的文本中具有最短的比特组合的单元。对于 UTF-8 来说,码元是 8 比特长;对于 UTF-16 来说,码元是 16 比特长。换一种说法就是 UTF-8 的是以一个字节为最小单位的,UTF-16 是以两个字节为最小单位的。
- UTF-16BE指的是大端序,UTF-16LE指的是小端序,后续的UTF32同理
下面我们简单介绍下UTF32和UTF8
# UTF-32
在本系列第一篇文章我们说过,UCS-4字符集使用4个字节存储一个字符。
UTF-32是最好理解的一个了,其编码始终占用 4 个字节,足以容纳所有的 Unicode 字符,所以直接存储 Unicode 编号即可,不需要任何编码转换,提高了效率,固定的长度也能让计算机知道每个字符的截断范围,但浪费了空间。
# UTF-8
在1992年,UTF8编码出现了,针对不同字符,其可以用不同的字节数来存储,解决了存储空间浪费的问题。
UTF8则灵活的多,根据需要来决定使用多少个字节来存储字符,节省了很多空间
UTF-8的编码规则很简单,只有2个:
- 对于一个字节就能存储的符号,和ASCII的符号表一样(字节的第一位设为0,后面7位为这个符号的Unicode码)
- 对于需要多字节存储的符号,那么第一个字节从最高位开始,连续有几个比特位的值为 1,就使用几个字节编码,剩下的字节均以 10 开头。
举个莉子:
- 0xxxxxxx:单字节编码形式,这和 ASCII 编码完全一样,因此 UTF-8 是兼容 ASCII 的;
- 110xxxxx 10xxxxxx:双字节编码形式(第一个字节有两个连续的 1);
- 1110xxxx 10xxxxxx 10xxxxxx:三字节编码形式(第一个字节有三个连续的 1);
- 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx:四字节编码形式(第一个字节有四个连续的 1)
这里的x就是对应Unicode码点的二进制,如果一个字符的Unicode码点可以用2个字节存储,那么就会用第2种方式编码;如果要用3个字节,那么就用第3种方式编码。
由此我们可以得到下表:
十进制 | Unicode符号范围(十六进制) | UTF-8编码方式(二进制) |
---|---|---|
0-127 | 0000 0000-0000 007F | 0xxx xxxx |
128-2047 | 0000 0080-0000 07FF | 110x xxxx 10xx xxxx |
2048-65535 | 0000 0800-0000 FFFF | 1110 xxxx 10xx xxxx 10xx xxxx |
65536-1114111 | 0001 0000-0010 FFFF | 1111 0xxx 10xx xxxx 10xx xxxx 10xx xxxx |
那么一个Unicode符号如何转为UTF8呢?步骤如下:
- 首先将 16 进制的码点,通过进制转换 为十进制
- 然后使用十进制的数字查找上述表格处于哪个范围中,得出编码规则。
- 然后将码点转换为 2 进制,从低位到高位替换 x 即可得到字二进制的原码
- 将二进制的原码转换为补码存储。
举个例子,汉字“严”的Unicode是十六进制数4E25,十进制为20005,也就是上表的第3行,需要3个字节,二进制数为(0100 1110 0010 0101)。
我们分别将这15个二进制,填充到 1110 xxxx 10xx xxxx 10xx xxxx
中的x中(不足的补0),得到的结果:
1110 xxxx 10xx xxxx 10xx xxxx
0100 11 1000 10 0101
1110 0100 1011 1000 1010 0101
2
3
对三个字节分别求补码,得到
原码:11100100 10111000 10100101
取反:00011011 01000111 01011010
加一:00011100 01001000 01011011
2
3
这几个分别是-28,-72,-91的补码。
我们在Java中验证下:
byte[] b2 = "严".getBytes("utf-8");
for (int i = 0; i < b2.length; i++) {
System.out.println(b2[i]);
}
2
3
4
输出了-28,-72,-91
我们可以顺便打印下其二进制:
byte[] b2 = "严".getBytes("utf-8");
for (int i = 0; i < b2.length; i++) {
System.out.print(b2[i]);
System.out.print(" ");
System.out.println(Integer.toBinaryString(b2[i]));
}
2
3
4
5
6
运行结果:
-28 11111111111111111111111111100100
-72 11111111111111111111111110111000
-91 11111111111111111111111110100101
2
3
GBK同理。赵的 GBK 码点为:D5D4 ,转换为二进制:11010101 11010100
原码:11010101 11010100
补码:10101011 10101100
补码对应的字节数组为:{-43,-44},
byte[] b3 = "赵".getBytes();
for (int i = 0; i < b3.length; i++) {
System.out.println(b3[i]);
}
2
3
4
# UTF编码系列小结
我们简单总结下UTF编码系列:
- UTF - 8 (变长字节,可以用1~4个字节来存储一个字符),
- UTF - 32(固定使用4个字节)
- UTF - 16:介于 UTF-8 和 UTF-32 之间,使用 2 个或者 4 个字节来存储,长度既固定又可变
UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式。
一般来说,Unicode编码之间可以相互转换,例如UTF8可以转换为UTF16.
在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。用记事本编辑的时候,从文件读取的UTF-8字符被转换为Unicode字符到内存里,编辑完成后,保存的时候再把Unicode转换为UTF-8保存到文件。
# 查询Unicode编码
如果我们想知道某个字符的Unicode编码,怎么查呢?
可以去官网查询:Unicode – The World Standard for Text and Emoji (opens new window)
如果是汉字,可以去看专门的汉字对应表:字体编辑用中日韩汉字Unicode编码表 - 编著:资深中韩翻译金圣镇 金圣镇 (opens new window)
还有很多其他的网站提供查询,例如:基本拉丁字母 — ✔️ ❤️ ★ Unicode 字符百科 (opens new window)
# 编程语言与Unicode
很多语言都有查询Unicode字符码点的内置函数。
例如Python的ord函数:
C:\Users\peterjxl>Python
Python 3.10.5 (tags/v3.10.5:f377153, Jun 6 2022, 16:14:13) [MSC v.1929 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> ord("严")
20005
2
3
4
5
ps:汉字“严”的Unicode是十六进制数4E25, 其十进制就是20005
JS的codePointAt函数(读者可以打开浏览器控制台里输入):
"严".codePointAt()
20005
2
对于Java来说,以下结果输出:20005
public class TestUnicode {
public static void main(String[] args) {
String str = "严";
System.out.println(str.codePointAt(0));
}
}
2
3
4
5
6
# Unicode转字符
知道一个字符的Unicode码点,能否知道其对应什么字符呢?可以的。例如Java,直接打印即可:
str = "\u4E25";
System.out.println(str); //输出严
2
其他语言同理,这里不一一演示了。
微信自带的翻译功能,也支持将Unicode码点转为字符,只需发送一段文字,然后右键翻译即可。如果不行,多翻译几次。
# 关于Emoji
表情包,可以说是社交的灵魂。
2010年,Emoji也被纳入Unicode。什么是?Emoji就是一些小巧的表情包 🙃:
相信大家在各种社交媒体上都遇到过,例如微信,Twitter等,基本上都内置了Emoji。B站也不例外:
截图来自 爷真可爱💗_哔哩哔哩_bilibili (opens new window)
除了社交媒体APP上自带的Emoji,在邮箱、word文档里,我们能否快速输入表情包呢?可以 ,请读者参考输入法的技巧 (opens new window)
# 相关代码
相关代码已上传到Gitee:01.JavaSE/05.OOP/15.character · 小林/LearnJava - 码云 - 开源中国 (opens new window)