unicode编码和utf-*编码

这个问题说起来其实挺简单的,但是很多刚接触的人因为这个问题而烦恼。其实就是对于计算机如何表示(存储)一个字符不理解。

它们的关系其实可以通过下图来表示:

1
2
3
4
5
                   +-------->utf-8
|
字 符+------>unicode+-------->utf-16
|
+-------->utf-32

我们知道,计算机只可以存储二进制的数据,所以,我们计算机如果需要存储字符(这里的字符包括英文字符、键盘上可见的字符、中文字符等等很多国家的字符),那么必须要有对应的二进制来表示(而二进制实际上我们可以人为的转化为一个数字)。

所以,我们要表示(存储)一个字符,就必须通过一个数字来表示,而unicode就用来做这件事情的。你可以理解为unicode编码实际上就是一张表,里面存储了所有数字->字符的关系,一个数字唯一的对应一个字符(反过来一个字符也唯一的对应一个数字)。

举个例子,汉字'严'对应的数字是4E25,这是十六进制(当然,你可以转换为十进制的你看起来顺眼的数字)。可以看出来要表示'严'这个字符,在计算机里面最少需要用两个字节来存储。为什么这里要用最少这个词呢?因为一个字符对应的unicode数字不一定就是它最终的存储形式。

因为计算机是不知道你表示'严'这个字符是用了两个字节。假设,我们在文件里面输入了如下字符串:

1
a严

其中,第一个字符是英文字母'a',第二个字符是中文字符'严'。那么,这个字符串转化为unicode就是:

1
614E25

OK,这是十六机制表示的。其中,第一个字节61代表字符'a',第二和第三个字节一起代表字符'严'

假设,我们直接以这种方式存储,这没问题对吧。但是,如果有一天,你拿到这串614E25,你知道它对应的字符串是什么吗?

是解析为614E25呢?还是614E25呢?或者是614E25呢?其实,我们都是无法得知的。所以,我们就需要一种规范来存储unicode。也就意味着,我们不可以简单的直接存储字符串对应的unicode串,而是需要规范化。

utf-8实际上就是unicode的一种规范。我们可以看看utf的全称:

1
Unicode Transformation Format

翻译过来就是:

1
Unicode转换格式

utf-8为例,unicode的存储格式如下:

1
2
3
4
5
6
7
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

左边的是unicode,右边的是utf-8(也就是字符在内存中真正的存储形式)。

那么,这里的协议体现在哪里呢?如下:

1
2
3
1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。

2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。

我们还是以字符串:

1
a严

为例。

这个字符串对应的unicode如下:

1
61 4E25

首先,我们需要把61这个unicode转换为utf-8的格式。因为61是在

1
0000 0000 ~ 0000 007F

范围里面的。所以我们对应第一条规则,对应的utf-8为:

1
00000061

然后,我们把4E25这个unicode转换为utf-8的格式。因为4E25是在:

1
0000 0800 ~ 0000 FFFF

范围里面的。所以我们对应第三条规则,对应的utf-8为:

1
2
3
4
5
6
7
8
9
10
11
12
13
11100100 10111000 10100101

如下是转化的过程:

4E25
对应的二进制如下,
0100 1110 0010 0101,
然后第三条规则的二进制模板如下,
1110xxxx 10xxxxxx 10xxxxxx,
我们发现有16个x被占了,
我们只需要按照顺序把0100 1110 0010 0101填进到对应的x里面就好了,
得到,
11100100 10111000 10100101

所以,当我们在编辑器里面输入字符串:

1
a严

会得到它的unicode

1
614E25

然后,如果我们是通过utf-8保存的,那么会以如下utf-8格式存储:

1
00000061 11100100 10111000 10100101

所以,当我们下次打开这个文件的时候,我们只需要以utf-8的格式打开,就可以得到对应的字符串了。解析的过程如下:

1
00000061 11100100 10111000 10100101

首先读入一个字节:

1
00000061

发现,最左边的一位是0,所以,这是在规则:

1
0000 0000 ~ 0000 007F | 0xxxxxxx

里面的,我们可以得到unicode

1
61

因为unicode唯一的对应一个字符,所以我们可以得到字符'a'

然后,我们继续读入下一个字节:

1
11100100

发现最左边的三位是三个1,这是在规则:

1
0000 0800 ~ 0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx

里面的,所以我们根据这条规则,从utf-8反解出它的unicode

1
4E25

得到对应的字符'严'

因此,我们就可以得到对应的字符串:

1
a严

因为,utf-16utf-32utf-8有着不同的规则,所以,如果我们先以utf-8的格式保存文件,然后再以utf-16或者utf-32的格式打开,那么就可能会乱码。原因就是unicode对应的utf-*的规则不同,导致打开文件的时候,从utf-*反解出来的unicode错了,不是原来的那个unicode,所以得到的字符自然就是错的了。

希望通过这篇文章,可以让大家理解编码问题,以及为什么会出现乱码。