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

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


一、需求:

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

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

  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>  
2015/09/01 12:23:12 435[INFO]: xxxxx.xxx.xxx:
<xml>
	…
<xml>
2015/09/01 12:23:12 735[INFO]: xxxxx.xxx.xxx:
…
2015/09/01 12:23:13 835[INFO]: xxxxx.xxx.xxx:
<xml>
	…
<xml>
2015/09/01 12:23:13 835[INFO]: xxxxx.xxx.xxx:
<xml>
	…
<xml>


我们要提取得到:

  1. <xml>  
  2.          …  
  3. <xml>  
  4. <xml>  
  5.          …  
  6. <xml>  
  7. <xml>  
  8.          …  
  9. <xml>  
<xml>
         …
<xml>
<xml>
         …
<xml>
<xml>
         …
<xml>


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

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



TextInputFormat对文件处理流程


三、各个自定义类说明

1、MInputFormat.java

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

  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.  }  
 @Override
  //把开始与结束的限定符从configuration中读取,再传人到MeacorderRdader中
  public RecordReader<LongWritable, Text> 
    createRecordReader(InputSplit split,
                       TaskAttemptContext context) {
	//record结束的限定符
    String delimiter = context.getConfiguration().get(
        "textinputformat.record.end.delimiter");
    //record限定的开始符
    String startdelimiter = context.getConfiguration().get(
            "textinputformat.record.start.delimiter");
    
    byte[] recordDelimiterBytes = null;
    byte[] startRecordDelimiterBytes = null;
    
    if (null != delimiter)
      recordDelimiterBytes = delimiter.getBytes(Charsets.UTF_8);
    startRecordDelimiterBytes = startdelimiter.getBytes(Charsets.UTF_8);
    //返回自定义的recordReader
    return new MRecordReader(startRecordDelimiterBytes,recordDelimiterBytes);
  }


2、MFileInputFormat.java

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

  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.   }  
public static void setInputPaths(Job job, 
                                   Path... inputPaths) throws IOException {
	  Configuration conf = job.getConfiguration();
  	
      StringBuffer str = new StringBuffer();
      for(int i = 0; i < inputPaths.length;i++) {
        Path path = inputPaths[i].getFileSystem(conf).makeQualified(inputPaths[i]);
        
        RemoteIterator<LocatedFileStatus> files = inputPaths[0].getFileSystem(conf).listFiles(path, true);
        while(files.hasNext())
	      {
	      		str.append(StringUtils.escapeString(files.next().getPath().toString()));
	      		str.append(StringUtils.COMMA_STR);
	      }
      }
      String strTmp = str.toString();
      if(strTmp != null && !strTmp.trim().equals(""))
    	  strTmp = strTmp.substring(0, strTmp.length()-StringUtils.COMMA_STR.length());
      LOG.info("**************************strTmp:"+strTmp);
      conf.set(INPUT_DIR, strTmp);
  }

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)找到起始限定符开始的位置,然后提取内容,直到匹配上了结束限定符。

  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.   }  
private int readCustomLine(Text str, int maxLineLength, int maxBytesToConsume)
      throws IOException {
	  int txtLength = 0; // tracks str.getLength(), as an optimization
	  long bytesConsumed = 0;//record的长度
	  int delPosn = 0;记录当前已匹配到delimiter的第几个字符
	  int ambiguousByteCount=0; // To capture the ambiguous characters count  //保存上一次buffer结束时候匹配到结束字符串的第几个字符
    str.clear();
    //这个是我加上去的,可以限定record从哪里开始哦
    this.startMarkbytesConsumed = 0;
    bufferPosn = readStartMark(this.startRecordDelimiterBytes);
    bytesConsumed += this.startMarkbytesConsumed;
    //添加匹配头
    str.append(this.startRecordDelimiterBytes, 0, this.startRecordDelimiterBytes.length);
    
    do {
      int startPosn = bufferPosn; // Start from previous end position
      if (bufferPosn >= bufferLength) {//貌似读到了当前buffer尽头了
        startPosn = bufferPosn = 0;
        //又要重新去load缓存的数据
        bufferLength = fillBuffer(in, buffer, ambiguousByteCount > 0);
        if (bufferLength <= 0) {//如果未能load进数据,要把那匹配的几个delimiter中的字符加上去
          str.append(endRecordDelimiterBytes, 0, ambiguousByteCount);
          break; // EOF
        }
      }
      for (; bufferPosn < bufferLength; ++bufferPosn) {
        if (buffer[bufferPosn] == endRecordDelimiterBytes[delPosn]) {//又找到一个匹配的字符了
        	//记录当前已匹配到delimiter的第几个字符
          delPosn++;
          if (delPosn >= endRecordDelimiterBytes.length) {//恭喜你
        	//完全可以匹配到delimiter,跳出循环,就要返回了。
            bufferPosn++;
            break;
          }
        }
        //很不幸,未能完全匹配到delimiter 
        else if (delPosn != 0) {
          bufferPosn--;//稍微前移一下
          delPosn = 0;
        }
      }
      int readLength = bufferPosn - startPosn;
      bytesConsumed += readLength;
    //当重新读取下一个buffer时,先让str不添加delimiter的前部分
      int appendLength = readLength - delPosn;
      //超过最大长度了,要截断
      if (appendLength > maxLineLength - txtLength) {
        appendLength = maxLineLength - txtLength;
      }
      if (appendLength > 0) {
        if (ambiguousByteCount > 0) {//若新的buffer不能构成完整的delimiter,就应该增加上次buffer的尾巴
          str.append(endRecordDelimiterBytes, 0, ambiguousByteCount);
          //appending the ambiguous characters (refer case 2.2)
          //bytesConsumed增加上次buffer的尾巴的计数
          bytesConsumed += ambiguousByteCount;
          ambiguousByteCount=0;
        }
        str.append(buffer, startPosn, appendLength);
        txtLength += appendLength;
      }
      if (bufferPosn >= bufferLength) {
    	//终于到一个buffer的终点了
	    //居然buffer的结尾匹配到了几个delimiter中的字符
        if (delPosn > 0 && delPosn < endRecordDelimiterBytes.length) {
        	//bytesConsumed要先减去那几个字符
          ambiguousByteCount = delPosn;
          bytesConsumed -= ambiguousByteCount; //to be consumed in next
        }
      }
    } while (delPosn < endRecordDelimiterBytes.length
        && bytesConsumed < maxBytesToConsume);//一次循环下来未能完全匹配一个结束字符串
    if (bytesConsumed > Integer.MAX_VALUE) {
      throw new IOException("Too many bytes before delimiter: " + bytesConsumed);
    }
    
    //只有构成一个完整的开始与结束
    if(delPosn == endRecordDelimiterBytes.length){
    	//作为结果返回
    	str.append(this.endRecordDelimiterBytes,0,this.endRecordDelimiterBytes.length);
    	str.set(str.toString());
    }
    
    return (int) bytesConsumed; 
  }

四、运行

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

  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);  
Configuration conf = new Configuration();
		Job job = Job.getInstance(conf, "job_"+args[0]+"_"+args[1]);
		job.setJarByClass(ReqExtracJob.class);
		// TODO: specify input and output DIRECTORIES (not files)
		MFileInputFormat.setInputPaths(job, new Path(args[2]));
		FileOutputFormat.setOutputPath(job, new Path(args[3]));
		//抽取字符串的起始与截止位置
		job.getConfiguration().set("textinputformat.record.startdelimiter","<xml>");
		job.getConfiguration().set("textinputformat.record.delimiter","</xml>");
		job.setInputFormatClass(MInputFormat.class);
		job.setOutputFormatClass(TextOutputFormat.class);
 

附:

相关代码下载链接
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值