zoukankan      html  css  js  c++  java
  • 聊聊计算机中的编码(Unicode,GBK,ASCII,utf8,utf16,ISO8859-1等)以及乱码问题的解决办法

    作为一个程序员,一个中国的程序员,想来“乱码”问题基本上都遇到过,也为之头疼过。出现乱码问题的根本原因是编码与解码使用了不同而且不兼容的“标准”,在国内一般出现在中文的编解码过程中。

    我们平时常见的编码有Unicode,GBK,ASCII,utf8,utf16,ISO8859-1等,弄清这些编码之间的关系,就不难理解“乱码”出现的原因以及解决办法。

    所谓字符集编码其实就是将字符(包括英文字符、特殊符号,控制字符,数字,汉子等)与计算机中的一个数字(二进制存储)一一对应起来,用这个数字来表示该字符,存储该字符的时候就存储这个数字。比如a对应数字97。因此,理解编码很简单,所有的编码都是字符与数字的一种对应关系

    ASCII编码:

    计算机最早出现在美国,因此老美搞编码只需要对26个英文字符大小写以及常用的字符对应数字就可以了,这种对应就是ASCII(American Standard Code for Information Interchange,美国信息互换标准代码)码。标准ASCII 码使用7 位二进制数来表示所有的大写和小写字母,数字0 到9、标点符号, 以及在美式英语中使用的特殊控制字符。这样可以表示27=128个字符。标准ASCII码的最高位恒为0,没有使用。

    用java输出英文字符的ASCII码如下:

    public class TestCode {
        public static void main(String[] args) throws Exception {
            int code='a';
            System.out.println(code);
        }
    }

    输出:

    97

    iso8859-1:

    随着计算机的推广,世界各地都开始使用计算机。各国不同语言对字符编码提出了新的需求,原ASCII的128个字符已经显得严重不足。那怎么办呢,ASCII码不是只用了一个字节中的7位吗,还剩余1位呢?那就赶紧用上吧!于是人们把编码扩展到了8位,即256个字符的编码,这就是ISO8859-1。这种扩展保持了与ASCII的兼容性,即最高位为0的ISO8859-1编码等同于ASCII码。

    用java随便输出一个iso8859-1字符如下:

    public class TestCode {
        public static void main(String[] args) throws Exception {
            char code=0xA7;
            System.out.println(code);
        }
    }

    输出:

    §

    GBK码:

    等到计算机进入中国,人们又头疼了。常用汉字就有6000多个,像ASCII那样用一个字节来编码撑爆了也不够啊。但是这难不倒智慧的中国人们,我们直接定下标准:小于127的字符与原意义相同(保持与ASCII的兼容性),但是两个大于127的字符连在一起时,就表示一个汉字。这样我们就凑出来了7000多个简体汉字的编码了。此外,这些编码还对ASCII码中已有的标点、数字、字母都用两字节重新编码,这就是通常说的“全角”字符。这种编码就是GB2312

    但是中国的汉字实在太多了,GB2312还是不够用,一些不常用的汉字还是显示不出来啊。于是我们不得不继续挖掘GB2312的潜能,干脆只要求第一个字节大于127而不管后一个字节的大小了。这种扩展之后的编码方案称为GBK

    下图为中文“你好”二字的GB2312编码输出(GBK输出相同):

    public class TestCode {
        public static void main(String[] args) throws Exception {
            String s = "你好";
            byte[] code = s.getBytes("gb2312");
            for (byte b : code) {
                System.out.print(Integer.toHexString(b & 0xFF) + " ");
            }
        }
    }

    输出:

    c4 e3 ba c3

    Unicode码:

    中国造出了GBK编码,其他国家呢,他们也要显示自己的文字啊。于是各个国家都搞出了一套自己的编码标准,结果相互之间谁也不懂谁的编码,互不兼容。这样不行啊,于是乎ISO(国际标谁化组织)不得不站出来说话了:“你们都不要各自搞编码了,我给你们搞一套统一的!”。于是ISO搞了一个全球统一的字符集编码方案,叫UCS(Universal Character Set),俗称Unicode

    Unicode标准最早是1991年发布了,目前实际应用的版本是UCS-2,即使用两个字节编码字符。这样理论上一共可以编码216=65536个字符,基本能够满足各种语言的需求。

    UTF8、UTF16码:

    其实Unicode码已经完美解决编码国际化问题了,那utf8和utf16又是神马东东,用来解决什么问题呢?

    前面已经说过,编码只是字符与数字的一种对应关系,这完全是一个数学问题,跟计算机和存储以及网络都没有半毛钱关系。Unicode码就是这样一种对应关系,它并没有涉及到如何存储以及传输的问题。看下面一个例子:

    假如某个字符的Unicode编码为0xabcd,也就是两个字节。那存储的时候是哪个字节在前哪个在后呢?网络传输的时候又是先传输哪个字节呢?计算机从文件中读取到0xabcd又是怎么知道这是两个ASCII码还是一个Unicode码呢?

    因此需要一种统一的存储和传输格式来标示Unicode码。这种统一的实现方式称为Unicode转换格式(Unicode Transformation Format,简称为UTF)。编码utf8和utf16就是因此而产生的。

    其中utf16与16位的Unicode码完全对应。在Mac和普通PC上,对于字节顺序的理解是不一致的。比如MAC是从低字节开始读取的,因此前文的0xabcd如果按照所见的顺序存储,则会被MAC认为是0xcdab,而windows会从高字节开始读取,得到的是0xabcd,这样根据Unicode码表对应出来的字符就不一致了。

    因此,utf16使用了大端序(Big-Endian,简写为UTF-16 BE)、小端序(Little-Endian,简写为UTF-16 LE)以及BOM(byte order mark)的概念。如果在windows上用记事本写上一些中文字符并以Unicode码格式保存,然后使用十六进制查看器打开即可以看到文件的前两个字节为0xfffe(0xfffe在Unicode码中不对应字符),用来标记使用小端序存储(windows平台默认使用小端序),

    下图为中文“你好”二字在windows7上的十六进制数据。

    imageimage

    如果用java程序输出“你好”二字的utf16码,则如下:

    public class TestCode {
        public static void main(String[] args) throws Exception {
            String s = "你好";
            byte[] code = s.getBytes("utf16");
            for (byte b : code) {
                System.out.print(Integer.toHexString(b & 0xFF) + " ");
            }
        }
    }

    输出:

    fe ff 4f 60 59 7d

    可以看到java默认输出的是大端序的utf16编码(BOM为0xfeff)。

    由于Unicode统一采用16位二进制编码字符,试想一篇英文文章如果用UTF16来存储的话整整比用ASCII存储多占用一倍的存储空间(英文字符的Unicode码高字节是0),这样白白的浪费让人于心不忍啊。于是utf8诞生了。utf8是一种变长编码,根据不同的Unicode码值采用不同的存储长度。那么问题又来了,既然是变长的系统怎么知道几个字节表示一个字符编码呢?对于这类问题计算机中通用的处理方式就是使用标志位,就像ip段的划分一样。具体如下:

    0xxxxxxx,如果是这样的01串,也就是以0开头后面是啥就不用管了XX代表任意bit.就表示把一个字节做为一个单元.就跟ASCII完全一样.

    110xxxxx 10xxxxxx.如果是这样的格式,则把两个字节当一个单元

    1110xxxx 10xxxxxx 10xxxxxx 如果是这种格式则是三个字节当一个单元.

    用java输出“你好”的utf8编码如下:

    public class TestCode {
        public static void main(String[] args) throws Exception {
            String s = "你好";
            byte[] code = s.getBytes("utf8");
            for (byte b : code) {
                System.out.print(Integer.toHexString(b & 0xFF) + " ");
            }
        }
    }

    输出:

    e4 bd a0 e5 a5 bd

    我们可以跟上面对应一下,“你”字的第一个字节0xe4的高四位二进制是1110,因此这是一个三字节编码,系统识别时就一次读取三个字节再组合成Unicode码数字,然后就可以对应到字符“你”了。字符“好”类似。

    Unicode码的发展

    Unicode码采用16位编码世界字符其实还是有点捉襟见肘的。因此从 Unicode 3.1 版本开始,设立了16个辅助平面(相当于Unicode码又扩充了4位),使 Unicode 的可使用空间由六万多字增至约一百万字。用白话说就是增加了几个区段,比如原始版本的Unicode码的范围是0x0000 ~ 0xffff,第一辅助平面的范围是0x10000~0x1FFFD,第二辅助平面的范围是0x20000 ~ 0x2FFFD,……

    最新版的Unicode码规范提出了UCS-4,即使用4字节做Unicode编码。类似前面的utf16,对于UCS-4的Unicode码,可以采用utf32来存储,同样需要定义大小端序和BOM信息。

    URLEncode

    URL编解码是WEB开发中常用的编解码方法,这种编码不同于上面介绍的几种编码。上文中介绍的编码都是将一个字符对应到一个数字上,而URL编码则是字符替换,将一些非ASCII字符和一些容易引起问题的字符替换为其编码字符,解码时原样替换回来,从而解决url在网络传输中的乱码问题。

    看下面一个例子:

    public class TestUrlCode {
        public static void main(String[] args) throws Exception {
            String url="http://www.baidu.com?username=你好";
            String encodeStr=java.net.URLEncoder.encode(url, "utf8");
            System.out.println(encodeStr);
        }
    }

    输出:

    http%3A%2F%2Fwww.baidu.com%3Fusername%3D%E4%BD%A0%E5%A5%BD

    我们将原url与编码后的url做一个对比(这里为了让原字符与编码字符对照起来加了一些空格):

    http:   / /  www.baidu.com?  username=   你        好
    http%3A%2F%2Fwww.baidu.com%3Fusername%3D%E4%BD%A0%E5%A5%BD

    对比发现,编码后http、www.baidu.com、username这一些并没有改变,“:”被替换为“%3A”, “/”被替换为“%2F”, “?”被替换为“%3F”,“=”被替换为“%3D”, "你好”被替换为“%E4%BD%A0%E5%A5%BD”,这些%后面的十六进制字符都是哪里来的呢,其实就是原字符的utf8码值。前面我们已经看过“你好”的utf8码为“e4 bd a0 e5 a5 bd”,这些十六进制转换为字符串形式然后在前面加上% 就是URL编码了。因此解码就是将这些字符串去掉%然后用utf8码译出来 。

    常见乱码问题:

    前文提到,文件保存、网络传输时,所保存和传输的都是字符对应的码值,因此查看文件时必须将这些码值反过来对应到相应的字符(解码),才能形成我们人能够看懂的字符串形式。如果解码时选取的解码方式与编码方式不一致呢,这就是乱码问题的根本原因了。看下面几个例子:

    例子1:

    在中文版的windows系统(本文中使用win7 64位 简体中文旗舰版)桌面上新建txt文件,写上“联通”二字,保存,关闭。然后再双击打开,看到了什么?哇!乱码!

    imageimage

    我们看看windows都做了什么。我们写的“联通”俩字没问题,保存,也就是将这俩字对应出几个数字(码值)保存起来,也没问题。等等,windows是选用哪种编码进行保存的呢?用搜索引擎查一下,原来默认选用ANSI编码,对应在中文版windows系统中就是GBK,我们用十六进制查看器验证一下:

    imageimage

    果然是GBK。然后我们将这几个十六进制数字的二进制写出来:

    c1    1100 0001
    aa    1010 1010
    cd    1100 1101
    a8    1010 1000

    看第一个字节,以110开头,第二个字节,以10开头,第三个字节,以110开头,第四个字节,以10开头。这不正好符合utf8双字节编码格式吗?因此我们再次双击打开的时候记事本就错误地认为这是utf8编码的文件,这当然的就形成乱码了。

    下面使用notepad++打开,同样乱码。然后选择 格式--以ANSI格式编码  瞬间看到亲切的“联通”二字了!

    imageimage

    例子2:

    做java开发的童鞋经常遇到原Project导入Eclipse乱码问题,如下图(其实这几个乱码的原字符是“你好”二字)

    image

    这个就比较简单了,因为原项目采用GBK编码保存的,新导入项目的Eclipse配置的是utf8编码,用utf8来解码读取gbk编码,乱码是必须滴。解决办法就是将两者保持一致即可。

    例子 3:

    有时候编码信息是在文件开始的地方声明的,比如xml文件和html文件,如下:

    百度首页的源码精简后摘出一部分源码如下:

    <html><head><meta http-equiv="content-type" content="text/html;charset=utf8”ead><body></body></html>

    常用的xml文件开头标示如下:

    <?xml version='1.0' encoding='utf-8'?>

    这样的头部标示可以明确地看到文件的编码信息,然而有时候却会引发另一个问题(主要针对开发者):

    比如我们写一个html文件:

    <html><head><meta charset="utf-8"><title>test</title></head>
    <body>
    aaa你好bbb
    </body>
    </html>

    然后在保存这个文件的时候我们不小心选择了GBK编码保存。这样问题就来了,浏览器在读取这个html文件的时候会按照文件中声明的编码utf8的规则读取,这样中文又成乱码了。(因此各位coder要注意文件保存时的编码,遇到类似的问题也要知道原因和解决办法)

    image

    例子4:

    网络流的乱码问题,看下面java小程序socket通信的例子

    服务器端

    public class TestSocket {
        public static void main(String[] args) throws Exception {
             java.net.ServerSocket ss=new java.net.ServerSocket(7777);
             java.net.Socket socket=ss.accept();
             byte[] buffer=new byte[1024];
             int len=socket.getInputStream().read(buffer);
             String outStr=new String(buffer,0,len,"utf8");
             System.out.println(outStr);
             socket.close();
             ss.close();
        }
    }

    客户端

    public class TestSocketClient {
        public static void main(String[] args) throws  Throwable {
            java.net.Socket client=new java.net.Socket("localhost",7777);
            String input="你好";
            client.getOutputStream().write(input.getBytes("gbk"));
            client.close();
        }
    }

    先启动服务器端,再启动客户端,最后可以看到服务器端输出

    image

    这里故意将客户端发送的编码(GBK)与服务器端接收的解码(utf8)设的不一致,出现乱码是必然的。这个错误也是显而易见的。然后并不是所有的通信乱码都是这么明显。比如java中不明确指定编码而使用默认编码,比如使用Reader和Writer对象,这时候编码信息是隐藏的,就不是那么容易发现了。

    如果不确定网络中传输的编码,我们可以用wireshark抓个包看看就知道了。由于wireshark抓本地回环包比较麻烦,所以这里我们做实验的时候把客户端程序中的localhost改为百度的ip,把数据包发给百度好了。对应端口也改为80. 首先我们用nslookup命令查看百度域名对应的ip,然后随便选一个(不同地区可能得到的ip不一样,这里选择第一个180.149.132.47来当小白鼠吧)

    image

    public class TestSocketClient {
        public static void main(String[] args) throws  Throwable {
            java.net.Socket client=new java.net.Socket("180.149.132.47",80);
            String input="你好";
            client.getOutputStream().write(input.getBytes("gbk"));
            client.close();
        }
    }

    抓到的数据包如下图,其中10号数据包就是我们程序发出的数据,数据包前面是各种协议的头信息,最后四个字节(c4 e3 ba c3)才是传输的内容。这4个字节正好是“你好”二字的GBK编码。

    image

  • 相关阅读:
    使用 Dockerfile 定制镜像
    UVA 10298 Power Strings 字符串的幂(KMP,最小循环节)
    UVA 11090 Going in Cycle!! 环平均权值(bellman-ford,spfa,二分)
    LeetCode Best Time to Buy and Sell Stock 买卖股票的最佳时机 (DP)
    LeetCode Number of Islands 岛的数量(DFS,BFS)
    LeetCode Triangle 三角形(最短路)
    LeetCode Swap Nodes in Pairs 交换结点对(单链表)
    LeetCode Find Minimum in Rotated Sorted Array 旋转序列找最小值(二分查找)
    HDU 5312 Sequence (规律题)
    LeetCode Letter Combinations of a Phone Number 电话号码组合
  • 原文地址:https://www.cnblogs.com/zffenger/p/5448235.html
Copyright © 2011-2022 走看看