自定义MapReduce的InputFormat,提取指定开始与结束限定符间的内容

来源:互联网 发布:Java 获取项目发布路径 编辑:程序博客网 时间:2024/05/21 06:41

转:http://blog.csdn.net/kent7306/article/details/49443899


一、需求:

在编写MapReduce程序时,常用的TextInputFormat是以换行符作为Record分隔符的,即该行的内容作为MapReduce中map方法中的value,而该行头在文件中的偏移值作为key。

但是在实际应用中,我们在提取日志内容时,有可能遇到一条Record包含多行的情况,并且要提取字段开始限定符到结束限定符的情况,如下。

[html] view plain copy print?
  1. 2015/09/01 12:23:12 435[INFO]: xxxxx.xxx.xxx:  
  2. <xml>  
  3.     …  
  4. <xml>  
  5. 2015/09/01 12:23:12 735[INFO]: xxxxx.xxx.xxx:  
  6. …  
  7. 2015/09/01 12:23:13 835[INFO]: xxxxx.xxx.xxx:  
  8. <xml>  
  9.     …  
  10. <xml>  
  11. 2015/09/01 12:23:13 835[INFO]: xxxxx.xxx.xxx:  
  12. <xml>  
  13.     …  
  14. <xml>  


我们要提取得到:

[html] view plain copy print?
  1. <xml>  
  2.          …  
  3. <xml>  
  4. <xml>  
  5.          …  
  6. <xml>  
  7. <xml>  
  8.          …  
  9. <xml>  


二、相关类的UML图及TextInputFormat流程:

这时候就要自定义自己的InputFormat来完成这个功能。其中涉及到的类如下,其中黑色的类是org.apache.Hadoop.mapreduce.lib.input的源码,蓝色的类是对应左边新创建的类。



TextInputFormat对文件处理流程


三、各个自定义类说明

1、MInputFormat.java

说明:该类ctrl+v了org.apache.hadoop.mapreduce.lib.input.TextInputFormat, 调整了以下方法:

[java] view plain copy print?
  1. @Override  
  2.  //把开始与结束的限定符从configuration中读取,再传人到MeacorderRdader中  
  3.  public RecordReader<LongWritable, Text>   
  4.    createRecordReader(InputSplit split,  
  5.                       TaskAttemptContext context) {  
  6. //record结束的限定符  
  7.    String delimiter = context.getConfiguration().get(  
  8.        "textinputformat.record.end.delimiter");  
  9.    //record限定的开始符  
  10.    String startdelimiter = context.getConfiguration().get(  
  11.            "textinputformat.record.start.delimiter");  
  12.      
  13.    byte[] recordDelimiterBytes = null;  
  14.    byte[] startRecordDelimiterBytes = null;  
  15.      
  16.    if (null != delimiter)  
  17.      recordDelimiterBytes = delimiter.getBytes(Charsets.UTF_8);  
  18.    startRecordDelimiterBytes = startdelimiter.getBytes(Charsets.UTF_8);  
  19.    //返回自定义的recordReader  
  20.    return new MRecordReader(startRecordDelimiterBytes,recordDelimiterBytes);  
  21.  }  


2、MFileInputFormat.java

说明:该类ctrl+v了org.apache.hadoop.mapreduce.lib.input.FileInputFormat,原来是不用改的,但是现在有这样的需求,递归读取指定目录下的文件,而原来的FileInputFormat只会读取当前指定目录的文件而已。调整了以下方法:

[java] view plain copy print?
  1. public static void setInputPaths(Job job,   
  2.                                    Path... inputPaths) throws IOException {  
  3.       Configuration conf = job.getConfiguration();  
  4.       
  5.       StringBuffer str = new StringBuffer();  
  6.       for(int i = 0; i < inputPaths.length;i++) {  
  7.         Path path = inputPaths[i].getFileSystem(conf).makeQualified(inputPaths[i]);  
  8.           
  9.         RemoteIterator<LocatedFileStatus> files = inputPaths[0].getFileSystem(conf).listFiles(path, true);  
  10.         while(files.hasNext())  
  11.           {  
  12.                 str.append(StringUtils.escapeString(files.next().getPath().toString()));  
  13.                 str.append(StringUtils.COMMA_STR);  
  14.           }  
  15.       }  
  16.       String strTmp = str.toString();  
  17.       if(strTmp != null && !strTmp.trim().equals(""))  
  18.           strTmp = strTmp.substring(0, strTmp.length()-StringUtils.COMMA_STR.length());  
  19.       LOG.info("**************************strTmp:"+strTmp);  
  20.       conf.set(INPUT_DIR, strTmp);  
  21.   }  

3、MRecordReader.java

说明:该类ctrl+v了org.apache.hadoop.mapreduce.lib.input.LineRecordReader,该类的nextKeyVlaue方法是读取(key,value)传递给Mapper的map方法,该类修改不多,主要把限定符传递给splitReader,就不贴代码了。


4、SplitMReader.java与MCompressedSplitReader.java

说明:该类ctrl+v了org.apache.hadoop.mapreduce.lib.input. CompressedSplitLineReader与SplitLineReader,MCompressedSplitReader继承SplitMReader,MRecordReader根据会判断文件是否为压缩文件选择用MCompressedSplitReader或SplitMReader,修改不多,就不贴代码了。

5、MReader.java

说明:该类ctrl+v了org.apache.hadoop.mapreduce.lib.input.LineReader,主要的内容分隔读取逻辑在该类实现。思路是类中有个buffer缓存数组,以游标形式顺序读取文件或数据块不断放入buffer中,内容提取从readCustomLine这个方法开始,不断扫描buffer中的数据,用方法readStartMark(byte[] startMarkBytes)找到起始限定符开始的位置,然后提取内容,直到匹配上了结束限定符。

[java] view plain copy print?
  1. private int readCustomLine(Text str, int maxLineLength, int maxBytesToConsume)  
  2.       throws IOException {  
  3.       int txtLength = 0// tracks str.getLength(), as an optimization  
  4.       long bytesConsumed = 0;//record的长度  
  5.       int delPosn = 0;////记录当前已匹配到delimiter的第几个字符  
  6.       int ambiguousByteCount=0// To capture the ambiguous characters count  //保存上一次buffer结束时候匹配到结束字符串的第几个字符  
  7.     str.clear();  
  8.     //这个是我加上去的,可以限定record从哪里开始哦  
  9.     this.startMarkbytesConsumed = 0;  
  10.     bufferPosn = readStartMark(this.startRecordDelimiterBytes);  
  11.     bytesConsumed += this.startMarkbytesConsumed;  
  12.     //添加匹配头  
  13.     str.append(this.startRecordDelimiterBytes, 0this.startRecordDelimiterBytes.length);  
  14.       
  15.     do {  
  16.       int startPosn = bufferPosn; // Start from previous end position  
  17.       if (bufferPosn >= bufferLength) {//貌似读到了当前buffer尽头了  
  18.         startPosn = bufferPosn = 0;  
  19.         //又要重新去load缓存的数据  
  20.         bufferLength = fillBuffer(in, buffer, ambiguousByteCount > 0);  
  21.         if (bufferLength <= 0) {//如果未能load进数据,要把那匹配的几个delimiter中的字符加上去  
  22.           str.append(endRecordDelimiterBytes, 0, ambiguousByteCount);  
  23.           break// EOF  
  24.         }  
  25.       }  
  26.       for (; bufferPosn < bufferLength; ++bufferPosn) {  
  27.         if (buffer[bufferPosn] == endRecordDelimiterBytes[delPosn]) {//又找到一个匹配的字符了  
  28.             //记录当前已匹配到delimiter的第几个字符  
  29.           delPosn++;  
  30.           if (delPosn >= endRecordDelimiterBytes.length) {//恭喜你  
  31.             //完全可以匹配到delimiter,跳出循环,就要返回了。  
  32.             bufferPosn++;  
  33.             break;  
  34.           }  
  35.         }  
  36.         //很不幸,未能完全匹配到delimiter   
  37.         else if (delPosn != 0) {  
  38.           bufferPosn--;//稍微前移一下  
  39.           delPosn = 0;  
  40.         }  
  41.       }  
  42.       int readLength = bufferPosn - startPosn;  
  43.       bytesConsumed += readLength;  
  44.     //当重新读取下一个buffer时,先让str不添加delimiter的前部分  
  45.       int appendLength = readLength - delPosn;  
  46.       //超过最大长度了,要截断  
  47.       if (appendLength > maxLineLength - txtLength) {  
  48.         appendLength = maxLineLength - txtLength;  
  49.       }  
  50.       if (appendLength > 0) {  
  51.         if (ambiguousByteCount > 0) {//若新的buffer不能构成完整的delimiter,就应该增加上次buffer的尾巴  
  52.           str.append(endRecordDelimiterBytes, 0, ambiguousByteCount);  
  53.           //appending the ambiguous characters (refer case 2.2)  
  54.           //bytesConsumed增加上次buffer的尾巴的计数  
  55.           bytesConsumed += ambiguousByteCount;  
  56.           ambiguousByteCount=0;  
  57.         }  
  58.         str.append(buffer, startPosn, appendLength);  
  59.         txtLength += appendLength;  
  60.       }  
  61.       if (bufferPosn >= bufferLength) {  
  62.         //终于到一个buffer的终点了  
  63.         //居然buffer的结尾匹配到了几个delimiter中的字符  
  64.         if (delPosn > 0 && delPosn < endRecordDelimiterBytes.length) {  
  65.             //bytesConsumed要先减去那几个字符  
  66.           ambiguousByteCount = delPosn;  
  67.           bytesConsumed -= ambiguousByteCount; //to be consumed in next  
  68.         }  
  69.       }  
  70.     } while (delPosn < endRecordDelimiterBytes.length  
  71.         && bytesConsumed < maxBytesToConsume);//一次循环下来未能完全匹配一个结束字符串  
  72.     if (bytesConsumed > Integer.MAX_VALUE) {  
  73.       throw new IOException("Too many bytes before delimiter: " + bytesConsumed);  
  74.     }  
  75.       
  76.     //只有构成一个完整的开始与结束  
  77.     if(delPosn == endRecordDelimiterBytes.length){  
  78.         //作为结果返回  
  79.         str.append(this.endRecordDelimiterBytes,0,this.endRecordDelimiterBytes.length);  
  80.         str.set(str.toString());  
  81.     }  
  82.       
  83.     return (int) bytesConsumed;   
  84.   }  

四、运行

在job的main方法中使用该自定义InputFormat,读取日志文件:

[java] view plain copy print?
  1. Configuration conf = new Configuration();  
  2.         Job job = Job.getInstance(conf, "job_"+args[0]+"_"+args[1]);  
  3.         job.setJarByClass(ReqExtracJob.class);  
  4.         // TODO: specify input and output DIRECTORIES (not files)  
  5.         MFileInputFormat.setInputPaths(job, new Path(args[2]));  
  6.         FileOutputFormat.setOutputPath(job, new Path(args[3]));  
  7.         //抽取字符串的起始与截止位置  
  8.         job.getConfiguration().set("textinputformat.record.startdelimiter","<xml>");  
  9.         job.getConfiguration().set("textinputformat.record.delimiter","</xml>");  
  10.         job.setInputFormatClass(MInputFormat.class);  
  11.         job.setOutputFormatClass(TextOutputFormat.class);  
[java] view plain copy print?
  1.   

附:

相关代码下载链接
0 0