编码研究笔记

本次讨论的编码涉及到Unicode、传统的ascii、和ascii的扩展集ansi中的中国编码、西文编码。
老实说老师都一直以ascii编码讲解文字的编码,汉子编码则是一笔带过,害我在很长时间误认为电脑世界中只有一种编码,如果我是个以英语为母语的人的话,好像没什么问题,不巧我是个中国人,就不可能靠这种误解顺利认识整个编码世界
编码样式非常之多,但常用的也就那么几个,其中以我国为例,国内常用的编码格式有gb2312、gbk、utf-8、utf-16、iso-8859-1,其中iso-8859-1格式的广泛应用,并不是因为他多么优秀,相反,他只对西文字符编码,完全不可能表示汉子,对国内来说,这有时是个灾难,只是因为国内的软件产品大都是从国外传递过来的,他们采用的编码格式默认就会是iso-8859-1。
这里首先解释一个概念性的问题,编码方式和实现方式这两个概念的区别,编码方式是指对数字进行组织,选取一个具体的范围,然后将范围中的数字设置为码位,为每个码位编制一个文字符号,这样文字符号就和数字建立了一一对应的关系,而实现方式则是指将数字转换到程序数据的编码方案。举个例子,unicode为编码方式,而utf-8、utf-16则为实现方式。要知道unicode是用0-0x10FFFF文字范围来进行文字符号映射的,而计算机则是以二进制八位为基本单位的,如果能让程序识别,显然需要一个将编码方式映射具体字符的数字通过某种方式转化为程序数据,因此实现方式就应运而生了,utf-8、utf-16就产生了。
我们先从现在世界范围内应用最广的unicode开始,概念性的东西咱就一笔带过吧,Unicode是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案,unicode编码范围为0-0x10FFFF,可以看到如果用二进制表示,最大值0x10FFFF实际上是
10000  11111111  11111111,最多可以容纳1114112个字符,这里要澄清一个大家可能会产生模糊的概念意义,UCS和Unicode的关系,通用字符集(Universal Character Set,UCS)是由ISO制定的ISO 10646(或称ISO/IEC 10646)标准所定义的标准字符集。UCS-2用两个字节编码,UCS-4用4个字节编码。而Unicode 是基于通用字符集(Universal Character Set)的标准来发展,历史上存在两个独立的尝试创立单一字符集的组织,即国际标准化组织(ISO)和多语言软件制造商组成的统一码联盟。前者开发的 ISO/IEC 10646 项目,后者开发的统一码项目。因此最初制定了不同的标准。
1991年前后,两个项目的参与者都认识到,世界不需要两个不兼容的字符集。于是,它们开始合并双方的工作成果,并为创立一个单一编码表而协同工作。从Unicode 2.0开始,Unicode采用了与ISO 10646-1相同的字库和字码;ISO也承诺,ISO 10646将不会替超出U+10FFFF的UCS-4编码赋值,以使得两者保持一致。两个项目仍都存在,并独立地公布各自的标准。但统一码联盟和ISO/IEC JTC1/SC2都同意保持两者标准的码表兼容,并紧密地共同调整任何未来的扩展。在发布的时候,Unicode一般都会采用有关字码最常见的字型,但ISO 10646一般都尽可能采用Century字型。
Unicode在实现上还是和UCS有一些差距的,标准UCS-4根据最高位为0的最高字节分成2^7=128个group。每个group再根据次高字节分为256个平面(plane)。每个平面根据第3个字节分为256行 (row),每行有256个码位(cell)。group 0的平面0被称作BMP(Basic Multilingual Plane)。将UCS-4的BMP去掉前面的两个零字节就得到了UCS-2。每个平面有2^16=65536个码位。Unicode计划使用了17个平面(注意unicode并只是用了group0),一共有17*65536=1114112个码位。在Unicode 5.0.0版本中,已定义的码位只有238605个,分布在平面0、平面1、平面2、平面14、平面15、平面16。其中平面15和平面16上只是定义了两个各占65534个码位的专用区(Private Use Area),分别是0xF0000-0xFFFFD和0x100000-0x10FFFD。所谓专用区,就是保留给大家放自定义字符的区域,可以简写为PUA。
  平面0也有一个专用区:0xE000-0xF8FF,有6400个码位。平面0的0xD800-0xDFFF,共2048个码位,是一个被称作代理区(Surrogate)的特殊区域。代理区的目的用两个UTF-16字符表示BMP以外的字符。这个待会再解释。
       注意,在Unicode 5.0.0版本中,238605-65534*2-6400-2048=99089。余下的99089个已定义码位分布在平面0、平面1、平面2和平面14上,它们对应着Unicode目前定义的99089个字符,其中包括71226个汉字。平面0、平面1、平面2和平面14上分别定义了52080、3419、43253和337个字符。平面2的43253个字符都是汉字。平面0上定义了27973个汉字。可以很清楚的看到,平面2也有很多汉字,我们常用的汉字大都在平面0,但如果我们要用平面2的汉字时,就意味着这个码数用两个二进制的字节是装不下的,而两个字节是utf-16的代码单元的大小,显然这需要一定的转换手段,而且,可以很确定的是,这个汉字是不能用双字节表示的,所以要注意,因为java内部用的编码方式就是utf-16,双字节是常态,但如果高于两个字节就可能出现什么问题了,比如java中的char基本类型实际上大小为两个字节,当他要表示一个大于两个字节的代码点时,显然,是不可表示的,所以应该认识到,char类型并不能表示所有的字符,他只能表示平面0中的字符,对于其他平面的字符,他就无能为力了,因此,应该慎用char类型,建议用String替代char类型。
       先来讲解utf-16的实现方式,UTF-16编码以16位无符号整数为单位。我们把Unicode编码记作U。编码规则如下:
  如果U<0x10000,U的UTF-16编码就是U对应的16位无符号整数
  如果U≥0x10000,我们先计算U'=U-0x10000,然后将U'写成二进制形式:yyyy yyyy yyxx xxxx xxxx,U的UTF-16编码(二进制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx。
为什么U'可以被写成20个二进制位?Unicode的最大码位是0x10ffff,减去0x10000后,U'的最大值是0xfffff,所以肯定可以用20个二进制位表示。例如:Unicode编码0x20C30,减去0x10000后,得到0x10C30,写成二进制是:0001 0000 1100 0011 0000。用前10位依次替代模板中的y,用后10位依次替代模板中的x,就得到:1101100001000011 1101110000110000,即0xD843 0xDC30。
  按照上述规则,Unicode编码0x10000-0x10FFFF的UTF-16编码有两个WORD,第一个WORD的高6位是110110,第二个WORD的高6位是110111。可见,第一个WORD的取值范围(二进制)是11011000 00000000到11011011 11111111,即0xD800-0xDBFF。第二个WORD的取值范围(二进制)是11011100 00000000到11011111 11111111,即0xDC00-0xDFFF。
  为了将一个WORD的UTF-16编码与两个WORD的UTF-16编码区分开来,Unicode编码的设计者将0xD800-0xDFFF保留下来,并称为代理区(Surrogate):
D800-DB7F ║ High Surrogates ║ 高位替代
DB80-DBFF ║ High Private Use Surrogates ║ 高位专用替代
DC00-DFFF ║ Low Surrogates ║ 低位替代
  高位替代就是指这个范围的码位是两个WORD的UTF-16编码的第一个WORD。低位替代就是指这个范围的码位是两个WORD的UTF-16编码的第二个WORD。那么,高位专用替代是什么意思?我们来解答这个问题,顺便看看怎么由UTF-16编码推导Unicode编码。
  如果一个字符的UTF-16编码的第一个WORD在0xDB80到0xDBFF之间,那么它的Unicode编码在什么范围内?我们知道第二个WORD的取值范围是0xDC00-0xDFFF,所以这个字符的UTF-16编码范围应该是0xDB80 0xDC00到0xDBFF 0xDFFF。我们将这个范围写成二进制:
1101101110000000 11011100 00000000 - 1101101111111111 1101111111111111
  按照编码的相反步骤,取出高低WORD的后10位,并拼在一起,得到
1110 0000 0000 0000 0000 - 1111 1111 1111 1111 1111  XML即0xe0000-0xfffff,按照编码的相反步骤再加上0x10000,得到0xf0000-0x10ffff。这就是UTF-16编码的第一个WORD在0xdb80到0xdbff之间的Unicode编码范围,即平面15和平面16。因为Unicode标准将平面15和平面16都作为专用区,所以0xDB80到0xDBFF之间的保留码位被称作高位专用替代。
再次注意,非BMP的文字字符在用utf-16表示时是用四个字节表示的,也即双字,在java中,utf-16的代码单元是两个字节,也即以char为基本单位,而unicode中,每一个码位叫做一个代码点,一般在字符码数在BMP下时,一个代码点对应一个代码单元,但是如果超过BMP,这就不会是一一对应的关系了,在这里还要说一个题外话,一个代码点可能对应者多个代码单元,哪一个代码点可以表示一个字符吗?这在unicode中是可以的,但在latin-1也即iso-8859-1中这可能就存在变数了,latin-1可以看做是对传统ascii的扩展,其采用单字节编码方式,因此,其最大能表示255个字符,这对西文字符看起来足够了,但是还会有一些特殊字符或者未来可能诞生的字符,因此他们会采用多个代码点表示一个字符,这些字符可能是一个组合字符
  下面讲解utf-8,
UTF-8以字节为单位对Unicode进行编码。从Unicode到UTF-8的编码方式如下:
Unicode编码(16进制) ║ UTF-8 字节流(二进制)
000000 - 00007F ║ 0xxxxxxx
000080 - 0007FF ║ 110xxxxx 10xxxxxx
000800 - 00FFFF ║ 1110xxxx 10xxxxxx 10xxxxxx
010000 - 10FFFF ║ 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
UTF-8的特点是对不同范围的字符使用不同长度的编码。对于0x00-0x7F之间的字符,UTF-8编码与ASCII编码完全相同。UTF-8编码的最大长度是4个字节。从上表可以看出,4字节模板有21个x,即可以容纳21位二进制数字。Unicode的最大码位0x10FFFF也只有21位。
注意首字节的标志位,有几个1就代表有几个字符,标志位中的0作为标志位的结尾标志
例1:“汉”字的Unicode编码是0x6C49。0x6C49在0x0800-0xFFFF之间,使用用3字节模板了:1110xxxx 10xxxxxx 10xxxxxx。将0x6C49写成二进制是:0110 1100 0100 1001, 用这个比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。
  例2:Unicode编码0x20C30在0x010000-0x10FFFF之间,使用用4字节模板了:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx。将0x20C30写成21位二进制数字(不足21位就在前面补0):0 0010 0000 1100 0011 0000,用这个比特流依次代替模板中的x,得到:11110000 10100000 10110000 10110000,即F0 A0 B0 B0。
下面对编码unicode做一个总体的概括,现在用的最多的是utf-8,因为对于至少双字节的utf-16来说,灵活字节数可变的编码方式可以有效的减少字节数,这对于在网络带宽有限的今天,这是很有意义的,而且,计算机中的很多数据是用西文字符表示的,utf-8在表示西文时只用了一个字符,这显然会比双字节的utf-16有优势,还有一点,utf-8的编码方式有助于本身代码点的校验,可以不用借助外界的校验方式,在复杂多变的网络世界,出错是常态,采用本身的特点校验可以有效的将损失降至最低,因为一个字节的缺失只会对当前代码点有影响,可以很容易定位到出错代码点,对其他代码点没有影响,而utf-16则没有这种特性,如果出现字节缺失,出错点后的字节可能都会解码都会出现问题,与外界校验想比,utf-8可以达到细粒化的校验,而外界校验则是总体性的,具有不确定性,而且外界校验一旦出错,就直接丢弃,而utf-8则是可以继续解码的,当然utf-8也是优缺点的,仔细想想,其编码解码方式都是相当耗时的,相对于utf-8,utf-16编码解码方式则非常简单,对于以效率为主要目而且出错率很底的的单机,utf-16显然更具有优势。
下面讨论常见的BOM也即字节序问题,根据字节序的不同,UTF-16可以被实现为UTF-16LE或UTF-16BE
那么,怎么判断字节流的字节序呢?Unicode标准建议用BOM(Byte Order Mark)来区分字节序,即在传输字节流前,先传输被作为BOM的字符"零宽无中断空格"。这个字符的编码是FEFF,而反过来的FFFE(UTF-16)在Unicode中都是未定义的码位,不应该出现在实际传输中。下表是各种UTF编码的BOM:
UTF编码 ║ Byte Order Mark
UTF-8 ║ EF BB BF
UTF-16LE ║ FF FE
UTF-16BE ║ FE FF
事实上,utf-8是没有字节序问题的,因此BOM可加可不加,因此会有utf-8和utf-8 without BOM,注意这两者是等价的,没有所谓的常态、特殊态,utf-8带BOM也是有好处的,我们可以通过BOM来识别其是哪种编码方式,当然,这仅仅限定在unicode系列,utf-16是存在字节序问题的,因此其一定会有BOM
下面讲解GBK 和GB2312
下面为Copy的概念
GBK即汉字内码扩展规范,K为扩展的汉语拼音中“扩”字的声母。英文全称Chinese Internal Code Specification。GBK编码标准兼容GB2312,共收录汉字21003个、符号883个,并提供1894个造字码位,简、繁体字融于一库。GB2312码是中华人民共和国国家汉字信息交换用编码,全称《信息交换用汉字编码字符集——基本集》,1980年由国家标准总局发布。基本集共收入汉字6763个和非汉字图形字符682个,通行于中国大陆。新加坡等地也使用此编码。GBK是对GB2312-80的扩展,也就是CP936字码表 (Code Page 936)的扩展(之前CP936和GB 2312-80一模一样)。
GBK采用双字节表示,总体编码范围为8140-FEFE,首字节在81-FE 之间,尾字节在40-FE 之间,剔除 xx7F一条线。总计23940 个码位,共收入21886个汉字和图形符号,其中汉字(包括部首和构件)21003 个,图形符号883 个。P-Windows3.2和苹果OS以GB2312为基本汉字编码, Windows 95/98则以GBK为基本汉字编码。
 字符有一字节和双字节编码,00–7F范围内是一位,和ASCII保持一致,此范围内严格上说有96个字符和32个控制符号。
  之后的双字节中,前一字节是双字节的第一位。总体上说第一字节的范围是81–FE(也就是不含80和FF),第二字节的一部分领域在40–7E,其他领域在80–FE。
Copy完毕
注意,gb系列是兼容ascii的,基本上所有的编码格式都是兼容ascii,即使双字节的utf-16也是高位为0,低位为ascii的,要知道在ascii中,0是无意义的,因此,即使文字采用其他编码格式编码,采用ascii解码都是可以看到西文字符的,西文字符是不会乱码的
这里讨论一个有趣的问题,就是给你一个二进制文件,你怎么确定采用正确的解码方式解析这个文件以显示正确的字符,这实际上是有一些困难的,让我们针对几种方式进行讨论,首先是unicode系列,因为其有BOM,可以通过BOM识别utf-16和部分带BOM的utf-8,因此首先是utf-8 without BOM 和gb系列的比较,gb编码范围为8140-FEFE,首字节在81-FE 之间,尾字节在40-FE 之间,而utf-8在双字节时范围为
C080 到dfbf,实际上这个范围是在gb的编码范围的,如果文件中的所有字符经过gb编码后都是在C080 到dfbf这个范围,那么编码格式就是不可确定的,在中文环境的windows中,如果在文本文件中写入“联通”时,在保存时默认是为gbk(即ansi),当在打开时,因为这两个字经过gbk编码范围都在C080 到dfbf,而windows默认似乎是按unicode编码格式打开,在确定无BOM后,其按utf-8 without BOM处理,当有数字不在utf-8编码范围时,其就认为此文件编码格式不是utf-8,转而用本地编码格式,在中文环境下就是gbk,注意windows假定文件没有出现错误,这是不严谨的,因为“联通”在utf-8的范围,其就会按照utf-8解码,显然会是乱码,还有会出现四字节的冲突情况,在四字节上,gb两个文字总体范围会是81408140 到fefefefe,而utf-8四字节表示范围为f0808080到f7bfbfbf,这显然在gb四字节的范围内,因此也可能出现冲突情况,至于三字节,通过分析不会出现冲突情况 

 

猜你喜欢

转载自clannadyue.iteye.com/blog/2246532