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-1 | 1 | 1 (乱码) |
GBK-2312 | 1 | 2 (查码表) |
GBK | 1 | 2 (查码表) |
UTF-16 | 2 | 2 |
UTF-8 | 1 | 3 |
-
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);
}
}