字符编码格式

来源:互联网 发布:vuze mac 编辑:程序博客网 时间:2024/06/04 18:36

1. ASCII码


    我们知道,在计算机内部,所有的信息最终都表示为一个由多个二进制位组成的串。每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出2^8=256种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从0000000到11111111。

    上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为ASCII码,一直沿用至今。ASCII码一共规定了128个字符的编码,比如空格“SPACE”是32(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的1位统一规定为0。


2、非ASCII编码


    英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。比如,在法语中,字母上方有注音符号,它就无法用ASCII码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。

    但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0—127表示的符号是一样的,不一样的只是128—255的这一段。

    至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式GB2312。


3.汉字编码。


    GB2312(1980年)一共收录了7445个字符,包括6763个汉字和682个其它符号。汉字区的内码范围高字节从B0-F7,低字节从A1-FE,占用的码位是72*94=6768。其中有5个空位是D7FA-D7FE。

    GB2312支持的汉字太少。1995年的汉字扩展规范GBK1.0收录了21886个符号,它分为汉字区和图形符号区。汉字区包括21003个字符。

    2000年的GB18030是取代GBK1.0的正式国家标准。该标准收录了27484个汉字,同时还收录了藏文、蒙文、维吾尔文等主要的少数民族文字。

    现在的PC平台必须支持GB18030,对嵌入式产品暂不作要求。所以手机、MP3一般只支持GB2312。

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

    有的中文Windows的缺省内码还是GBK,可以通过GB18030升级包升级到GB18030。不过GB18030相对GBK增加的字符,普通人是很难用到的,通常我们还是用GBK指代中文Windows内码。

    这里还有一些细节:

    GB2312的原文还是区位码,从区位码到内码,需要在高字节和低字节上分别加上A0。

    在DBCS中,GB内码的存储格式始终是big endian,即高位在前。

    GB2312的两个字节的最高位都是1。但符合这个条件的码位只有128*128=16384个。所以GBK和GB18030的低字节最高位都可能不是1。不过这不影响DBCS字符流的解析:在读取DBCS字符流时,只要遇到高位为1的字节,就可以将下两个字节作为一个双字节编码,而不用管低字节的高位是什么。


    BIG5字符集用于表示中文繁体字,常用于台湾地区。


4.Unicode


    如上所述,世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样。

    可以想象,如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是Unicode,就像它的名字都表示的,这是一种所有符号的编码。

    Unicode当然是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号在Unicode中都对应一个值,这个值称为代码点(code point).每个符号的编码都不一样,比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+4E25表示汉字“严”。


    Unicode也是一种字符编码方法,不过它是由国际组织设计,可以容纳全世界所有语言文字的编码方案。Unicode的学名是"Universal Multiple-Octet Coded Character Set",简称为UCS。UCS可以看作是"Unicode Character Set"的缩写。 UCS有两种格式:UCS-2和UCS-4。顾名思义,UCS-2就是用两个字节编码,UCS-4就是用4个字节(实际上只用了31位,最高位必须为0)编码。

  UCS-2有2^16=65536个码位,UCS-4有2^31=2147483648个码位。

  UCS-4根据最高位为0的最高字节分成2^7=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之外。


5.Unicode的问题

    需要注意的是,Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。比如,汉字“严”的unicode是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。

    这里就有两个严重的问题,第一个问题是,如何才能区别unicode和ascii?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。

    它们造成的结果是:

        1)出现了unicode的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示unicode。比如常见的几种UTF(UCS Transformation Format):UTF-32UTF-16、UTF-8。

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

6. UTF-16

    UTF-16由RFC2781规定,它使用两个字节来表示一个代码点。

    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为U+FEFF这个字符。

    其实BOM是个小聪明的想法。由于UCS-2没有定义U+FFFE, 因此只要出现 FF FE 或者 FE FF 这样的字节序列,就可以认为它是U+FEFF, 并且可以判断出是Big Endian还是Little Endian。

    举个例子。“ABC”这三个字符用各种方式编码后的结果如下:
    UTF-16BE               00 41 00 42 00 43
    UTF-16LE               41 00 42 00 43 00
    UTF-16(Big Endian)     FE FF 00 41 00 42 00 43
    UTF-16(Little Endian)  FF FE 41 00 42 00 43 00
    UTF-16(No BOM)         00 41 00 42 00 43

    Windows平台下默认的Unicode编码为Little Endian的UTF-16(即上述的 FF FE 41 00 42 00 43 00)。 你可以打开记事本,写上ABC,然后保存,再用二进制编辑器看看它的编码结果。
   

    另外,UTF-16还能表示一部分的UCS-4代码点——U+10000~U+10FFFF。 表示算法比较复杂,简单说明如下: 1. 从代码点U中减去0×10000,得到U’。这样U+10000~U+10FFFF就变成了 0×00000~0xFFFFF。 2. 用20位二进制数表示U’。 U’=yyyyyyyyyyxxxxxxxxxx 3. 将前10位和后10位用W1和W2表示,W1=110110yyyyyyyyyy,W2=110111xxxxxxxxxx,则 W1 = D800~DBFF,W2 = DC00~DFFF。

    例如,U+12345表示为 D8 08 DF 45(UTF-16BE),或者08 D8 45 DF(UTF-16LE)。

    但是由于这种算法的存在,造成UCS-2中的 U+D800~U+DFFF 变成了无定义的字符。

7. UTF-32

    UTF-32用四个字节表示代码点,这样就可以完全表示UCS-4的所有代码点,而无需像UTF-16那样使用复杂的算法。 与UTF-16类似,UTF-32也包括UTF-32、UTF-32BE、UTF-32LE三种编码,UTF-32也同样需要BOM字符。 仅用’ABC’举例:
    UTF-32BE               00 00 00 41 00 00 00 42 00 00 00 43
    UTF-32LE               41 00 00 00 42 00 00 00 43 00 00 00
    UTF-32(Big Endian)     00 00 FE FF 00 00 00 41 00 00 00 42 00 00 00 43
    UTF-32(Little Endian)  FF FE 00 00 41 00 00 00 42 00 00 00 43 00 00 00
    UTF-32(No BOM)         00 00 00 41 00 00 00 42 00 00 00 43

8.UTF-8

    鉴于UTF-16的复杂算法和UTF-32的大存储量,UTF-8是在互联网上使用最广的一种unicode的实现方式。

    UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。

    UTF-8的编码规则很简单,只有二条:

        1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。

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

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

    Unicode符号范围       | UTF-8编码方式
    (十六进制)            | (二进制)
    -------------------- +---------------------------------------------
    0000 0000-0000 007F  | 0xxxxxxx
    0000 0080-0000 07FF  | 110xxxxx 10xxxxxx
    0000 0800-0000 FFFF  | 1110xxxx 10xxxxxx 10xxxxxx
    0001 0000-0010 FFFF  | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

下面,以汉字“严”为例,演示如何实现UTF-8编码。

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


9. Little endian和Big endian

    这两个古怪的名称来自英国作家斯威夫特的《格列佛游记》。在该书中,小人国里爆发了内战,战争起因是人们争论,吃鸡蛋时究竟是从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开。为了这件事情,前后爆发了六次战争,一个皇帝送了命,另一个皇帝丢了王位。

    因此,第一个字节在前,就是”大头方式“(Big endian),第二个字节在前就是”小头方式“(Little endian)。

    那么很自然的,就会出现一个问题:计算机怎么知道某一个文件到底采用哪一种方式编码?

    很简单,就是使用前面提到的BOM。Unicode规范中定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格“(ZERO WIDTH NO-BREAK SPACE),用FEFF表示。这正好是两个字节,而且FF比FE大1。

    如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。


10.总结

    使用ascii/GB码存储字符时,英文字符占一个字节,中文字符占两个字节。

    使用Unciode存储字符时,UTF16占2个字节;UTF-32占用四字节。

    Utf8存储字符时,英文字符占一个字节,中文占三个字节,泛欧语系、斯拉夫语字母占2 Bytes


11.示例代码

UTF8-->UCS-2

int Utf8ToUCS2(const unsigned char *in, unsigned char *out){    unsigned char *p = (unsigned char *)in;    int resultsize = 0;    unsigned char *tmp = out;    while(*p)    {        if (*p >= 0x00 && *p <= 0x7f) //one byte: 0xxxxxxx        {            *tmp = *p;            tmp++;            *tmp = '\0';            tmp++;            resultsize += 2;        }        else if ((*p & (0xff << 5))== 0xc0) //two byte: 110xxxxx 10xxxxxx        {            unsigned char t1 = 0;            unsigned char t2 = 0;            t1 = *p & (0xff >> 3);            p++;            t2 = *p & (0xff >> 2);#if 0//Big Endian*tmp = t1 >> 2;            tmp++;*tmp = t2 | ((t1 & (0xff >> 6)) << 6);            tmp++;#else//Little Endian            *tmp = t2 | ((t1 & (0xff >> 6)) << 6);            tmp++;            *tmp = t1 >> 2;            tmp++;#endif            resultsize += 2;        }        else if ((*p & (0xff << 4))== 0xe0) //three byte: 1110xxxx 10xxxxxx 10xxxxxx        {            unsigned char t1 = 0;            unsigned char t2 = 0;            unsigned char t3 = 0;            t1 = *p & (0xff >> 3);            p++;            t2 = *p & (0xff >> 2);            p++;            t3 = *p & (0xff >> 2);#if 0    //Big Endian            *tmp = (t1 << 4) | (t2 >> 2);            tmp++;            *tmp = ((t2 & (0xff >> 6)) << 6) | t3;            tmp++;#else    //Little Endian            *tmp = ((t2 & (0xff >> 6)) << 6) | t3;            tmp++;            *tmp = (t1 << 4) | (t2 >> 2);            tmp++;#endif            resultsize += 2;        }        p++;    }    *tmp = '\0';    tmp++;    *tmp = '\0';    resultsize += 2;    return resultsize;}
UCS-2-->UTF8
 int UCS2ToUtf8(const unsigned char *in, unsigned char *out){    int i = 0;    int outsize = 0;    unsigned char *tmp = out;    unsigned char *p = (unsigned short*)in;while(0 != *p || 0 != *(p + 1)){if(*p > 0x0000 && *p <= 0x007f){#if 0//Big Endian*tmp++ = *(p + 1);#else//Little Endian*tmp++ = *p;#endifoutsize += 1;}else if(*p >= 0x0080 && *p <= 0x07ff){#if 0//Big Endian*tmp++ = 0xc0 | (*p & 0x07) << 2 | *(p + 1) >> 6;*tmp++ = 0x80 | *(p + 1) & 0x3f;#else//Little Endian*tmp++ = 0xc0 | (*(p + 1) & 0x07) << 2 | *p >> 6;*tmp++ = 0x80 | *p & 0x3f;#endifoutsize += 2;}else if(*p >= 0x0800 && *p <= 0xffff){#if 0//Big Endian*tmp++ = 0xe0 | *p >> 4;*tmp++ = 0x80 | (*p & 0x0f) << 2 | *(p + 1) >> 6;*tmp++ = 0x80 | *(p + 1) & 0x3f;#else//Little Endian*tmp++ = 0xe0 | *(p + 1) >> 4;*tmp++ = 0x80 | (*(p + 1) & 0x0f) << 2 | *p >> 6;*tmp++ = 0x80 | *p & 0x3f;#endifoutsie += 3;}p += 2;}    *tmp = '\0';    return outsize;}

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 租一个房子小孩一进房就哭怎么办 墙缝里有蝙蝠窝怎么办 小蝙蝠在墙缝里怎么办 小孩一进屋就哭怎么办 屋门对着厕所门怎么办 入室门对厨房门怎么办 厕所正对入户门怎么办 小区楼交错冲路怎么办 床的位置在五鬼上怎么办 被甩了很痛苦怎么办 和对象想分手了怎么办 对象想跟你啪啪怎么办 相亲对象好像不太想理我怎么办 想跟对象分手了怎么办 异地恋分手后该怎么办 面膜敷了一晚上怎么办 梦见被刺猬咬了怎么办 梦见死人叫我名字答应怎么办 香瓜苗叶子长斑怎么办 奶油打出来很稀怎么办 寄的水果压坏了怎么办 吃了一个烂水果怎么办 孕期吃了烂水果怎么办 邮快递水果坏了怎么办 快递寄水果坏了怎么办 闲鱼买家拒收水果怎么办 洗澡桶里有很多老鼠屎怎么办? 塑料和金属断了怎么办 孕妇吃了沙拉酱怎么办 孕妇淀粉吃多了怎么办 怀孕初期吃了杏怎么办 如果睡觉吃梨了怎么办 怀孕6个月有点贫血怎么办 怀孕八个多月有点贫血怎么办 生完小孩身体虚怎么办 孩子咳嗽厉害怎么办吃什么药 新生儿三天不拉大便怎么办 胃吃的变大了怎么办 小孩长高长的慢怎么办 小孩长高长得慢怎么办 小孩吃东西不吸收营养怎么办