走进编码

       作为一个程序员,了解计算机编码是一门必修课。下面是一些有关编码的总结与体会。从此你会对ANSI UNICODE UTF-8 GB2312 GBK DBCS UCS有一个全新的认识。

一段小代码引起的腥风血雨--------“编码”到底是何方神圣?

(1)ANSI

       接触过编程的同学,相信没有人不知道ASCII编码(American Standard Code For Infomation Interchange,美国信息互换标准代码)。

       开始计算机只在美国用,八位的字节一共可以组合出256(2的8次方)种不同的状态。他们把其中的编号从0开始的32种状态分别规定了特殊的用途,一但终端、打印机遇上约定好的这些字节被传过来时,就要做一些约定的动作。遇上00x10, 终端就换行,遇上0x07, 终端就向人们嘟嘟叫,例好遇上0x1b, 打印机就打印反白的字,或者终端就用彩色显示字母。他们看到这样很好,于是就把这些0x20以下的字节状态称为"控制码"。他们又把所有的空格、标点符号、数字、大小写字母分别用连续的字节状态表示,一直编到了第127号,这样计算机就可以用不同字节来存储英语的文字了。

       后来,就像建造巴比伦塔一样,世界各地的都开始使用计算机,但是很多国家用的不是英文,他们的字母里有许多是ASCII里没有的,为了可以在计算机保存他们的文字,他们决定采用127号之后的空位来表示这些新的字母、符号,还加入了很多画表格时需要用下到的横线、竖线、交叉等形状,一直把序号编到了最后一个状态255。从128到255这一页的字符集被称"扩展字符集"。

 

   (2)UNICODE

       因为当时各个国家都搞出一套自己的编码标准,结果互相之间谁也不懂谁的编码,谁也不支持别人的编码。正在这时,出现了——一个叫 ISO (国际标谁化组织)的国际组织决定着手解决这个问题。他们采用的方法很简单:废了所有的地区性编码方案,重新搞一个包括了地球上所有文化、所有字母和符号的编码!

       ISO 就直接规定必须用两个字节,也就是16位来统一表示所有的字符,对于ascii里的那些“半角”字符,UNICODE 包持其原编码不变,只是将其长度由原来的8位扩展为16位,而其他文化和语言的字符则全部重新统一编码。由于"半角"英文符号只需要用到低8位,所以其高8位永远是0,因此这种大气的方案在保存英文文本时会多浪费一倍的空间。

       Unicode当然是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+4E25表示汉字“严”。具体的符号对应表,可以查询unicode.org,或者专门的汉字对应表。

       Unicode规范中定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格“
(ZERO WIDTH NO-BREAK SPACE),用FEFF表示。这正好是两个字节,而且FF比FE大1。如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。需要注意的是,Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。

      因为UTF-16 区分大小端,严格讲:unicode==UTF16BE。即unicode默认是大端存储的。

String bytetohex3=bytesToHex(srcStr.getBytes("UNICODE"));
String bytetohex4=bytesToHex(srcStr.getBytes("UTF-16LE"));
String bytetohex5=bytesToHex(srcStr.getBytes("UTF-16BE"));
System.out.println("unicode byte:"+bytetohex3);
System.out.println("unicode byte:"+bytetohex4);
System.out.println("unicode byte:"+bytetohex5);

测试结果:

unicode byte:FEFF75286237
unicode byte:28753762
unicode byte:75286237

 

     

 

(3)UTF-8

       UNICODE 来到时,一起到来的还有计算机网络的兴起,UNICODE 如何在网络上传输也是一个必须考虑的问题,于是面向传输的众多 UTF(UCS Transfer Format)标准出现了,顾名思义,UTF8就是每次8个位传输数据,而UTF16就是每次16个位,只不过为了传输时的可靠性,从UNICODE到UTF时并不是直接的对应,而是要过一些算法和规则来转换。互联网的普及,强烈要求出现一种统一的编码方式。UTF-8就是在互联网上使用最广的一种unicode的实现方式。其他实现方式还包括UTF-16和UTF-32,不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8是Unicode的实现方式之一。

       在网络里传递信息时有一个很重要的问题,就是对于数据高低位的解读方式,一些计算机是采用低位先发送的方法,例如我们PC机采用的 INTEL 架构,而另一些是采用高位先发送的方式,在网络中交换数据时,为了核对双方对于高低位的认识是否是一致的,采用了一种很简便的方法,就是在文本流的开始时向对方发送一个标志符——如果之后的文本是高位在位,那就发送"FEFF",反之,则发送"FFFE"。  

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

 

(4)GB2312

       等中国人们得到计算机时,已经没有可以利用的字节状态来表示汉字,况且有6000多个常用汉字需要保存呢。但是这难不倒智慧的中国人民,我们不客气地把那些127号之后的奇异符号们直接取消掉, 规定:一个小于127的字符的意义与原来相同,但两
个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了。在这些编码里,我们还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。于是就把这种汉字方案叫做 "GB2312"。GB2312 是对 ASCII 的中文扩展。

 

(5)GBK

       但是中国的汉字太多了,我们很快就就发现有许多人的人名没有办法在这里打出来,特别是某些很会麻烦别人的国家领导人。于是我们不得不继续把 GB2312 没有用到的码位找出来老实不客气地用上结果扩展之后的编码方案被称为 GBK 标准,GBK 包括了 GB2312 的所有内容,同时又增加了近20000个新的汉字(包括繁体字)和符号。

       这里只指出,虽然都是用多个字节表示一个符号,但是GB类的汉字编码与后文的Unicode和UTF-8是毫无关系的。

 

(6)DBCS:

       后来少数民族也要用电脑了,于是我们再扩展,又加了几千个新的少数民族的字,GBK 扩成了 GB18030。从此之后,中华民族的文化就可以在计算机时代中传承了。中国的程序员们看到这一系列汉字编码的标准是好的,于是通称他们叫做 "DBCS"(Double Byte Charecter Set 双字节字符集)。

 

(7)UCS:

      "Universal Multiple-Octet Coded Character Set",简称 UCS, 俗称 "UNICODE"。  

 

(8)EXAMPLE

打开”记事本“程序Notepad.exe,新建一个文本文件,内容就是一个”严“字,依次采用ANSI,Unicode,Unicode big endian 和 UTF-8编码方式保存。然后,用文本编辑软件UltraEdit中的”十六进制功能“,观察该文件的内部编码方式。

 

(9) UNICODE和UTF8的转换规则

       因为英文字符也全部使用双字节,存储成本和流量会大大地增加,所以Unicode编码大多数情况并没有被原始地使用,而是被转换编码成UTF8。下表就是其转换公式:

  • 1)ANSI:文件的编码就是两个字节“D1 CF”,这正是“严”的GB2312编码,这也暗示GB2312是采用大头方式存储的。
  • 2)Unicode:编码是四个字节“FF FE 25 4E”,其中“FF FE”表明是小头方式存储,真正的编码是4E25。
  • 3)Unicode big endian:编码是四个字节“FE FF 4E 25”,其中“FE FF”表明是大头方式存储。
  • 4)UTF-8:编码是六个字节“EF BB BF E4 B8 A5”,前三个字节“EF BB BF”表示这是UTF-8编码,后三个“E4B8A5”就是“严”的具体编码,它的存储顺序与编码顺序是一致的。

走进编码_第1张图片

第一种:Unicode从 0x0000 到 0x007F 范围的,是不是有点熟悉?对,其实就是标准ASCII码里面的内容,所以直接去掉前面那个字节 0x00,使用其第二个字节(与ASCII码相同)作为其编码,即为单字节UTF8。

第二种:Unicode从 0x0080 到 0x07FF 范围的,转换成双字节UTF8。

第三种:Unicode从 0x8000 到 0xFFFF 范围的,转换成三字节UTF8,一般中文都是在这个范围里。

第四种:超过双字节的Unicode目前还没有广泛支持,仅见emoji表情在此范围。

例如“博”字的Unicode编码是\u535a。0x535A在0x0800~0xFFFF之间,所以用3字节模板 1110yyyy 10yyyyxx 10xxxxxx。将535A写成二进制是:0101 0011 0101 1010,高八位分别代替y,低八位分别代替x,得到 11100101 10001101 10011010,也就是 0xE58D9A ,这就是博字的UTF8编码。

 

(10)不同编码之间如何互相转换

     10.1用ISO-8859-1中转UTF-8数据

     10.2用GBK中转UTF-8数据

直接上代码:

import java.io.UnsupportedEncodingException;

public class EncodingTest {

    public static void Transfer_with_GBK(String srcStr){
        /*
         * ISO-8859-1可以作为中间编码,不会导致数据的丢失
         * GBK如果汉字数量为偶数,不会丢失数据,如果汉字数量为奇数,必定会丢失数据
         * */
        System.out.println("原始字符串:"+srcStr);
        try {
            //得到utf8 byte
            String bytetohex=bytesToHex(srcStr.getBytes("UTF-8"));
            System.out.println("utf byte:"+bytetohex);

            //得到gbk
            String utf2GBKStr=new String(srcStr.getBytes("UTF-8"),"GBK");
            System.out.println("UTF-8->GBK:"+utf2GBKStr);

            //得到gbk byte
            String bytetohex2=bytesToHex(utf2GBKStr.getBytes("GBK"));
            System.out.println("gbk byte:"+bytetohex2);

            //将gbk还原成utf8
            String utf2GBK2UTFStr=new String(utf2GBKStr.getBytes("GBK"),"UTF-8");
            System.out.println("UTF-8->GBK->UTF-8:"+utf2GBK2UTFStr);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    public static void Transfer_with_ISO(String srcStr){
        /*
         * 这是一个unicode字符串,与字符集无关
         * 其实在java虚拟机中,所有字符串都是以unicode字符串进行存储的
         * */

        System.out.println("原始字符串:"+srcStr);
        try {
            //得到utf8 byte
            String bytetohex=bytesToHex(srcStr.getBytes("UTF-8"));
            System.out.println("utf byte:"+bytetohex);

            //得到gbk
            String utf2GBKStr=new String(srcStr.getBytes("UTF-8"),"ISO-8859-1");
            System.out.println("UTF-8->ISO-8859-1:"+utf2GBKStr);

            //得到gbk byte
            String bytetohex2=bytesToHex(utf2GBKStr.getBytes("ISO-8859-1"));
            System.out.println("ISO-8859-1 byte:"+bytetohex2);

            //将gbk还原成utf8
            String utf2GBK2UTFStr=new String(utf2GBKStr.getBytes("ISO-8859-1"),"UTF-8");
            System.out.println("UTF-8->ISO-8859-1->UTF-8:"+utf2GBK2UTFStr);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    final protected static char[] hexArray="0123456789ABCDEF".toCharArray();

    public static String bytesToHex(byte[] bytes){
        char[] hexChars = new char[bytes.length * 2];
        for ( int j = 0; j < bytes.length; j++ ) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = hexArray[v >>> 4];
            hexChars[j * 2 + 1] = hexArray[v & 0x0F];
        }
        return new String(hexChars);
    }

    public static void main(String[] args){
        String str="用户";
        String str2="用户名";
        System.out.println("GBK偶数个汉字没问题:");
        Transfer_with_GBK(str);
        System.out.print("\n");
        System.out.println("GBK奇数个汉字有问题:");
        Transfer_with_GBK(str2);
        System.out.print("\n");
        System.out.println("ISO偶数个汉字没问题:");
        Transfer_with_ISO(str);
        System.out.print("\n");
        System.out.println("ISO奇数个汉字没问题:");
        Transfer_with_ISO(str2);
    }
}

 

运行结果:

GBK偶数个汉字没问题:
原始字符串:用户
utf byte:E794A8E688B7
UTF-8->GBK:鐢ㄦ埛
gbk byte:E794A8E688B7
UTF-8->GBK->UTF-8:用户

GBK奇数个汉字有问题:
原始字符串:用户名
utf byte:E794A8E688B7E5908D
UTF-8->GBK:鐢ㄦ埛鍚?
gbk byte:E794A8E688B7E5903F
UTF-8->GBK->UTF-8:用户??

ISO偶数个汉字没问题:
原始字符串:用户
utf byte:E794A8E688B7
UTF-8->ISO-8859-1:??¨??·
ISO-8859-1 byte:E794A8E688B7
UTF-8->ISO-8859-1->UTF-8:用户

ISO奇数个汉字没问题:
原始字符串:用户名
utf byte:E794A8E688B7E5908D
UTF-8->ISO-8859-1:??¨??·???
ISO-8859-1 byte:E794A8E688B7E5908D
UTF-8->ISO-8859-1->UTF-8:用户名

 

结论:

ISO-8859-1 可以作为中间编码,不会导致数据丢失;

GBK 如果汉字数量为偶数,不会丢失数据,如果汉字数量为奇数,必定会丢失数据。

 

分析:为什么GBK会出错,而ISO-8859-1 没问题

(1)奇数个汉字的utf-8字节流转成GBK,再转回来,前面数据都正常,最后一个字节,变成了0x3f,即字符“?”

通过unicode和utf8的转换规则我们知道,“用户名”的UTF-8的字节流为:

[E7 94 A8] [E6 88 B7] [E5 90 8D],每个汉字用三字节来表示。

然后将这些数据用GBK(双字节)编码,得到如下字节流:

[E7 94] [A8 E6] [88 B7] [E5 90] [8D ?],每个汉字用两个字节来表示。字节不够怎么办呢?然后GBK编码把0x8d当作一个未知字符进行处理,用一个半角Ascii字符的 "?"代替,于是就有了[E7 94] [A8 E6] [88 B7] [E5 90] 3F。所以原始数据就被破坏了。

(2)因为 ISO-8859-1 是单字节编码,因此它的分组方案是:[E7] [94] [A8] [E6] [88] [B7] [E5] [90] [8D],所以这种编码不会改变原来数据。

最早的编码是iso8859-1,和ascii编码相似。ISO-8859-1属于单字节编码,最多能表示的字符范围是0-255,应用于英文系列。比如,字母a的编码为0x61=97。很明显,iso8859-1编码表示的字符范围很窄,无法表示中文字符。但是,由于是单字节编码,和计算机最基础的表示单位一致,所以很多时候,仍旧使用iso8859-1编码来表示。而且在很多协议上,默认使用该编码。比如,虽然"中文"两个字不存在iso8859-1编码,以gb2312编码为例,应该是"d6d0 cec4"两个字符,使用iso8859-1编码的时候则将它拆开为4个字节来表示:"d6 d0 ce c4"(事实上,在进行存储的时候,也是以字节为单位处理的)

 

unicode是安全的,因为他是java使用的标准类型,跨平台无差异。

 

 

 

 

 

 

 

 

 

      

 

 

 

 

 

 

 

 

你可能感兴趣的:(JAVA)