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