Unicode 和 UTF-8 有什么区别?老虎会游泳的回答

@Ta 2021-06-14发布,2021-06-14修改 2993点击

同时发布于 https://www.zhihu.com/question/23374078/answer/1940110162


Unicode是一种为字符分配编号的方案,一个字符的Unicode编号(又称为码点)是一个数字,通常用十六进制表示,取值范围是0到10FFFF(转换成十进制就是0到1114111)。

比如,“虎”字的Unicode码点为“864E”,通常写作“U+864E”,也就是十进制的34382。

所以,当有人说“U+864E”时,他就是在说“虎”字。当有人说“Unicode码点34382”时,他也是在说“虎”字。

但如果对方说的是“U+1F405”或者“Unicode码点128005”,那就不再是“虎”字,而是虎的emoji图案?。

图片.png


看了上面的陈述,我们可以明确一点:当我们说“Unicode”的时候,我们说的是一种为字符编号的方式。一个特定字符有且只有一个Unicode编号(或者称为码点),但是怎么表达和存储这个编号,则不在Unicode的规范之内。

比如,人们可能会使用Unicode推荐的十六进制表示法,表示成“U+864E”。人们也可以使用十进制表示法,表示成“34382”。还可以使用八进制(103116)、二进制(1000011001001110)等等。但是无论这个数怎么表示,它都是同一个数,那个分配给字符“虎”的数。而这些数与字符的对应关系,就叫做Unicode。


Unicode是数与字符的对应关系,它是一种概念,所以它只对人类有意义。计算机不能处理和存储概念,只能处理和存储数据。所以我们必须想个办法,把Unicode的概念转换成计算机可以处理的具体数据,而这就是“UTF”(Unicode Transformation Format,Unicode转换格式,Unicode转写方式)。

“UTF”的设计目的,是把Unicode规范为字符分配的编号转换为计算机可以直接存储和处理的“八位字节序列”。

现今大多数计算机体系结构都使用8个二进制位做为一个字节,在处理和存储数据时只能以字节为单位。而Unicode码点是一个数,它的取值范围是十六进制的0到10FFFF,如果要转换为8位字节,就需要1字节到3字节的存储空间,不同的码点需要的字节数不同。

但是,如果直接按码点所需的最小字节数进行存储,在读取时就会产生问题:我们怎么知道一个码点有多少字节?

拿“虎”(U+864E)及其emoji图案(U+1F405)举例,存储它俩最少只要5字节,十六进制表示为“86 4E 01 F4 05”。但是如果真的这样存储,读取时就会陷入困境,因为字节和字节之间是没有边界的,我们不知道“86 4E 01 F4 05”中的哪部分属于第一个字符,哪部分属于第二个字符。我们甚至不知道“86 4E 01 F4 05”到底表示了几个字符,因为这一串序列可以拆分成多种形式,比如:

†Nô
图片.png

U+86U+4EU+01U+F4U+05

虎ô
图片.png
U+864EU+01U+F4U+05

†丁ô
图片.png
U+86U+4E01U+F4U+05

虎Ǵ
图片.png
U+864EU+01F4U+05

虎?
图片.png
U+864EU+01F405

不同的拆分方法将得到完全不同的字符,所以这种存储方式肯定是不可行的。要让计算机可以正确拆分码点以便还原字符,就必须明确指定码点的边界。不同的“UTF”(Unicode转写方式)就该问题采用了不同的技术方法。


首先想到的方案就是固定长度。既然拆分难是因为不同码点的字节数不同,那把字节数少的前面补0填充到和字节数多的一样长不就可以了吗。既然Unicode的最大码点占3字节,那就所有码点都补0到3字节后再存储。比如,“U+864EU+1F405”存储为“00 86 4E 01 F4 05”。

此时,我们就得到了第一种Unicode转写方式:UTF-24

但该转写方式没有得到业界的广泛采用,它甚至从未正式推出,只是作为想法在Unicode邮件列表中被人提出过。该方案最大的问题在于它是3字节为一组,但计算机一次只能读取1、2、4、8个字节。所以计算机要么分两次读取,先读取2字节再读取1字节,要么一次性读取4字节再删除1字节,操作都不是很方便。


所以,在前面再补一个字节不就行了?既然计算机一次只能读取1、2、4、8个字节,那干脆用4个字节存储一个码点。这样“U+864EU+1F405”就存储为“00 00 86 4E 00 01 F4 05”,浪费一个字节就浪费嘛,反正现在的设备内存硬盘都很大。

于是,我们就得到了第二种Unicode转写方式:UTF-32。它在很多现代计算机程序中都得到了采用。


UTF-32有什么优点?简单明确。

UTF-32有什么缺点?显而易见,占用空间大,存储一个字符最少浪费1字节,最多浪费3字节!虽然现代计算机内存和硬盘都很大,但是网速却往往不够快,有的还是按流量计费的。如果要通过网络发送文本,采用UTF-32似乎不太经济,所以能不能减少一点,采用2字节?

对大部分字符来说,没问题。比如“虎”(U+864E)就恰好可以存储为“86 4E”,字母“A”(U+41)可以存储为“00 41”。

但是虎的emoji?表情怎么办?它是“U+1F405”。如果存储为2字节,那开头的“1”不就没了吗?这样肯定不行。

还有笑哭?(U+1F602)、OK?(U+1F44C)、鼓掌?(U+1F44F)等等,如果采用2字节,我们是不是只能和它们说再见了?

别担心,还是有方法。

Unicode给字符编号的时候并不是连续分配的,而是按空间一块一块分配的。所以实际上有些编号范围从未分配给字符,其中比较大的一块范围是U+D800U+DFFF。于是我们很自然的就会想到,“我们可以用这块空间存储emoji表情包啊”。

确实可以,但是这块空间太小了,只有2048码点。但是现在emoji表情包已经有1816个了,再加上一些增补汉字、数学符号、以及各国的古文字……2048个码点根本不够用。毕竟如果0000到FFFF够用的话,Unicode就不会扩展到最大10FFFF了。

那怎么办?既然一个2048码点存不下,那就再跟一个2048码点不就行了?这样就有了2048×2048=4194304码点,绝对够用了。就算砍一半,1024*1024=1048576码点,也已经足够了。

所以现在的方案就是,用两个U+D800U+DFFF范围的码点(共4字节)表示一个范围在U+10000U+10FFFF的字符,码点数值减去0x10000之后恰好剩20个二进制位(0到0xFFFFF),其中前10位用0xD800到0xDBFF表示,后10位用0xDC00到0xDFFF表示。而这种表示方法就被称为“代理对”。

4字节一组的代理对,再加上原先2字节一组的普通字符,就构成了一种更省空间的Unicode转写方式:UTF-16

尝试把“虎”及其emoji编码为UTF-16。

虎:

  • U+864E
  • 很简单,直接编码为 86 4E

虎的emoji?:

  • U+1F405
  • 用2字节无法表示,所以需要使用代理对
  • 先减去0x10000,得到 F405
  • 转换为二进制,得到 1111010000000101
  • 从尾部按10位切分,得到 111101 和 0000000101
  • 转换为两个十六进制数,得到 3D 和 5
  • 与 D800 和 DC00 相加,得到 D83D 和 DC05
  • 组合在一起,D8 3D DC 05,这就是虎的emoji?的UTF-16代理对表示

整体表示为 86 4E D8 3D DC 05

解码器每次读取2字节,在读取到 D8 3D 这个字节序列时,因为它位于 D800 到 DBFF 之间,解码器就会知道它是代理对起始字符,会把它和下一个字符组合起来,按上述方法的逆过程还原成 U+1F405 码点。


可是,字母和数字的Unicode码点(U+0U+7F)明明用一个字节就能表示,在UTF-16里却需要两个字节。每个字母数字前面都得补个0字节,这不是一种浪费吗?特别是在字母和数字非常多的程序代码和网页代码里,UTF-16几乎要浪费一半的空间。有没有什么方法能把这些空间节省下来,让字母和数字只用1个字节就能表示?至于边界的问题,就不能在字节内部把边界表示出来吗?

当然可以。在字节内部表示边界的方法通常称为“自同步”,它有多种达成方式,以下是其中一种:

把字节序列的二进制表示从左往右排开,用最左边1的数量表示该字符所具有的字节数,直到遇到第一个0为止。

比如:

  • 如果第一个字节是0xxxxxxx,表示该字符只有1字节。
  • 如果第一个字节是110xxxxx,表示该字符有2字节。
  • 如果第一个字节是1110xxxx,表示该字符有3字节。
  • 如果第一个字节是11110xxx,表示该字符有4字节。
  • ……

而这种表示方法,就是“UTF-8”采用的方法。“UTF-8”中的“8”,既是指它的字符最少只需8位,也是指它以8位为一组实现了可变字节数的字符编码。

此外细心的读者可能发现了,上面说“如果第一个字节是110xxxxx,表示该字符有2字节”,而不是“如果第一个字节是10xxxxxx,表示该字符有2字节”。我们跳过了“10xxxxxx”,是因为“UTF-8”用它来表示尾随字节(字符的第2、3、4字节)。因为字母和数字用最高位为0的单字节表示,所以为了防止与它们发生冲突导致程序解码困难,尾随字节最高位必须为1。这样一来,为了尽可能充分利用空间,尾随字节的次高位就应该立即为0,以在防止与起始字节冲突的同时给编码留下最大的可用空间。所以表示2字节字符的起始字节跳到了从“110xxxxx”开始。

以下是UTF-8编码方案的完整形式:

图片.png

但是实际上5字节和6字节的编码不会出现,因为Unicode最大码点只到U+10FFFF,用4字节就够了。

下面我们来把“虎”字及其emoji转换为UTF-8编码。

虎:

  • 码点为 U+864E
  • 转换为二进制,1000011001001110
  • 从末尾开始按6位分组,1000 011001 001110
  • 需要3字节,开头插入1110,中间插入两个10,得到 11101000 10011001 10001110
  • 转换为十六进制,得到 E8 99 8E

虎的emoji?:

  • 码点为 U+1F405
  • 转换为二进制,11111010000000101
  • 从末尾开始按6位分组,得到 11111 010000 000101
  • 第一部分为5位,开头插入1110的话就9位了,一个字节放不下,所以前面得补一个字节,用4字节表示。字节中间和末尾空隙填充0,然后每个尾随字节前面都补10,也就是 11110000 10011111 10010000 10000101
  • 转换成十六进制,得到 F0 9F 90 85

所以“虎”及其emoji的UTF-8编码就是:

E8 99 8E F0 9F 90 85

解码器读取到E8就能知道第一个字符3字节。然后跳过2字节读取到F0就能知道第二个字符4字节,所以不会有边界问题。


看了这么多之后,现在我们可以回到问题本身。

Unicode和UTF-8有什么区别?

数字和文件有什么区别?

无法定义区别。只有同类事物才能找出区别,两者是不同类型的事物,因此是不可比较的。

在这里,Unicode是分配给一系列字符的一系列数字,而UTF-8,是把这一系列数字存储成计算机文件的其中一种方案。

回复列表(6|隐藏机器人聊天)
添加新回复
回复需要登录