MapReducer中文编码

当MapReduce处理包含中文的输入文件时,输出文件可能出现乱码。问题源于编码转换错误。解决方法包括:1)确保输入文件为UTF-8格式;2)在map阶段转换编码格式;3)继承并重写TextOutputFormat类以指定输出编码。通过实验验证了三种方法的有效性,第三种方法能直接输出GBK编码的文件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

如果输入文件有中文,输出文件可能会出现乱码。乱码问题的话一般都是编解码错误。
本文的最后有一篇参考文档,那篇文档已经解决了问题了,但是,可是自己是小白,那篇文档没做解释的话,有的我理解不了,所以就把那篇文章解释了一遍。基础好的可以直接看那篇文章。

编码的问题

首先看一下这段代码

String s = "中国";
Text t1 = new Text();
Text t2 = new Text();

t1.set(s.getBytes());
t2.set(s);


############  s   #####################
System.out.println(new String(s.toString()));
//结果是:中国

test01.printByte(s.getBytes());
//结果是:-42   -48 -71 -6

############   t1  ##################
System.out.println(new String(t1.toString()));
//结果是:?й?

test01.printByte(t1.getBytes());  // 打印数组的方法
//结果是:-42   -48 -71 -6

############   t2  #################
System.out.println(new String(t2.toString()));
//结果是:中国

test01.printByte(t2.getBytes());  // 打印数组的方法
//结果是:-28   -72 -83 -27 -101    -67     

说明一下 Text不是简单的封装String 这句话。
如果你看源码的话,你可以看到,Text类里核心是一个byte数组,而String类的核心是一个char数组。
java的默认编码格式为Unicode,Text默认且惟一的编码格式为UTF-8。

用Text.set( byte[] )赋值时,是把String的char[]数组按照Unicode编码格式找到 ‘中国’ ,然后转换成byte[] ,再赋值给t1的核心byte[] ,
而使用Text.set( string )赋值时,把String的char[]数组按照Unicode编码格式找到 ‘中国’ ,然后查找utf-8编码表,依次查出 ‘中国’ 的byte,然后赋给t2的核心byte[]。所以说 t1中保存的Unicode(也就是最原始的编码格式) 编码的字节。t2保存的是经过转换的,utf-8编码格式的字节。

然后就是Text.toString() 方法了,Text会默认把自己核心的byte[]数组用utf-8解码出一个String,纵使byte[]不是utf-8编码的。这就解释了为什么t1.toString() 打印出来的是乱码,因为t1的toString方法把 -42 -48 -71 -6 按utf-8解码,然后就是乱码了,t2.toString()打印出来是 ‘中国’的原因是 其核心byte[] 就是由utf-8编码的,在用 utf-8 解码,自然不会出错。

我们出现乱码的原因跟 t1 乱码的原因一样,在Task中,我们读入文件时,是按字节读入的,然后用Text.set( byte[] )方法赋值给map函数中第二个参数value。而输出代码是在TextOutputFormat类的writeObject方法,其源码如下:

...
## TextOutputFormat规定死了,就是要用utf-8解码
private static final String utf8 = "UTF-8";
...
 private void writeObject(Object o) throws IOException {
      if (o instanceof Text) {
        Text to = (Text) o;
        out.write(to.getBytes(), 0, to.getLength());
      } else {
        out.write(o.toString().getBytes(utf8));
      }
    }

可以看出来除了Text类之外,其余的都是编码成utf-8才输出的,而Text的toString方法也是输出utf-8的字符串的。所以说,TextOutputFormat类的输出都是utf-8编码的。

故此,如果想解决中文乱码问题,可以有以下三种方法。
1. 输入文件就是utf-8编码的。这样输出文件也是utf-8格式的。
2. 在map中,因为此时value的核心byte[]存储的都是别的编码格式的字节,所以要把byte[]取出来,转换成utf-8,这样输出的时候就能输出正确的中文,但是这个中文是以utf-8格式编码的。
3. 继承TextOutputFormat,重写writeObject方法,使之输出按照规定的编码格式输出。

实验

上面只是从理论上说明了一下乱码问题,现在实际实验一下,先准备两个不同编码格式的文件,我准备的是utf-8的和GBK的,你也可以准备更多。

主程序如下:

## map和reducer什么都没干,就是接收输入值在输出。
    public static class MapImpl extends
            Mapper<LongWritable, Text, NullWritable, Text> {
        @Override
        protected void map(LongWritable key, Text value, Context context)
                throws IOException, InterruptedException {
            context.write(NullWritable.get(), value);
        }
    }

    public static void main(String[] args) {
        try {

            Configuration conf = new Configuration();
            Job job = Job.getInstance(conf);

            job.setJarByClass(mr01.class);

            job.setMapperClass(MapImpl.class);

            job.setOutputKeyClass(NullWritable.class);
            job.setOutputValueClass(Text.class);

            job.setInputFormatClass(TextInputFormat.class);
           job.setOutputFormatClass(TextOutputFormat.class);

            Path inpath = new Path(args[0]);
            Path outpath = new Path(args[1]);

            FileInputFormat.addInputPath(job, inpath);
            FileOutputFormat.setOutputPath(job, outpath);

            System.exit(job.waitForCompletion(true) ? 0 : 1);

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

把两种不同编码格式的文件做参数传入,你可以发现,utf-8文件的输出文件是正常的,没有乱码,GBK文件的输出文件就会产生乱码。
好,接下来我们要讨论解决方案了,为了简单,我们还是以GBK文件做实验,即把传入的GBK文件输出不会产生乱码。

第一种方法,把文件转换成utf-8格式的

这个方法的意义不大,都是大数据的数据文件了,自然挺大的,转换起来听麻烦的,所以不做解释了,就说明这种方法可以。

第二种方法,在map中转换编码格式

因为map中的byte[]储存的还是以GBK编码的字节,我们只要把这个byte[]数组取出来,然后转成utf-8格式的,在传给value的byte[]就行。

参考文章中,专门写了一个方法,来吧byte[]转成utf-8格式的,具体代码如下:

public static Text transformTextToUTF8(Text text, String encoding) {
String value = null;
try {
value = new String(text.getBytes(), 0, text.getLength(), encoding);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return new Text(value);
}

如果要使用这个方法,就要把 实验 那个代码中的重写map方法改成如下这样就行:

        @Override
        protected void map(LongWritable key, Text value, Context context)
                throws IOException, InterruptedException {

            Text t = transformTextToUTF8(value, "gbk");

            context.write(NullWritable.get(), t);
        }

如果不想写方法,可以直接在map中转换,代码如下:

        @Override
        protected void map(LongWritable key, Text value, Context context)
                throws IOException, InterruptedException {
            String s = new String(value.getBytes(),"gbk");

            Text t = new Text();
            t.set(s);
            context.write(NullWritable.get(), t);
        }

这种方法的重点就在
String s = new String(value.getBytes(),”gbk”);
这句话,原理大致是: 取出value的核心byte[] ,然后以gbk格式进行解码,并构造成一个新的字符串,到这为止,这个字符串s就不是乱码的了,然后用Text.set(String) 方法,把s转码成utf-8的byte[],并传给t,因为t的byte[]本就是utf-8编码的,所以输出时,用utf-8解码,自然不会有乱码。
这种方法,输出的文件还是以utf-8编码格式编码的,如果像参考文章所说的那样,真的需要别的编码格式的输出文件,那就用第三种方法了。

第三种方法,继承并重写TextOutputFormat

从参考文章可以看出,TextOutputFormat已经固定输出为utf-8格式了,我们要做的就是继承TextOutputFormat,重写所有能够输出的函数。
但是真的控制输出的并不是TextOutputFormat类,而是,TextOutputFormat类里的静态类LineRecordWriter。最简单的重写方法就是继承TextOutputFormat类,然后把TextOutputFormat的静态类LineRecordWriter整个复制过来,然后那所有 使用utf8变量的地方,都改成gbk变量,gbk变量是字符串”gbk”,代码如下

    public static class GBKOutputFormat<k, V> extends TextOutputFormat<k, V> {
        protected static class LineRecordWriter<K, V> extends
                RecordWriter<K, V> {
            private static final String gbk = "gbk";
            private static final byte[] newline;
            static {
                try {
                    newline = "\n".getBytes(gbk);
                } catch (UnsupportedEncodingException uee) {
                    throw new IllegalArgumentException("can't find " + gbk
                            + " encoding");
                }
            }

            protected DataOutputStream out;
            private final byte[] keyValueSeparator;

            public LineRecordWriter(DataOutputStream out,
                    String keyValueSeparator) {
                this.out = out;
                try {
                    this.keyValueSeparator = keyValueSeparator.getBytes(gbk);
                } catch (UnsupportedEncodingException uee) {
                    throw new IllegalArgumentException("can't find " + gbk
                            + " encoding");
                }
            }

            public LineRecordWriter(DataOutputStream out) {
                this(out, "\t");
            }

            /**
             * Write the object to the byte stream, handling Text as a special
             * case.
             * 
             * @param o
             *            the object to print
             * @throws IOException
             *             if the write throws, we pass it on
             */
            private void writeObject(Object o) throws IOException {
                if (o instanceof Text) {
                    Text to = (Text) o;
                    out.write(to.getBytes(), 0, to.getLength());
                } else {
                    out.write(o.toString().getBytes(gbk));
                }
            }

            public synchronized void write(K key, V value) throws IOException {

                boolean nullKey = key == null || key instanceof NullWritable;
                boolean nullValue = value == null
                        || value instanceof NullWritable;
                if (nullKey && nullValue) {
                    return;
                }
                if (!nullKey) {
                    writeObject(key);
                }
                if (!(nullKey || nullValue)) {
                    out.write(keyValueSeparator);
                }
                if (!nullValue) {
                    writeObject(value);
                }
                out.write(newline);
            }

            public synchronized void close(TaskAttemptContext context)
                    throws IOException {
                out.close();
            }
        }

    }

复制了源码中的LineRecordWriter类之后,把utf8变量都改成gbk变量,这个方法十分简单,也能解决gbk编码问题,更重要的是,这种方法产生的输出文件都是以gbk编码格式编码的。

参考文档:
Hadoop 中文编码相关问题 – mapreduce程序处理GBK编码数据并输出GBK编码数据
http://blog.youkuaiyun.com/zklth/article/details/11829563

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值