文本处理(text processing)是可以说计算机的最重要的应用领域之一。要使计算机能够处理文本,必须将文本中的字符映射成计算机可以处理的单元,也就是字节。早期的计算机系统都采用ASCII字符集,映射方法比较简单,一个字节就对应一个ASCII字符。后来发展到了Unicode,字符集太大,映射方法(也就是编码方法)变得多种多样,常见的如:UTF-8,UTF-16,UTF-32等等。

此文写得比较混乱,有比较大的改进空间。需要厘清各种概念,并增加例子和图示。

字符(character)和字符集(character set)

字符是构成语言的基本单位,而文本是由语言书写。比如在英文文本中,A是一个字符;在汉语文本中,是一个字符。出了前面两个例子以外,文本处理中还专门用到一些控制字符,来对文本进行一些操作,例如回车、换行等等。

人类语言之丰富,其书写的文本灿若繁星,其中所需要用到的字符更是数不胜数。如何让计算机能够处理这些文本呢?这就涉及到文本的编码(text encoding)。计算机处理的基本单位是字节,所谓编码,就是用字节去表示字符。一个基本的问题就产生了,有多少字符需要编码?如果涉及的字符集太小了,可能不够日常使用。早期我们处理汉字文本的时候,使用的是GB2312的字符集,只包含两千多个汉字,导致有些带生僻字的人名无法子啊计算机中显示。

如果涉及的字符集太大了,处理起来不经济,需要使用更多的字节来表示字符。试想,作为一个普通的中国人,只需要认识一些常用的字符,比如英文字母,常用汉字等,日常生活就足够了。如果你去学习阿拉伯文,恐怕除了做翻译以外,没有其他更好的实际用途呢。

ASCII字符集

如果一个计算机系统和另外一个计算机系统之间采用不同的字符集,那么这两个计算机系统就很难对话。就像一个说英文国家的人到了一个说西班牙语的国际,语言不同,需要翻译和转化。为了能够在计算机之间能够顺畅实现文本的传递,那么最好有一个标准化的字符集供大家使用。目前应用最广泛,影响最深远的不过于由ANSI(American National Standards Institute)定义的ASCII字符集

ASCII是一个以英文为主的字符集,总共包含128个字符,可以用7个比特表示。而计算机的字节一般是8个比特,所以一个字节可以编码所有ASCII字符还不止。所以计算机在处理ASCII文本的时候,可以直接假设:一个字节 = 一个字符。事实上,在早期的编程语言,比如C语言中,字节和字符是不分的,字节类型用character的缩写char表示。

扩展ASCII

毕竟ASCII定义的字符集太小了,只够说英文的国家用。用来表示其他拉丁语种都嫌少,更别谈支持中日韩等字符特多的语言了。正好8位字节编码完7位的ASCII,还有一位比特剩余。好多语言就对这个多出来的比特位动起了念头。最常见的针对拉丁语言的扩展是ISO-8859-1,它也是单字节编码,共定义了256个字符,低128个字符跟ASCII完全兼容,高128位包括扩展字符集。

但是呢,ISO-8859-1相比ASCII多出来的128个字符,还是不能够满足所有欧洲国家的需求。事实上,ISO-8859-1只能覆盖西欧国家的语言。另外还有:

  • ISO-8859-2 扩展的字符集用于中欧国家的语言,如:波兰、捷克等等
  • ISO-8859-3 扩展的字符集用于南欧国家的语言,如:土耳其、马耳他等等
  • 截止到目前,ISO-8859一共提了16中扩展ASCII方案,可以在维基百科的ISO-8859页面查看

我们的GB系列编码(GB2313、GBK、GB18030)也是基于ASCII的扩展,具体可以参考这篇知乎回答: 为什么那么多网站钟情 UTF-8? - 李昊的回答 - 知乎

码位表(CodePage)

在ASCII之前,不同的计算机系统使用不同的字符集。在ASCII出现之后,情况有所改观,各个计算机系统都支持ASCII了。但是并没有完全解决问题,不同的系统依然使用的不同的ASCII扩展。针对ISO-8859-1字符集编写的程序,在使用GB2312的系统上运行,就有可能出现乱码,导致显示不正确。为了应对这个问题,好多系统引入了码位表(CodePage)的概念,也就是说系统使用码位表来判断当前运行的程序使用的是哪个ASCII扩展。举个例子,在Windows系统上,用于支持简体中文的码位表是CP936;而针对日文的码位表是CP930。如果把针对CP930的程序直接放在CP936的系统上运行,就会出现乱码,因为两者使用不同的ASCII扩展字符集。

Unicode的出现

为了扫清混乱,unicode出现了。Unicode试图囊括这世界上所有的文字系统,形成一个统一的码位表。只要程序使用这个码位表,即便使用不同的编码,互相之间也可以无缝转化转化。

一个字符在Unicode里面是用一个码位(code point)来表示。比如英文H在Unicode的里面的码位是U+0048。如果用Unicode表示Hello呢,则是一串码位U+0048 U+0065 U+006C U+006C U+006F。这一串码位需要通过一定的编码方式,才能转化为计算机系统认识的字节,才可以在内存或者硬盘上存储。如果是使用UTF-8方式编码的话,上面那串码位直接对应ASCII的字符串"Hello",占5个字节存储空间,结果是48 65 6C 6C 6F

除了UTF-8以外,还有其他几种常见的Unicode编码方式:

  • UCS-2
  • UCS-4
  • UTF-16
  • UTF-32

UCS-2采用定长的两字节来表示一个码位,所以理论上最多能编码65535个码位,但是Unicode包含的码位已经超过了这个数字。后来变长编码的UTF-16作为UCS-2的替代者出现了,UTF-16同样采用两字节,也就是16bit作为一个编码单位。一个Unicode字符在UTF-16以一个或者两个16bit的编码单位表示,故UTF-16可以编码所有1,112,064个Unicode码位。现代的Windows内核基本上采用的都是UTF-16编码。

UCS-4和UTF-32可以看作是对等的,两者都是采用4字节(32bit)来表示一个Unicode码位。4字节的长度足够用来表示任意的Unicode字符了,所以UCS-4和UTF-32都是定长的。

用多个字节来编码一个Unicode码位可能会带来一些问题,一是对以英文这种基于字母表的语言,使用多字节编码其实挺浪费空间的,明明单字节足够了。二是在传输的时候还需要注意字节序的问题,比如UCS-2就有大端的UCS-2BE和小端的UCS-2LE两个变种。为了解决这个问题,Ken Thompson和Rob Pike发明了UTF-8。UTF-8的好处在于它是单字节变长编码,可以直接用C语言中的char数组表示,而不需要使用16-bit的数组,操作系统实现起来可以沿用很多现有的char数组操作函数。UTF-8完全兼容7bit的ASCII,并在设计上有许多独到之处,方便操作系统UTF-8编码的字符串当成一个char数组处理,具体可参照Wikipedia的UTF-8页面。很多操作系统(特别是Unix类的)和编程语言都把UTF-8作为默认的Unicode编码。

但UTF-8编码并不是十全十美的,它在编码中文的时候需要采用三个字节,而不像UTF-16那样只需两个字节。因此很多人觉得UTF-8对中文有歧视。这个问题见仁见智了。我个人的观点是编码是天生带有歧视,因为字符集收录字符的时候总有先后次序,计算机是美国人发明的,自然英文字符先被收录,然后被广泛支持,地位就高。

更多参考

(完)