Python编解码小结(一)—— Unicode的来龙去脉

来源:互联网 发布:java静态代理模式 编辑:程序博客网 时间:2024/06/08 06:53

在正文开始前,请首先阅读并理解以下关键字:
  • 字符集(Character Set):顾名思义,就是字符的集合。如ASCII字符集,定义了128个字符,而gb2312定义了7445个字符。计算机中字符集的严格定义来说指的是已编号的字符的有序集合(不一定连续)。
  • 字符码(Code Point):指的就是字符集中每个字符的数字编号。例如ASCII字符集用0-127这连续的128个数字分别表示128个字符;GBK字符集使用区位码的方式为每个字符编号,首先定义一个94X94的矩阵,行称为“区”,列称为“位”,然后将所有国标汉字放入矩阵当中,这样每个汉字就可以用唯一的“区位”码来标识了。例如“中”字被放到54区第48位,因此字符码就是5448。Unicode字符集将所有字符按照使用上的频繁度划分为17个层面,每个层面上有216=65536个字符码空间,其中第0个层面称为Basic Multilingual Plane,基本涵盖了当今世界用到的所有字符。其他的层面要么是用来表示一些远古时期的文字,要么是留作扩展。我们平常用到的Unicode字符,一般都是位于BMP层面上的,Unicode总共拥有的字符码,也即是Unicode的字符空间总共有17*65536=1114112。
  • 编码的过程是将字符转换成字节流。
  • 解码的过程是将字节流解析为字符。
  • 字符编码:将字符集中的字符码映射为字节流的一种具体实现方案。例如ASCII字符编码规定使用单字节中低位的7个比特去编码所有的字符。例如‘A’的编号是65,用单字节表示就是0×41,因此写入存储设备的时候就是b’01000001’。GBK编码则是将区位码(GBK的字符码)中的区码和位码的分别加上0xA0(160)的偏移(之所以要加上这样的偏移,主要是为了和ASCII码兼容),例如刚刚提到的“中”字,区位码是5448,十六进制是0×3630,区码和位码分别加上0xA0的偏移之后就得到0xD6D0,这就是“中”字的GBK编码结果。
  • 代码页(Code Page)一种字符编码具体形式。早期字符相对少,因此通常会使用类似表格的形式将字符直接映射为字节流,然后通过查表的方式来实现字符的编解码。现代操作系统沿用了这种方式。例如Windows使用936代码页即"CP936"、Mac系统使用EUC-CN代码页实现GBK字符集的编码,名字虽然不一样,但对于同一汉字的编码肯定是一样的。(windows系统中ANSI其实也是Windows code pages,这个模式根据当前的locale选定具体编码,如果系统locale是简体中文则采用GBK编码,繁体中文为BIG5编码,日文则是JIS编码)
  • 多字节字符集MBCS(Multi-Byte Character Set),有时候也在Windows系统中也叫做DBCS(Double-Byte Character Set)。MBCS只是一种编码类型,并不是某一种特定的编码,Windows里根据你设定的区域不同,MBCS指代不同的编码,而Linux里无法使用MBCS作为编码。在Windows系统中常规情况是看不到MBCS这几个字符,因为微软为了更加洋气的名称“ANSI”,记事本里的编码ANSI就是MBCS。
  • 大小端的说法源自《格列佛游记》。我们知道,鸡蛋通常一端大一端小,小人国的人们对于剥蛋壳时应从哪一端开始剥起有着不一样的看法。同样,计算机界对于传输多字节字(由多个字节来共同表示一个数据类型)时,是先传高位字节(大端)还是先传低位字节(小端)也有着不一样的看法,这就是计算机里头大小端模式的由来了。无论是写文件还是网络传输,实际上都是往流设备进行写操作的过程,而且这个写操作是从流的低地址向高地址开始写(这很符合人的习惯),对于多字节字来说,如果先写入高位字节,则称作大端模式。反之则称作小端模式。也就是说,大端模式下,字节序和流设备的地址顺序是相反的,而小端模式则是相同的。一般网络协议都采用大端模式进行传输。如果一个文本文件的头两个字节是FEFF,就表示该文件采用大头方式;如果头两个字节是FFFE,就表示该文件采用小头方式。
注:以上术语说明引用《关于字符编码,你所需要知道的知识》,本文稍作增减注释。

一、历史:

上世纪80年代,大部分电脑使用8bit地址存储。8bit空间(即byte)可以存储0到255的数值。ASCII码选择单字节(0-127)数值作为其标准编码区间(即前127个数字来做字符映射), 而剩下的128-255数值变成各团体、组织自定义分配。注:ASCII标准本身就规定了字符和字符编码方式,ASCII既是字符集又是编码方案。

随着字符数量需求不断增加,Unicode被提上来了讨论日程。其初始目标是“to have Unicode contain the alphabets for every single human language”然而,即使用全16bit编码空间,仍然不能满足上述目标,最后采取更宽的编码空间,即 0-1,114,111 (0x10ffff)。Unicode/UCS(Unicode Character Set)标准只是一个字符集标准,但是它并没有规定字符的存储和传输方式。Unicode是一种字符集而不是具体的编码。具体的符号对应表,可以查询unicode.org。由于字符需要在物理内存中存储,我们还需要一种对unicode进行编码的方案。任何能够将Unicode字符映射为字节流的编码都属于Unicode编码。

一个完整的Unicode字符称作code point(字符码,又称Unicode码),例如某Unicode字符U+12ca的真实编号为0x12ca (十进制4810)。虽然每个字符在Unicode字符集中都能找到唯一确定的编号,但是决定最终字节流的却是具体的字符编码。另外每个字符在实际显示屏或者印刷中的显示称为glyph(标志符号,即字符的给定字体的物理表示形式)。通常Unicode string则被定义为一组连续的code points


二、Unicode的编码

Unicode是一种字符集而不是具体的编码,它有以下几类编码方式:

1) 最初Unicode标准使用2个字节表示一个字符,编码方案是UCS-2/UTF-16【注:UCS(Unicode Character Set),仅仅是字符对应码位的一张表,字符具体如何传输和储存则是由UTF(UCS Transformation Format)来负责】 UCS-2和UTF-16对于BMP层面的字符均是使用2个字节来表示,并且编码得到的结果完全一致。不同之处在于,UCS-2最初设计的时候只考虑到BMP字符,因此使用固定2个字节长度,也就是说,他无法表示Unicode其他层面上的字符,而UTF-16为了解除这个限制,支持Unicode全字符集的编解码,采用了变长编码,最少使用2个字节,如果要编码BMP以外的字符,则需要4个字节结。

2) 使用4个字节表示一个字符的编码方案UCS-4/UTF-32

3)目前应用最广泛的编码方案是UTF-8。根据规定,UTF8用1-4个字节来表示UNICODE,对于绝对值在7F以上的字符,UTF-8用两个及以上的字节表示,其中第一个字节的高位连续的1的个数,表示“用UTF8编码表示的字符的字节数”,此字符其他的字节必须是以10开头。如某UTF8编码的字符,第一个字节为110X XXXX,那么说明表示此字符的UTF-8编码要用到两个字节,其第二个字节必须是10YY YYYY。否则就是非法的UTF8编码。

UTF-8编码与Unicode字符码转换方法如下:


UTF-8编码规则总结:

  Ascii Chars:  00-7F                          // 1 Bytes = 0xxxxxxx
  Multi Bytes:  C0-DF + 80-BF                  // 2 Bytes = 110xxxxx 10xxxxxx
            E0-EF + 80-BF + 80-BF          // 3 Bytes = 1110xxxx 10xxxxxx 10xxxxxx
            F0-F7 + 80-BF + 80-BF + 80-BF  // 4 Bytes = 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

4)中国国标编码及其他

早期的计算机使用7位的ASCII编码,但为了处理汉字,又设计出用于简体中文的GB2312和用于繁体中文的big5。
GB2312:(1980年)一共收录了7445个字符,包括6763个汉字和682个其它符号。汉字区的内码范围高字节从B0-F7,低字节从A1-FE,占用的码位是72*94=6768。其中有5个空位是D7FA-D7FE。另外在这套编码里,数学符号、罗马希腊的字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。
CJK:Unicode为了节省码位,将中日韩三国语言中的文字统一编码为CJK。GB13000.1就是ISO/IEC 10646-1的中文版,相当于Unicode 1.1。
GB13000: 完全等同于ISO 10646-1/Unicode 2.1, 今后也将随ISO 10646/Unicode的标准更改而同步更改.
GBK: 由于GB2312支持的汉字太少,1995年的汉字扩展规范GBK1.0收录了21886个符号,它分为汉字区和图形符号区,基本上采用了原来GB2312-80所有的汉字及码位,并涵盖了原Unicode中所有的汉字20902,总共收录了883个符号, 21003个汉字及提供了1894个造字码位。容纳GB2312字符集范围以外的Unicode 2.1的统一汉字部分, 并且增加了部分unicode中没有的字符。由于GBK同时也涵盖了Unicode所有CJK汉字,也可以认为是Unicode的一种编码方式。
GB18030(即GB18030-2000):2000年的GB18030取代GBK1.0的正式国家标准。该标准收录了27484个汉字,同时还收录了藏文、蒙文、维吾尔文等主要的少数民族文字。该编码作为Unicode 3.0的GBK扩展版本, 覆盖了所有unicode编码, 地位等同于UTF-8, UTF-16, 也是一种unicode编码形式。 GB18030向下兼容GB2312/GBK.  现在的PC平台必须支持GB18030,对嵌入式产品暂不作要求,所以手机、MP3一般只支持GB2312。从汉字字汇上说,GB18030在GB13000.1的20902个汉字的基础上增加了CJK扩展A的6582个汉字(Unicode码0x3400-0x4db5),一共收录了27484个汉字。 GB18030的编码采用单字节、双字节和4字节方案。其中单字节、双字节和GBK是完全兼容的。4字节编码的码位就是收录了CJK扩展A的6582个汉字。例如:UCS的0x3400在GB18030中的编码应该是8139EF30,UCS的0x3401在GB18030中的编码应该是8139EF31。

GBK编码规则:
  Ascii Chars:               00-7F
  Chinses Chars:          81-FE + 40-7E
                     81-FE + 80-FE
GB18030-2000编码规则:
  Ascii Chars:               00-7F
  Chinses Chars:          81-FE + 40-7E
                     81-FE + 80-FE
  Chinses Chars(4 Bytes):    81-FE + 30-39 + 81-FE + 30-39
微软提供了GB18030的升级包,但这个升级包只是提供了一套支持CJK扩展A的6582个汉字的新字体:新宋体-18030,并不改变内码。Windows 的内码仍然是GBK。从ASCII、GB2312、GBK到GB18030,这些编码方法是向下兼容的,即同一个字符在这些方案中总是有相同的编码,后面的标准支持更多的字符。在这些编码中,英文和中文可以统一地处理。区分中文编码的方法是高字节的最高位不为0。按照程序员的称呼,GB2312、GBK到GB18030都属于双字节字符集 (DBCS)。
ANSI:算是一种压缩编码,当遇到标准的ASCII字符时,采用单字节表示,当遇到非标准的ASCII字符(如中文)时,采用双字节表示。在简体中文系统下,ANSI 编码代表 GB2312 编码,在日文操作系统下,ANSI 编码代表 JIS 编码。 不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。 当然对于ANSI编码而言,0x00~0x7F之间的字符,依旧是1个字节代表1个字符。


最后,本节附上微软notepad记事本的一个彩蛋作为结束。

打开记事本,在里边输入“联通”两个字,记住是只输入“联通”这两个字。然后关闭记事本,然后再打开。会有什么情况?

文件内容变成了乱码!

记事本默认保存的是 ANSI 编码,文件保存并关闭后,当在windows系统中重新打开文件时,操作系统不知道文件用什么编码保存的,使用了“猜测”的方式选择解析文件内容编码系统一般来说,识别是通过判断字符范围实现的,只有格式猜对了才能将文本正确显示。如果文件有BOM头比较好办。但是如果没有头文件字节时,首先会调用IsTextUTF8函数确定这个文本文件的编码方式。对于ANSI编码的GBXXXX标准的中文字符来说,直到IsTextUTF8函数返回FALSE,Notepad.exe才会以CP_ACP的方式调用MultiByteToWideChar,直到这时中文字符才会被正确解析,也就是说,如果IsTextUTF8返回了TRUE,那么ANSI的中文字符就会被当成UTF8编码转换成Unicode再显示出来。很不幸的是,有段编码区间,即文档中一切字符都在 C0≤AA≤DF 80≤BB≤BF 这个范围时,IsTextUTF8函数无法确认文档的格式,notepad也就无法正确显示了。

对于“联通”这个case,其ANSI(GBK)编码是:

联(0xC1AA) 通(0xADA8)
c1 1100 0001
aa 1010 1010
cd 1100 1101
a8 1010 1000
第一二个字节、第三四个字节的起始部分的都是"110"和"10",正好与UTF8规则里的两字节模板是一致的,于是再次打开记事本时,记事本就误认为这是一个UTF8编码的文件,把第一个字节的110和第二个字节的10去掉,得到"00001 101010",再把各位对齐,从而得到"0000 0000 0110 1010",这是UNICODE的006A,即小写的字母"j",而之后的两字节用UTF8解码之后是0368,这个字符什么也不是。类似的还有字如:写(0xD0B4),即1101 0000 1011 0100,去(0xC8A5)1010 0101 1100 1000

IsTextUTF8函数如下:

BOOL IsTextUTF8( LPSTR lpBuffer, int iBufSize )
{
  /*
  0zzzzzzz;
  110yyyyy, 10zzzzzz
  1110xxxx, 10yyyyyy, 10zzzzzz
  11110www, 10xxxxxx, 10yyyyyy, 10zzzzzz
  */
  int iLeftBytes = 0;
  BOOL bUtf8 = FALSE;
  if( iBufSize <= 0 )
    return FALSE;
  for( int i=0;i<iBufSize;i++)
  {
    char c = lpBuffer[i];
    if( c < 0 )        //至少有一个字节最高位被置位
      bUtf8 = TRUE;
    if( iLeftBytes == 0 )//之前尚无UTF-8编码的字符的前导字节,或者是下个字符。
    {
      if( c >= 0 )  //0000 0000 - 0100 0000
        continue;
      do//统计出高位连续的的个数
      {
        c <<= 1;
        iLeftBytes++;
      }while( c < 0 );
      iLeftBytes--;    //表示本字符的剩余字节的个数;
      if( iLeftBytes == 0 )//最高位是10,不能作为UTF-8编码的字符的前导字节。
        return FALSE;
    }
    else           
    {
      c &= 0xC0;       //1100 0000
      if( c != (char)0x80 )//1000 0000 对于合法的UTF-8编码,非前导字节的前两位必须为。
        return 0;
      else
        iLeftBytes--;
    }
  }
  if( iLeftBytes )
    return FALSE;
  return bUtf8;
}

0 0