最近在排查异常数据的时候,有童鞋发现 hadoop 中 String.split() 方法会导致字段丢失,而测试 java 不会。
其实仔细想想这是不应该发生的事情,因为 hadoop 压根就没重写 string 的 split 方法,不会导致如此差异。
还是那句话:Talk is cheap, show you the code.
我们的测试数据如下:
^I 表示 该字段是 TAB 字符,awk 处理下来都是 7 个字段。
june@deepin 13:24:29 ~ > hadoop fs -cat /tmp/table.txt|awk -F"\t" '{print NF}' 7 7 june@deepin 13:24:46 ~ > hadoop fs -cat /tmp/table.txt|cat -A ^I^I^IA^IB^IC^ID$ A^I^I^IB^IC^ID^I$ june@deepin 13:24:52 ~ >下面来看看hadoop处理结果:
public class Test { public static class RelevanceMapper extends Mapper<LongWritable, Text, Text, Text> { public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { System.out.println("---->> map: " + value.toString().split("\t").length); context.write(new Text("1"), value); } } public static class RelevanceReducer extends Reducer<Text, Text, NullWritable, Text> { public void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException { for(Text va : values){ System.out.println("---->> reduce: " + va.toString().split("\t").length); } } } public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException { Configuration configuration = new Configuration(); String[] otherArgs = new GenericOptionsParser(configuration, args) .getRemainingArgs(); Job job = new Job(configuration, "test"); job.setJarByClass(Test.class); job.setNumReduceTasks(1); job.setMapperClass(RelevanceMapper.class); job.setReducerClass(RelevanceReducer.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(Text.class); job.setOutputKeyClass(NullWritable.class); job.setOutputValueClass(Text.class); FileInputFormat.addInputPath(job, new Path(args[0])); org.apache.hadoop.mapreduce.lib.output.FileOutputFormat.setOutputPath( job, new Path("/tmp/test2")); System.exit(job.waitForCompletion(true) ? 0 : 1); } }结果是:
---->> map: 7
---->> map: 6
---->> reduce: 7
---->> reduce: 6
这说明第2行最后的 \t 被忽略了,而前导 \t 是在hadoop中正常识别了。
那是不是hadoop才这样,而java不这样呢?
我们还是看代码:
public class test { public static void main(String[] args) { System.out.println("\t\t\tA\tB\tC\tD".split("\t").length); System.out.println("A\t\tB\t\tC\tD\t".split("\t").length); System.out.println("A\t\tB\t\tC\tD\t".split("\t", -1).length); } }结果是
7
6
7
可以看到 java中呈现的结果和hadoop一样,因为 hadoop 没有重写该方法,所以没有差别的。
那为什么第2行的末尾的 \t 没有被识别呢?
翻翻 jdk 官方文档就知道了:
public String[] split(String regex)根据给定 正则表达式 的匹配拆分此字符串。
该方法的作用就像是使用给定的表达式和限制参数 0 来调用两参数 split 方法。因此,所得数组中不包括结尾空字符串。
官方说的很清楚了:split 还有个重载的方法:public String[] split(String regex, int limit)我们不带limit其实就是默认limit为0,那自然结尾的空字符串也被忽略了。
要想 split 尽可能多的匹配:启用limit即可,就像例子中的最后一行,limit 取值 -1 即可。
结尾:
不仔细检查,这个还真容易被我们忽略,因为我们习惯或者经验告诉我们 java 中split也应该
和awk、python等其他语言一样,但是恰恰理所当然的我们掉进了 java 的坑里,这样数据统计
的时候就出错了。
—————————————————————————————————————————
下面再举一个最近有童鞋遇到的一个java split 的坑:
有童鞋写了这么两行代码,一直跑的好好的,今天半夜程序却在这里挂掉了:
String strTrackUrl = oo.xx() // strTrackUrl 不可能为 null
if (strTrackUrl.isEmpty())
return (long) 0;
strTrackUrl = strTrackUrl.split("x\\.shtml")[0];
然后这里就报:java.lang.ArrayIndexOutOfBoundsException: 0
为了模拟当时的场景,我们用下面的代码试试:
public class Test1 { public static void main(String[] args) { String strTrackUrl = " "; if (strTrackUrl.isEmpty()) System.out.println("空串!"); ; System.out.println("---------->> 1: [" + strTrackUrl.split(" ", -1)[0]+"]"); System.out.println("---------->> 2: [" + strTrackUrl.split("1")[0]+"]"); System.out.println("---------->> 3: [" + strTrackUrl.split(" ")[0]+"]"); } }
这个抛异常:
---------->> 1: []
---------->> 2: [ ]
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
at tmp.Test1.main(Test1.java:11)
想想这是为什么呢?
我想上面分析的应该会给你答案:
当用split(" ")的时候碰上了原串是" ",这样字符串变成了模式,而split(" ")[0]取到了空,
但是恰好此时空是要被忽略的,所以 split(" ", -1) 能确保安全无侧漏~ 哈哈~
PS:
总觉得java是一种臃肿不堪的语言。。。