Hadoop乱码引发的思考

Reducer阶段的乱码

在Hadoop的Mapper阶段,我读入了一个有中文的txt文档。处理后,Reducer输出后乱码

原因是txt文档是GBK编码格式,而我用下面方式读取后,在Reducer阶段使用UTF-8编码,就会乱码。

@Override
protected void map(LongWritable key, Text value, Context context)
			throws IOException, InterruptedException {
	// 1 获取一行
	String line = value.toString();
    ...
}

正确的方式是:

String line = new String(value.getBytes(),0,value.getLength(),"GBK");

同样,如果Mapper阶段涉及到流也需要处理

 BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(path),"GBK"));

遇到这个问题,本质是自己对编码的知识认知有欠缺。

如何编码

下表是我们常用的编码类型,以及对中英文编码需要的字节数。

编码类型英文(字节数)中文(字节数)
ISO-8859-111 (乱码)
GBK-231212 (查码表)
GBK12 (查码表)
UTF-1622
UTF-813
  • ISO-8859-1编码

    字符串“I am君山”用ISO-8859-1编码,编码结果如图3-5所示。

    在这里插入图片描述

    可以看出,7个char字符经过ISO-8859-1编码转变成7个byte数组,ISO-8859-1是单字节编码,中文“君山”被转化成值是3f的byte。3f也就是“?”字符,所以经常会出现中文变成“?”,很可能就是错误地使用了ISO-8859-1这个编码导致的

  • GBK2312

    字符串“I am君山”用GB2312编码,编码结果如图3-6所示。
    在这里插入图片描述

    GB2312对应的Charset是sun.nio.cs.ext. EUC_CN,而对应的CharsetDecoder编码类是sun.nio.cs.ext. DoubleByte,GB2312字符集有一个char到byte的码表,不同的字符编码就查这个码表找到与每个字符的对应的字节,然后拼装成byte数组。查表的规则如下:

    c2b[c2bIndex[char >> 8] + (char & 0xff)]
    如果查到的码位值大于oxff则是双字节,否则是单字节。双字节高8位作为第一个字节,低8位作为第二个字节,如下代码所示:

    if (bb > 0xff) {  // DoubleByte
         if (dl - dp < 2)
           return CoderResult.OVERFLOW;
         da[dp++] = (byte) (bb >> 8);
        da[dp++] = (byte) bb;
    } else {          // SingleByte
        if (dl - dp < 1)
          return CoderResult.OVERFLOW;
        da[dp++] = (byte) bb;
    }
    

    可以看出,前5个字符经过编码后仍然是5个字节,而汉字被编码成双字节,GB2312只支持6763个汉字,所以并不是所有汉字都能够用GB2312编码。

  • GBK编码

    GBK与GB2312编码结果是一样的,不同的是,它们的码表长度不一样,GBK包含的汉字字符更多。所以只要是经过GB2312编码的汉字都可以用GBK进行解码,反过来则不然。

  • UTF-16编码

    字符串“I am君山”用UTF-16编码,编码结果如图3-8所示。
    在这里插入图片描述

    用UTF-16编码将char数组放大了一倍,单字节范围内的字符在高位补0变成两个字节,中文字符也变成两个字节。从UTF-16编码规则来看,仅仅将字符的高位和低位进行拆分变成两个字节,特点是编码效率非常高,规则很简单。由于不同处理器对2字节处理方式不同,Big-endian(高位字节在前,低位字节在后)或Little-endian(低位字节在前,高位字节在后)编码,在对字符串进行编码时需要指明到底是Big-endian还是Little-endian,所以前面有两个字节用来保存BYTE_ORDER_MARK值,UTF-16是用定长16位(2字节)来表示的UCS-2或Unicode转换格式,通过代理来访问BMP之外的字符编码。

  • UTF-8编码

    字符串“I am君山”用UTF-8编码,编码结果如图3-9所示。
    在这里插入图片描述

    UTF-16虽然编码效率很高,但是对单字节范围内的字符也放大了一倍,这无形也浪费了存储空间,另外UTF-16采用顺序编码,不能对单个字符的编码值进行校验,如果中间的一个字符码值损坏,后面的所有码值都将受影响。而UTF-8不存在这些问题,UTF-8对单字节范围内字符仍然用一个字节表示,对汉字采用三个字节表示。UTF-8编码与GBK和GB2312不同,不用查码表,所以在编码效率上UTF-8的效率会更好,所以在存储中文字符时UTF-8编码比较理想。

为何UTF-8需要三个字节?

UTF规定:

  • 如果一个符号只占一个字节,那么这个8位字节的第一位就为0。
  • 如果为两个字节,那么规定第一个字节的前两位都为1,然后第一个字节的第三位为0,第二个字节的前两位为10,
  • 然后如果是三个字节的话,那么第一个字节的前三位为111,第四位为0,剩余的两个字节的前两位都为10。

按照这样的算法去思考一个中文字符的UTF-8是怎么表示的:一个中文字符需要两个字节来表示,两个字节一共是16位,那么UTF-8下,两个字节是不够的,因为两个字节下,第一个字节已经占据了三位:110,然后剩余的一个字节占据了两位:10,现在就只剩下8位,与Unicode下的两个字节,16位去表示任意一个字符是相悖的,也就是Unicode下的16位减去UTF-8下的8位=8位,刚好差了一个字节的空间,所以就使用三个字节去表示非ANSI字符:三个字节下,一共是24位,第一个字节头四位是:1110,后两个字节的前两位都是:10,那么24位-8位=16位,刚好两个字节去表示Unicode下的任意一个非ANSI字符。这也就是为什么UTF-8需要使用三个字节去表示一个非ANSI字符的原因了!

测试代码
/**
 * CreateBy zxmao on  2020/11/12 0012 10:21
 */

import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;

/**
 * Copyright (C) zhongda
 *
 * @author zx
 * @date 2020/11/12 0012 10:21
 * @description:
 */

public class T {
    public static void main(String[] args) throws Exception {
        //获取系统默认编码
        System.out.println("系统默认编码:" + System.getProperty("file.encoding")); //查询结果GBK
        //系统默认字符编码
        System.out.println("系统默认字符编码:" + Charset.defaultCharset()); //查询结果GBK
        //操作系统用户使用的语言
        System.out.println("系统默认语言:" + System.getProperty("user.language")); //查询结果zh
        System.out.println("======================");
        //HexString 转为16进制 BinaryString转为二进制
        //计算机中数用补码表示:整数的补码与原码一致。负数的补码是反码加1 负数第一位用1表示,取反时,符号位不动。
        String re1 = Integer.toBinaryString(-64);
        System.out.println(re1);
        String a = new String("山");
        System.out.println("============默认编码==========");
        byte[]ab = a.getBytes();
        for (int i =0;i<ab.length;++i){
            System.out.println("原始byte数组:"+ab[i]);
        }
        for(int i=0;i<ab.length;++i){
            System.out.println(Integer.toBinaryString(ab[i]).substring(24));
        }
        System.out.println("======================");
        for(int i=0;i<ab.length;++i){
            System.out.println(Integer.toHexString(ab[i]).substring(6));
        }
        System.out.println("=========转为GBK 乱码=============");
        try {
            String b = new String(a.getBytes(),"GBK");
            System.out.println("re:"+b);
            byte[] gbs = b.getBytes();
            for(int i=0;i<gbs.length;++i){
                System.out.println(Integer.toBinaryString(gbs[i]).substring(24));
            }
            for(int i=0;i<gbs.length;++i){
                System.out.println(Integer.toHexString(gbs[i]).substring(6));
            }

        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        System.out.println("=============定义bytes,创建GBK编码==================");
        byte[]mgbs = new byte[]{(byte)0xc9,(byte)0xbd};
        for (int i =0;i<mgbs.length;++i){
            //原始数组打印的是补码:10110111 (-55)
            //减1-> 10110110 取反->11001001 (0xc9) 符号位不动
            //即
            System.out.println("原始byte数组:"+mgbs[i]);
        }
        for(int i=0;i<mgbs.length;++i){
            System.out.println(Integer.toBinaryString(mgbs[i]).substring(24));
            System.out.println(Integer.toBinaryString(mgbs[i]));
        }
        for(int i=0;i<mgbs.length;++i){
            System.out.println(Integer.toHexString(mgbs[i]).substring(6));
        }

        String gbks = new String(mgbs,"GBK");
        System.out.println(gbks);

        System.out.println("==========汉字的编码范围 UTF-8 ============");
        /**
         * 如果一个符号只占一个字节,那么这个8位字节的第一位就为0。
         * 单字符范围:0-0000000 ~ 0-1111111  即 0-127
         * 如果为两个字节,那么规定第一个字节的前两位都为1,然后第一个字节的第三位为0,第二个字节的前两位为10,
         * 110-00000 10-000000 ~ 110-11111 10-111111 即192 128 ~ 223 191 即第一个字节范围:192-223 第二个字节范围 128-191
         * 然后如果是三个字节的话,那么第一个字节的前三位为111,第四位为0,剩余的两个字节的前两位都为10
         * 1110-0000 10-000000 10-000000 到1110-1111 10-111111 10-111111
         *
         * 即:224  128  128 ~  239  191 191
         * 三个字节范围:224-239 第二个128-191 第三个 128-191
         *
         */

        System.out.println("===========根据上面的规则造几个汉字=============");
        /**
         * https://www.cr173.com/html/11686_all.html 常见汉字U8编码
         * %E4%BD%A0 %E5%A5%BD (你好)
         * %E6%88%91
         */
        byte[]crbs = new byte[]{(byte) 0xe4,(byte)0xbd,(byte)0xa0, (byte) 0xe5, (byte) 0xa5, (byte) 0xbd};
        String creStr = new String(crbs,"UTF-8");
        System.out.println(creStr);



    }

}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值