字符编码与乱码

          字符编码是计算机最基本的概念,但也是经常出错的地方。不管是小菜还是老鸟,也不分是前端与分端,乱码无处不在。

一、基本概念

  1. 字符集:是指一个系统支持的所有字符的集合。字符是文字和符号的统称。常见字符集有ASCII字符集,GB2312字符集,GBK字符集,Unicode字符集等。计算机中提到的字符集,指已编号的字符有序集合。编号为下面提到的字符码。所以提到一个字符集,至少包含两层意思,一个是包含了确定的字符集合;每个字符对应确定的字符编码。
  2. 字符码:指的是字符集中,每个符号的数字编号。如GBK中的区位码,ASCII用0~127个数字表示128个字符,Unicode字符集,按照一定类别分成17面,每个字符都对应一个编号。
  3. 字符编码(字符编码方案):是将字符集中的字符码映射到字节流的一种具体的编码方案。计算机用元器件的开关,表示0和1来,要让计算机处理符号,必须将字符码,转换为二进制,也就是字节流。字符编码表达的含义有:字符码与字节流和对应关系,一般都是直接转换为二进制表示;用多少个字节表示一个字符码,固定长度,还是可变长度;每个字节特定位表示的含义;字节序,如大端序、小端序。常见字符编码方案有:ASCII编码,GBK编码,UTF-8,UTF-16等。

早期字符集、字符编码表示同一概念。GBK字符集与GBK编码都包含了同样的含义,即同时指定了字符集、字符码、字符编码。将字符和最终的字节流绑定死了。Unicode将字符集和编码方案分开了,虽然每个字符在Unicode字符集中都能找到唯一确定的字符码,但是决定最终字节流的的却是具体的字符编码。任何能够将Unicode字符映射为字节流的编码都属于Unicode编码。 Unicode编码是对UTF-8、UTF-16等的统称,并不是指一种具体的字符编码。

        4. 编码:使用指定的编码方案将字符转换为字节流。

        5. 解码:使用特定的编码方案将字节流转换为字符。

另外,在JAVA中,采用Unicode字符集,JVM运行时采用utf-16编码,即JVM中所有字符都采用utf-16编码,不会改变,class 文件采用utf-8编码。字符与字节流转换采用的字符编码并不是想像当中utf-16,而是依赖于默认字符编码,如utf-8、gbk,与操作系统环境相关。

二、乱码

从上述概念,很容易就能想到,乱码是怎么回事,同一个字符码,在不同字符集中表示不同的字符。所以造成乱码原因是使用了错误的字符编码去解码字节流造成,从而使字符在使用和显示过程中不能被下一阶段正确的识别。要解决乱码问题,首要明确所使用的字符编码方案。

乱码为可还原和不可还原两种。可还原是指产生乱码后,前一个字符集中字符码,能够被下一个字符编码识别,即用下一个字符编码解码时,字节流对应的字符码,在下一个字符集中有相应字符存在。

不可还原指,当使用特定字符编码解析字节流的时候,一旦遇到无法解析的字节流时,就会用?或者�来替代。因此,一旦你最终解析得到的文本包含这样的字符,而你又无法得到原始字节流的时候,说明原始字符码信息已丢失,尝试任何字符编码都无法从这样的字符文本中还原出正确的信息来。


三、示例

  • 示例一

一个工作当中遇到的案例,调用某电商开放平台接口时遇到的。该平台提供了一个面单打印接口,该接口依赖JVM默认字符编码,否则就会产生乱码。这也是编码容易犯的错误,靠巧合编程,存在不必要或不合理的假设。暂不予过多评价,对该接口数据传播过程,编码和解码分析如下:


Base64编码过程如下:每三个字节转换为四个字节,前两位取零。6位二进制能表示64个数字,每个数字对应Base64码表中的一个字符。64个字符为ASCII字符集中的常见字符。

反编译SDK代码如下:

  public String getHtmlContent()
  {
    String hc = new String(Base64.decodeBase64(this.htmlContent_base64.getBytes()));
    return hc;
  }
这里的htmlContent_base64就是客户端接收字符串B。  上图示例过程,有点不是很明确,在服务器端字节流B是直接传到客户端,还是经过解码为字符串后再utf到客户端。  但这是影响不是很大,Base64字符utf-8或gb2312是字节流相同。但客户端如果默认使用utf-16编码,Base64解码就会报错。

上面出现乱码的原因有:

  1. 客户端编码和解码字节流A的字符编码不一样。
  2. 字节流B编码和解码的字符编码不兼容。

解决方案

1、首先能想到的是,字符串C还原成节节流A,然后用GB2312解码:

new String(htmlContent.getBytes(),"gb2312")
但还是乱码。 因为得到字符串C后,已经丢了字节流A的一些信息,无法将字符串C还原为节节流A。 在解码过程中,如果遇到无法解析的字节流时,会用?替代,这样,原始字符码丢失。

2、替换该class文件。 得吐槽一下,作为一个规模较大的电商平台,连客户端SDK,源码都不开放!

  public String getHtmlContent() {
    String hc = null;
    try {
      hc = new String(Base64.decodeBase64(this.htmlContent_base64.getBytes("utf-8")), "gb2312");
    } catch (UnsupportedEncodingException e) {
      throw new RuntimeException(e.getMessage());
    }
    return hc;
  } 

  • 示例二

前面一个例子稍微复杂,不太容易说明问题,下面看一个简单的示例:

    String china = "中国";
    String decodeStr = new String(china.getBytes("GB2312"), "utf-8");
    System.out.println(decodeStr);
    String repairStr = new String(decodeStr.getBytes("utf-8"),"GB2312");
    System.out.println(repairStr);

decodeStr为乱码,用utf-8解码g82312的字节流。而且不可还原,china与repairStr内容不同。

    String china = "中国";
    String decodeStr = new String(china.getBytes("GB2312"), "iso-8859-1");
    System.out.println(decodeStr);
    String repairStr = new String(decodeStr.getBytes("iso-8859-1"),"GB2312");
    System.out.println(repairStr);
这种是可以还原的乱码。即一般用单字节编码方案去解码多节字编码的字节流时,是可以还原的。

四、参考

如果想进一步了解,可以参考下面两篇文章,向两作者致谢。

  1. http://www.cnblogs.com/skynet/archive/2011/05/03/2035105.html 
  2. http://my.oschina.net/zengsai/blog/32040


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值