小白在学习 Python 中的“字符串”这个基本概念时,问了大神这样一个问题:既然 Python 内部的字符串都是使用 Unicode 来编码的,那干嘛还要在存储和传输的时候转成 UTF-8 呢?这样转来转去的多麻烦?
说实话,回答小白的问题,通常比回答大神的问题要难。因为他们总是问一些不着边际的问题。好在伟大的互联网时代,没有什么事情不可以通过在网上搜集信息,整理分析之后给出一个相对满意的回答的。
小白提出这个问题,主要是源于网上教程中一个错误的解释:Unicode标准也在不断发展,但最常用的是用两个字节表示一个字符(如果要用到非常偏僻的字符,就需要4个字节)。于是,小白就认为,既然2个字节可以搞定,干嘛还搞成 UTF-8,因为那样可能需要 3 个字节或者更多来表示一个字。这里的错误是什么呢?就是混淆了“标准”和“实现”。好吧,小白肯定又要晕了。
先来举一个现实中的例子:现在我们使用的长度单位是“米”。这就是一个标准。这个标准怎么定义的呢?在国际计量学会有几个标准“米”容器,那个东西的长度,就被定义成一个标准的“米”。但不会有谁真的跑去用那个标准容器。我们都是以那个标准容器为样板,生产自己的尺,来测量东西。这个尺就是“米”这个标准的一种实现。我们可以做一根硬的木尺,也可以是软的卷尺,还可以是使用激光测距的光尺。不管怎么说,只要这个尺量出来的“米”和标准容器量出来的“米”是一样的,那就可以了。
那么 Unicode 是怎么回事呢?Unicode 做为一个国际标准,其实只是定义了每个字符对应的一个数字(实际上 Unicode 包含很多复杂的内容,这里只说最简单的部分),比如 20031 这个数字就代表“中”这个字,25991 代表“文”这个字。但其实 Unicode 并没有说你要怎么保存一个字。
现在大家都知道,电脑里存信息,通常使用的单位叫:字节。比如我们说一首歌下载需要 3MB 的流量,翻译成“人话”,就是需要传输 3 百万字节的信息。一个字节能表示的数字非常的少,只能表示 0 - 255 这 256 个数。那如果我们需要表示超过 255 的数字要怎么办?那当然就是用多个字节。比如两个字节就可以表示 0 - 65535 这 65536 个数了。那我要表示负数怎么办?最笨的办法就是用3个字节嘛,用一个字节来表示正负,剩下的字节来表示数字。当然小白肯定会说:正负就两个状态,需要一个字节,那多浪费!于是,有就了各种不使用3个字节也能表示正负数的方法。这部分内容,任何一本“计算机原理”的书里都会讲到。什么符号位啊,补码啊……
讲这么一堆关于怎么表示一个数字的问题干什么呢?和这里说的 Unicode 和 UTF-8 有什么关系呢?当然有关系!这里大神要说的意思就是:同样一个数,你可以选不同的方式来表示它(比如刚才说的用3个字节来表示正负数,或者使用符号位来表示正负数)。选用什么方案,就是对 Unicode 的一种编码方法,也就是我们刚才说的“实现”。为什么前面提到的教程作者会说 Unicode 通常是两个字节呢?因为在 Unicode 早期,设计 Unicode 的人学识有限。他们在考查了世界上主要的文字之后,感觉用2个字节也就是最多 65536 个字符应该够用了。对应于一种 2 字节的编码方案叫 UCS2。后来做着做着,发现不对了,2个字节不够用了(现在 Unicode 包含了 12万 8 千个字符),又扩展到4个字节(因为计算机使用二进制的原因,3个字节在读写的时候很没效率,所以都是2的倍数来扩展的),对应于一种编码方案叫 UCS4。而对于我们来说,常用的中文字都在 65535 以下,所以通常 UCS2 就能表示常用的 6000 多个汉字了。这里说的 UCS2 和 UCS4 都是 Unicode 的“实现”,或者叫编码方案。虽然,因为 UCS2 不能覆盖全部的 Unicode 字符集,已经被标为“废弃”了,但不要被他们骗了,一个已经使用了很久的技术,想废弃是非常难的。
UCS2 和 UCS4 都是固定编码长度的方案。也就是说不管是什么字符,都使用 2 个字节或者 4 个字节来表示。对于我们来说,这并不是什么问题,但问题出在:这个世界上最流行的语言不是汉语,而是英语(中国人表示不服!汉语是世界上使用人数最多的语言)。虽然把每个英语字母都使用两个字节来表示也不是什么大的问题,可是,在遥远的计算机启蒙时代,无论是硬盘的大小还是网络的速度,都非常的有限,如果不对传输的内容进行压缩和限制,那成本是非常高的。于是就有了 UTF-8。
和 UCS2 这样的固定编码长度的方案不同的的是,UTF-8 并不是固定长度的。最短的 UTF-8 字符只需要使用 1 个字节。最长是 5 个字节。最长那 5 个字节能表示的数字,相当于用 4 个连续字节用来表示正负数,里面正数的个数,大概有20多亿,具体是多少,其实也不重要。看起来,多用了一个字节,还没有原来表示的数字多,好像很浪费,但这里的目的是:最常用的英文字母,使用最少的字节,不常用的,反正也不常用,多两个字节没关系。(这里请不要批判帝国主义霸权。不是人家想把英文搞这么短,是我们的中文想搞短也做不到……)
说回 Python。在 Python 3.3 版之后,对内部的 str 表示做了一些改进。教程里说:Python 内部使用 Unicode 来表示字符串,而在保存到硬盘或者发送到网上,需要转成 UTF-8。这个说法其实不够准确。在 Python 内部(不同的版本可能不同,大神在这里引用的是 Python 3 的文档),是使用 UCS 的各种版本来表示字符串的。比如说,对整个字符串做一次扫描,发现所有的字符都小于 255,那么就使用 UCS1,也就是一个字节的编码方案;如果发现有大于 255 的字符,但又都小于 65535,那么就使用 UCS2。当然,如果发现有奇怪的字符,那就只能使用 UCS4 了。为什么在内部要使用 UCS 呢?怎么不继续使用 UTF-8,那样不就不用转换了吗?(小白总是想省事)这个就涉及到 UTF-8 的一个缺点:计算字符串长度和查找子字符串非常没效率。在使用 UCS2 的时候,要想知道这个字符串有多长,只要看一下它占了几个字节,然后除个 2 就可以了,而 UTF-8 的话,就需要一个字符一个字符的数出来。在做子字符串搜索的时候,因为不知道下一个字符占几个字节,所以那些高效的搜索算法也都不灵了(小白:算法是啥?)。基于这两个原因,只好做一个转换:在保存到硬盘或网络传输的时候,使用一种压缩的方案,使得传输或保存需要的字节数最少;而在内部进行处理的时候,则使用效率更高的 UCS 方案。
PS:大神非常爽的讲完之后,小白似懂非懂的走开了……
参考资料: