蹲厕所的熊

benjaminwhx

编码的前世今生

2018-05-16 作者: 吴海旭


  1. 1、为什么要编码?
  2. 2、历史回顾
    1. 2.1、ASCII编码
    2. 2.2、ISO-8859-1
    3. 2.3、中文GB系列
    4. 2.4、字符集与字符编码
  3. 3、进入Unicode的世界
    1. 3.1、UCS
    2. 3.2、UTF
      1. 3.2.1、UTF-16
      2. 3.2.2、UTF-32
      3. 3.2.3、UTF-8
  4. 4、编码示例
  5. 5、总结
  6. 5、参考

1、为什么要编码?

很多刚入行甚至入行很久的人都不明白一个问题,那就是我们为什么要编码?我们能不能不编码?

由于人类的语言太多,表示这些语言的符号太多,无法用字节来表示,因为必须要经过拆分或一些翻译工作,才能让计算机理解我们的语言。我们可以把计算机能够理解的语言假定为英语,其他语言要能够在计算机中使用,必须得经过一次翻译,把它翻译成英语,这个翻译的过程就是编码。所以可以想象,只要不是说英语的国家,要使用计算机就必须经过编码。

注:一个字节(byte) = 8个比特(bit),范围 0~255,它是计算机中存储信息的最小单元。

下面我带着大家来看看编码的前世今生~

2、历史回顾

2.1、ASCII编码

很久以前,计算机制造商有自己的表示字符的方式。他们并不需要担心如何和其它计算机交流,并提出了各自的方式来将字形渲染到屏幕上。随着计算机越来越流行,厂商之间的竞争更加激烈,在不同的计算机体系间转换数据变得十分蛋疼,人们厌烦了这种自定义造成的混乱。

最终,计算机制造商一起制定了一个标准的方法来描述字符。他们定义使用一个字节的 低7位来表示字符,并且制作了如下图所示的对照表来映射七个比特的值到一个字符上。例如,字母A是65,c是99,~是126等等, ASCII码就这样诞生了。

原始的ASCII标准定义了从0到127 的字符,这样正好能用七个比特表示。不过好景不长。。。

为什么选择了7个比特而不是8个来表示一个字符呢?我并不关心。但是一个字节是8个比特,这意味着1个比特并没有被使用,也就是从128到255的编码并没有被制定ASCII标准的人所规定,这些美国人对世界的其它地方一无所知甚至完全不关心。

2.2、ISO-8859-1

其它国家的人趁这个机会开始使用128到255范围内的编码来表达自己语言中的字符。这一部分的字符集被称为“扩展字符集”。基于此,ISO 组织在ASCII码基础上又制定了一系列标准用来扩展ASCII编码,它们是ISO-8859-1~ISO-8859-15,其中ISO-8859-1(又称Latin-1)涵盖了大多数西欧语言字符,所有应用的最广泛。ISO-8859-1仍然是单字节编码,它总共能表示256个字符。

例如,144在阿拉伯人的ASCII码中是گ,而在俄罗斯的ASCII码中是ђ。即使在美国,对于未使用区域也有各种各样的利用。IBM PC就出现了“OEM 字体”或”扩展ASCII码”,为用户提供漂亮的图形文字来绘制文本框并支持一些欧洲字符,例如英镑(£)符号。

再强调一遍,ASCII码的问题在于尽管所有人都在0-127号字符的使用上达成了一致,但对于128-255号字符却有很多很多不同的解释。你必须告诉计算机使用哪种风格的ASCII码才能正确显示128-255号的字符。

但是问题又来了,不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。但是不管怎样,所有这些编码方式中,0-127表示的符号是一样的,不一样的只是多了128-255的这一段,因此它们都向下兼容ASCII编码。

同样,对于扩展ASCII,它也是既表示了字符集,也代表一种字符编码。

这些问题成为了系统开发者的噩梦。例如,MS DOS必须支持所有风格的ASCII码,因为他们想把软件卖到其他国家去。他们提出了「内码表」这一概念。例如,你需要告诉DOS(通过使用”chcp”命令)你想使用保加利亚语的内码表,它才能显示保加利亚字母。内码表的更换会应用到整个系统。这对使用多种语言工作的人来说是一个问题,因为他们必须频繁的在几个内码表之间来回切换。这依旧显得很繁琐。

注:内码表可以理解成是输入的字符和机器识别的二进制数的一个对应关系的码表。

2.3、中文GB系列

字符型语言的字符数量较少,因此用一个byte(8bit)基本就够用了,这就难为了我们博大精深的中文汉字——中文常用字就有好几千呢!但这也难不倒我们勤劳勇敢的中国人,为此,我们设计了GB2312字符集,意气风发走进那新时代。

GB2312的思想其实很简单——既然1个byte不够用,那咱们用2个呀!正所谓,“没有1byte解决不了的问题,如果有,就2byte”。理论上,2个字节便可以表示2^16=65536的字符。不过,GB2312最初被设计时,只规定了中文常见字,很多特殊字符还没有包含。GB2312一共收录了7445个字符,包括6763个汉字和682个其它符号。除了GB2312,还有用于中文繁体的Big5。

人们逐渐发现,GB2312规定的字符太少,甚至有些国家领导人名字中的汉字都表示不出来,这还了得!于是1995年的汉字扩展规范GBK1.0(《汉字内码扩展规范》)收录了21886个符号,它分为汉字区和图形符号区。GBK编码是GB2312编码的超集,向下完全兼容GB2312,同时GBK收录了Unicode基本多文种平面中的所有CJK汉字。GBK还收录了GB2312不包含的汉字部首符号、竖排标点符号等字符。

2000年的GB18030是取代GBK1.0的正式国家标准。GB18030编码向下兼容GBK和GB2312。GB18030收录了所有Unicode3.1中的字符,包括中国少数民族字符。GB18030虽然是国家标准,但是实际应用系统中使用的并不广泛。目前使用最广的仍是GBK编码。

GBK和GB2312都是双字节等宽编码,如果算上为与ASCII兼容所支持的单字节,也可以理解为是单字节和双字节混合的变长编码。GB18030编码是变长编码,采用单字节、双字节和4字节方案,其中单字节、双字节和GBK是完全兼容的,4字节编码的码位就是收录了CJK扩展A的6582个汉字。

从ASCII、GB2312、GBK到GB18030,这些编码方法是向下兼容的,即同一个字符在这些方案中总是有相同的编码,后面的标准支持更多的字符。在这些编码中,英文和中文可以统一地处理,区分中文编码的方法是高字节的最高位不为0。GB2312、GBK都属于双字节字符集 (DBCS)。

2.4、字符集与字符编码

看了上面的发展历史,你一定会觉得统一多国编码的重要性(就和秦始皇统一六国度量衡一样重要),但这个时候我们需要了解两个概念,什么是 字符集字符编码 呢?

所谓字符集,直观上讲,就是人们统计预先规定好的一系列字符与二进制序列(数字)之间的映射关系。只要大家都遵循这个规则,并且计算机也按照这种方式处理,那么这个世界不就很美好了!然而,全世界的语言实在太多了,由于历史和地域的原因,也就形成了多套应用于不同场合、语言的字符集,如ASCII、GBK、Unicode等。

那么这个时候我们有了一个疑问,计算机为什么不按照字符集中字符对应的数字来进行存储呢?为什么还需要字符编码的存在呢?因为有的时候,我们需要按照一定的规则,将字符的码元再次处理,以便能够更加适应计算机存储、网络传输的需要。

因此,从某种意义上我们可以理解为:字符集是一种协议,而字符编码是对字符集的一种实现,当然,既然称为实现,也说明对同一种字符集可能有不同的编码方式。可以想到,最直接的编码方式,便是直接使用字符对应的二进制序列,这就导致了字符集和字符编码看起来像是一个东东,长期受此思维影响,可能就会对Unicode和UTF的区别有些困惑。

3、进入Unicode的世界

介绍完ASCII、Latin和GBK以及字符集与字符集编码,想必各位会有这样的想法:为什么不规定一种字符集、编码方式,直接能够囊括全世界所有语言文字的符号呢?

出于这个目的,Unicode诞生了。

Unicode是为了整合全世界的所有语言文字而诞生的,全称是Universal Multiple-Octet Coded Character Set,它所规定的字符集也被称为Universal Character Set (UCS)。

再次提醒,Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。也即,UCS规定了怎么用多个字节表示各种文字,而怎样存储、传输这些编码,则是由UTF (UCS Transformation Format)规范规定的。UTF会在后文介绍。

3.1、UCS

UCS有两种不同的规定版本:UCS-2和UCS-4。顾名思义,UCS-2就是用2个字节编码,UCS-4就是用4个字节(实际上只用了31位,最高位必须为0)编码。因此,UCS-2有2^16=65536个码位,UCS-4有2^31=2147483648个码位。目前UCS-2已经足够用了,UCS-4估计都可以把Asgard和氪星的文字(传说都是英语?)也收录进来了……具体的符号对应表,可以查询unicode.org ,或者相关的字符对应表。

UCS-4根据最高位为0的最高字节分成128个Group,每个Group再根据次高字节分为256个Plane,每个Plane根据第3个字节分为256行 (Rows),每行包含256个Cells。同一行的Cells只是最后一个字节不同,其余都相同。

其中,Group 0的Plane 0被称作Basic Multilingual Plane,即BMP,或者说UCS-4中,高两个字节为0的码位被称作BMP。将UCS-4的BMP去掉前面的两个零字节就得到了UCS-2。在UCS-2的两个字节前加上两个零字节,就得到了UCS-4的BMP。而目前的UCS-4规范中还没有任何字符被分配在BMP之外。

那么,新的问题来了:

1)如何才能区别Unicode和ASCII?计算机怎么知道2个字节表示1个符号,而不是分别表示2个符号呢?

2)我们已经知道,英文字母只用1个字节表示就够了(ASCII),如果Unicode统一规定,每个符号用2个或4个字节表示,那么每个英文字母前都必然有许多字节是0,这对于存储来说是极大的浪费,是无法接受的。

这些问题造成的结果是:

1)出现了Unicode的多种存储方式,也就是说有许多种不同的格式,可以用来表示Unicode。

2)Unicode在很长一段时间内无法推广,直到互联网(UTF-8)的出现。

3.2、UTF

UTF(Unicode/UCS Transformation Format),即Unicode字符集的编码标准,可以理解为对Unicode字符集的具体使用/实现方式,主要有UTF-16、UTF-32和UTF-8。

3.2.1、UTF-16

UTF-16由RFC2781协议规定,它使用2个字节来表示1个字符。不难猜到,UTF-16是完全对应于UCS-2的(实际上稍有区别),即把UCS-2规定的代码点通过Big Endian(下文介绍)或Little Endian方式直接保存下来。UTF-16包括三种:UTF-16,UTF-16BE(Big Endian),UTF-16LE(Little Endian)。

UTF-16BE和UTF-16LE不难理解,而UTF-16就需要通过在文件开头以名为BOM(Byte Order Mark,下文介绍)的字符来表明文件是Big Endian还是Little Endian。BOM为\uFEFF这个字符。其实BOM是个小聪明的想法。由于UCS-2没有定义\uFFFE,因此只要出现 FF FE 或者 FE FF 这样的字节序列,就可以认为它是\uFEFF,并且据此判断出是Big Endian还是Little Endian。

例:“ABC”这三个字符用各种方式编码后的结果如下:

另外,UTF-16还能表示一部分的UCS-4字符——\u10000~\u10FFFF,表示算法就不再详细介绍了。

总结来说,UTF-16以2、4字节存储一个Unicode编码,对于小于0x10000的UCS码,UTF-16编码就等于UCS码对应的16位无符号整数。对于不小于0x10000的UCS码,定义了一个算法。不过由于实际使用的UCS2,或者UCS4的BMP必然小于0x10000,所以就目前而言,可以认为UTF-16和UCS-2基本相同(前提是明白UTF和UCS的差别)

3.2.2、UTF-32

UTF-32用4个字节表示字符,这样就可以完全表示UCS-4的所有字符,而无需像UTF-16那样使用复杂的算法。与UTF-16类似,UTF-32也包括UTF-32、UTF-32BE、UTF-32LE三种编码,UTF-32也同样需要BOM字符。仍以“ABC”为例:

UTF-16和UTF-32的一个缺点就是它们固定使用2个或4个字节,这样在表示纯ASCII文件时会有很多零字节,造成浪费。RFC3629定义的UTF-8则解决了这个问题,下面介绍UTF-8。

3.2.3、UTF-8

随着互联网的普及,强烈要求出现一种统一的编码方式,UTF-8就是在互联网上使用最广的一种Unicode的实现(传输)方式。

UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1-4个字节表示一个符号,根据不同的符号而变化字节长度。为什么采用边长的机制,实际上和字符出现的概率分布有关,其中蕴含着Huffman编码的思想——最常出现的字符编码尽量的短。

UTF-8的编码规则很简单,如下:

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

下表总结了编码规则,字母x表示可用编码的位。

可见,ASCII字符(\u0000~\u007F)部分完全使用一个字节,避免了存储空间的浪费,而且UTF-8可以不再需要BOM字节。并且可以从上图的编码结果一眼看出编码的字节数。

例:以汉字“严”为例。

已知“严”的Unicode(UCS-2)码是\u4E25(0100_1110_0010_0101),根据上表,可以发现\u4E25处在第三行的范围内(0000 0800 ~ 0000 FFFF),因此“严”的UTF-8编码需要3个字节,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然后,从“严”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,“严”的UTF-8编码是“11100100 10111000 10100101”,转换成十六进制就是E4 B8 A5。

可以看到“严”的Unicode码是\u4E25,而UTF-8编码是E4 B8 A5,两者是不一样的。它们之间的转换可以通过程序或一些编辑器实现。

4、编码示例

字符串“I am 君山”用 ISO-8859-1 编码时,编码结果如下图所示:

可以看出,7个char字符经过ISO-8859-1编码转变成7个byte数组,中文君山被转换成值是3f的byte。3f也就是“?”字符,所以会经常出现中文变成“?”,很可能就是错误的使用了ISO-8859-1这个编码导致的。这个通常我们称之为“黑洞”。

下面是使用GB2312编码的效果图:

GB2312有一个从char到byte的码表,不同的字符编码就是从这个码表找到与每个字符对应的字节,然后拼装成byte数组。可以看出,前5个字符经过编码后仍然是5个字节,而汉字则被编码成双字节,GB2312只支持6763个汉字,所以并不是所有汉字都能够用GB2312编码。

下面是使用GBK编码的效果图:

你可能已经发现,它与GB2312编码的结果是一样的。上面我们已经说过了,GBK向下兼容,只要是经过GB2312编码的汉字都可以用GBK进行解码,反之则不然。

下面是使用UTF-16编码的效果图:

UTF-16编码将char数组放大了1倍,单字节范围内的字符在高位补0变成两个字节,中文字符也变成两个字节。从UTF-16编码规则来看,仅仅将字符的高位和低位进行拆分变成两个字节,特点是编码效率非常高,规则很简单,由于不同处理器对2字节的处理方式不同,有Big-endian(高位字节在前,低位字节在后)或Little-endian(低位字节在前,高位字节在后)编码。在对字符串进行编码时需要指明到底是Big-endian还是Little-endian,所以前面有两个字节用来保存BYTE_ORDER_MARK值,UTF-16是用定长16位来表示的UCS-2或Unicode转换格式,通过代理来访问BMP之外的字符编码。

下面是使用UTF-8编码的效果图:

UTF-16虽然编码效率很高,但是对单字节范围内的字符也放大了1倍,这无形也浪费了存储空间。另外UTF-16采用顺序编码,不能对单个字符的编码值进行校验,如果中间的一个字符码值损坏,后面的所有码值都将受影响。而UTF-8不存在这些问题,UTF-8对单字节范围的字符仍然用1个字节表示,对汉字采用3个字节表示。

5、总结

  1. 编码界最初只有ASCII码,只用了1byte中的7bit(0~127);
  2. 欧洲人发现128个不够了,就把1byte中没用的最高位给用上了,出现了Latin系列(ISO-8859系列)编码;
  3. 中国人民通过对ASCII编码进行中文扩充改造,产生了GB2312编码,可以表示6000多个常用汉字;
  4. 汉字实在太多了,还有繁体、各种字符呀,于是加以扩展,有了GBK;
  5. GBK还不够,少数民族的字还木有呀,于是GBK又扩展为GB18030;
  6. 每个国家、语言都有自己的编码,彼此无法交流,迫切需要大一统局面的出现;
  7. Unicode诞生,可以容纳全世界的任何文字。Unicode分为UCS-2和UCS-4,分别是2字节和4字节,实际2字节就够用了;
  8. 为了Unicode能实际应用(存储、传输),制定了Unicode的编码方式,即UTF,有UTF-8、UTF-16、UTF-32,其中UTF-8应用广泛;
  9. UTF-16、UTF-32均是多字节传输,存在字节顺序的问题,于是有了大头还是小头的概念,为了解决这个问题,引入了BOM。UTF-8是单字节传输,不存在这个问题,也就不需要BOM,但可以有,仅用来表明编码格式;

5、参考

对于字符编码,程序员的话应该了解它的哪些方面?

Unicode 是不是只有两个字节,为什么能表示超过 65536 个字符?

学点编码知识又不会死:Unicode的流言终结者和编码大揭秘



坚持原创技术分享,您的支持将鼓励我继续创作!



分享

评论