自定义hadoop map/reduce输入文件切割InputFormat 更改输入value的分隔符

本文转载自:http://hi.baidu.com/lzpsky/blog/item/99d58738b08a68e7b311c70d.html  


hadoop会对原始输入文件进行文件切割,然后把每个split传入mapper程序中进行处理,FileInputFormat是所有以文件作 为数据源的InputFormat实现的基类,FileInputFormat保存作为job输入的所有文件,并实现了对输入文件计算splits的方 法。至于获得记录的方法是有不同的子类进行实现的。

        那么,FileInputFormat是怎样将他们划分成splits的呢?FileInputFormat只划分比HDFS block大的文件,所以如果一个文件的大小比block小,将不会被划分,这也是Hadoop处理大文件的效率要比处理很多小文件的效率高的原因。 

       hadoop默认的InputFormat是TextInputFormat,重写了FileInputFormat中的createRecordReader和isSplitable方法。该类使用的reader是LineRecordReader,即以回车键(CR = 13)或换行符(LF = 10)为行分隔符。

[java]  view plain copy
  1. start += in.readLine(new Text(), 0,    
  2.                              (int)Math.min((long)Integer.MAX_VALUE, end - start));    
  3.       } //这个按行读取就是响应的回车及换行    

      但大多数情况下,回车键或换行符作为输入文件的行分隔符并不能满足我们的需求,通常用户很有可能会输入回车键、换行符,所以通常我们会定义不可见字符(即用户无法输入的字符)为行分隔符,这种情况下,就需要新写一个InputFormat

      又或者,一条记录的分隔符不是字符,而是字符串,这种情况相对麻烦;还有一种情况,输入文件的主键key已经是排好序的了,需要hadoop做的只是把相 同的key作为一个数据块进行逻辑处理,这种情况更麻烦,相当于免去了mapper的过程,直接进去reduce,那么InputFormat的逻辑就相 对较为复杂了,但并不是不能实现。

1、改变一条记录的分隔符,不用默认的回车或换行符作为记录分隔符,甚至可以采用字符串作为记录分隔符
     1)自定义一个InputFormat,继承FileInputFormat,重写createRecordReader方法,如果不需要分片或者需要改变分片的方式,则重写isSplitable方法,具体代码如下:

  1. public class FileInputFormatB extends FileInputFormat<LongWritable, Text> {  
  2.    @Override  
  3.    public RecordReader<LongWritable, Text> createRecordReader( InputSplit split, TaskAttemptContext context) {  
  4.         <span style="color:#ff0000;">return new SearchRecordReader("\b");</span>  
  5.     }  
  6.     @Override  
  7.     protected boolean isSplitable(FileSystem fs, Path filename) {  
  8.         <span style="color:#ff6666;"// 输入文件不分片  
  9.         return false;</span>  
  10.      }  
  11. }  
这里的“\b“应该就是指定的分隔符,并要求不对单个文件进行分片处理。
2)关键在于定义一个新的SearchRecordReader继承RecordReader,支持自定义的行分隔符,即一条记录的分隔符。标红的地方为与hadoop默认的LineRecordReader不同的地方。

  1. private CompressionCodecFactory compressionCodecs = null;  
  2.  private long start;  
  3.  private long pos;  
  4.  private long end;  
  5.  private LineReader in;  
  6.  private int maxLineLength;  
  7.  private LongWritable key = null;  
  8.  private Text value = null;  
  9. <span style="color:#ff0000;"//行分隔符,即一条记录的分隔符  
  10.  private byte[] separator = {'\b'};  
  11.  private int sepLength = 1;  
  12. ‍ public IsearchRecordReader(){  
  13.  }  
  14.  public IsearchRecordReader(String seps){//赋值的分隔符  
  15.   this.separator = seps.getBytes();   
  16.   sepLength = separator.length;  
  17.  }</span>  
  18.  public void initialize(InputSplit genericSplit, TaskAttemptContext context) throws IOException {  
  19.   FileSplit split = (FileSplit) genericSplit;  
  20.   Configuration job = context.getConfiguration();<span style="color:#ff6666;">//获取对应的配置文件</span>  
  21.   this.maxLineLength = job.getInt("mapred.linerecordreader.maxlength", Integer.MAX_VALUE);  
  22.   this.start = split.getStart();<span style="color:#ff6666;">//分片的起始位置</span>  
  23.   this.end = (this.start + split.getLength());<span style="color:#ff6666;">//split的结尾</span>  
  24.   Path file = split.getPath();<span style="color:#ff6666;">//获得路径</span>  
  25.   this.compressionCodecs = new CompressionCodecFactory(job);  
  26.   CompressionCodec codec = this.compressionCodecs.getCodec(file);  
  27.   // open the file and seek to the start of the split  
  28.   FileSystem fs = file.getFileSystem(job);  
  29.   FSDataInputStream fileIn = fs.open(split.getPath())<span style="color:#ff6666;">;//打开分片数据</span>  
  30.   boolean skipFirstLine = false;  
  31.   if (codec != null) {  
  32.    this.in = new LineReader(codec.createInputStream(fileIn), job);  
  33.    this.end = Long.MAX_VALUE;  
  34.   } else {  
  35.    if (this.start != 0L) {  
  36.     skipFirstLine = true;  
  37.    <span style="color:#ff6666;"this.start -= sepLength;//改变分隔符需要修改的地方</span>  
  38.     fileIn.seek(this.start);  
  39.    }  
  40.    this.in = new LineReader(fileIn, job);  
  41.   }  
  42.   if (skipFirstLine) { // skip first line and re-establish "start".  
  43.    int newSize = in.readLine(new Text(), 0, (int) Math.min( (long) Integer.MAX_VALUE, end - start));  
  44.      
  45.    if(newSize > 0){  
  46.     start += newSize;  
  47.    }  
  48.   }  
  49.   this.pos = this.start;  
  50.  }  
  51.  public boolean nextKeyValue() throws IOException {  
  52.   if (this.key == null) {  
  53.    this.key = new LongWritable();  
  54.   }  
  55.   this.key.set(this.pos);  
  56.   if (this.value == null) {  
  57.    this.value = new Text();  
  58.   }  
  59.   int newSize = 0;  
  60.   while (this.pos < this.end) {  
  61.    newSize = this.in.readLine(this.value, this.maxLineLength, Math.max(  
  62.  (int) Math.min(Integer.MAX_VALUE, this.end - this.pos), this.maxLineLength));  
  63.    if (newSize == 0) {  
  64.     break;  
  65.    }  
  66.    this.pos += newSize;  
  67.    if (newSize < this.maxLineLength) {  
  68.     break;  
  69.    }  
  70.    LOG.info("Skipped line of size " + newSize + " at pos " + (this.pos - newSize));  
  71.   }  
  72.   if (newSize == 0) {  
  73.    <span style="color:#ff6666;">//读下一个buffer</span>  
  74.    this.key = null;  
  75.    this.value = null;  
  76.    return false;  
  77.   }  
  78.   //读同一个buffer的下一个记录  
  79.   return true;  
  80.  }  
  81.  public LongWritable getCurrentKey() {  
  82.   return this.key;  
  83.  }  
  84.  public Text getCurrentValue() {  
  85.   return this.value;  
  86.  }  
  87.  public float getProgress() {  
  88.   if (this.start == this.end) {  
  89.    return 0.0F;  
  90.   }  
  91.   return Math.min(1.0F, (float) (this.pos - this.start) / (float) (this.end - this.start));  
  92.  }  
  93.  public synchronized void close() throws IOException {  
  94.   if (this.in != null)  
  95.    this.in.close();  
  96.  }  
  97. }  

 3)重写SearchRecordReader需要的LineReader,可作为SearchRecordReader内部类。特别需要注意的地方就 是,读取文件的方式是 按指定大小的buffer来读, 必定就会遇到一条完整的记录被切成两半,甚至如果分隔符大于1个字符时分隔符也会被切成两半的情况, 这种情况一定要加以拼接处理。

  1. public class LineReader {  
  2.   //回车键(hadoop默认)  
  3.   //private static final byte CR = 13;  
  4.   //换行符(hadoop默认)  
  5.   //private static final byte LF = 10;  
  6.       
  7.   //按buffer进行文件读取  
  8.   private static final int DEFAULT_BUFFER_SIZE = 32 * 1024 * 1024;<span style="color:#ff6666;">//32M</span>  
  9.   private int bufferSize = DEFAULT_BUFFER_SIZE;  
  10.   private InputStream in;  
  11.   private byte[] buffer;  
  12.   private int bufferLength = 0;  
  13.   private int bufferPosn = 0;  
  14.     
  15.   LineReader(InputStream in, int bufferSize) {  
  16.    this.bufferLength = 0;  
  17.     this.bufferPosn = 0;  
  18.         
  19.    this.in = in;  
  20.    this.bufferSize = bufferSize;  
  21.    this.buffer = new byte[this.bufferSize];  
  22.   }  
  23.   public LineReader(InputStream in, Configuration conf) throws IOException {  
  24.    this(in, conf.getInt("io.file.buffer.size", DEFAULT_BUFFER_SIZE));  
  25.   }  
  26.   public void close() throws IOException {  
  27.    in.close();  
  28.   }  
  29.  public int readLine(Text str, int maxLineLength) throws IOException {  
  30.    return readLine(str, maxLineLength, Integer.MAX_VALUE);  
  31.   }  
  32.   public int readLine(Text str) throws IOException {  
  33.    return readLine(str, Integer.MAX_VALUE, Integer.MAX_VALUE);  
  34.   }  
  35.   <span style="color:#ff6666;">//以下是需要改写的部分_start,核心代码</span>  
  36. <span style="color:#ff6666;">  public int readLine(Text str, int maxLineLength, int maxBytesToConsume) throws IOException{//map函数中使用到得readline</span>  
  37.    str.clear();  
  38.    Text record = new Text();  
  39.    int txtLength = 0;  
  40.    long bytesConsumed = 0L;  
  41.    boolean newline = false;  
  42.    int sepPosn = 0;  
  43.      
  44.    do {  
  45.     //已经读到buffer的末尾了,读下一个buffer  
  46.     if (this.bufferPosn >= this.bufferLength) {  
  47.      bufferPosn = 0;  
  48.      bufferLength = in.read(buffer);  
  49.        
  50.     <span style="color:#ff6666;"//读到文件末尾了,则跳出,进行下一个文件的读取,</span><span style="color:#009900;">那这个时候算是新的splits吗?</span>  
  51.      if (bufferLength <= 0) {  
  52.       break;  
  53.      }  
  54.     }  
  55.       
  56.     int startPosn = this.bufferPosn;  
  57.     for (; bufferPosn < bufferLength; bufferPosn ++) {  
  58.     <span style="color:#ff6666;"//处理上一个buffer的尾巴被切成了两半的分隔符(如果分隔符中重复字符过多在这里会有问题)</span>  
  59.      if(sepPosn > 0 && buffer[bufferPosn] != separator[sepPosn]){  
  60.       sepPosn = 0;  
  61.      }  
  62.        
  63.      //遇到行分隔符的第一个字符  
  64.      if (buffer[bufferPosn] == separator[sepPosn]) {  
  65.       bufferPosn ++;  
  66.       int i = 0;  
  67.         
  68.       //判断接下来的字符是否也是行分隔符中的字符  
  69.       for(++ sepPosn; sepPosn < sepLength; i ++, sepPosn ++){  
  70.          
  71.        //buffer的最后刚好是分隔符,且分隔符被不幸地切成了两半  
  72.        if(bufferPosn + i >= bufferLength){  
  73.         bufferPosn += i - 1;  
  74.         break;  
  75.        }  
  76.          
  77.        //一旦其中有一个字符不相同,就判定为不是分隔符  
  78.        if(this.buffer[this.bufferPosn + i] != separator[sepPosn]){  
  79.         sepPosn = 0;  
  80.         break;  
  81.        }  
  82.       }  
  83.         
  84.       //的确遇到了行分隔符  
  85.       if(sepPosn == sepLength){  
  86.        bufferPosn += i;  
  87.        newline = true;  
  88.        sepPosn = 0;  
  89.        break;  
  90.       }  
  91.      }  
  92.     }  
  93.       
  94.     int readLength = this.bufferPosn - startPosn;  
  95.     bytesConsumed += readLength;  
  96.     //行分隔符不放入块中  
  97.     //int appendLength = readLength - newlineLength;  
  98.     if (readLength > maxLineLength - txtLength) {  
  99.      readLength = maxLineLength - txtLength;  
  100.     }  
  101.     if (readLength > 0) {  
  102.      record.append(this.buffer, startPosn, readLength);  
  103.      txtLength += readLength;  
  104.        
  105.      //去掉记录的分隔符  
  106.      if(newline){  
  107.       str.set(record.getBytes(), 0, record.getLength() - sepLength);  
  108.      }  
  109.     }  
  110.    } while (!newline && (bytesConsumed < maxBytesToConsume));  
  111.    if (bytesConsumed > (long)Integer.MAX_VALUE) {  
  112.     throw new IOException("Too many bytes before newline: " + bytesConsumed);  
  113.    }  
  114.      
  115.    return (int) bytesConsumed;  
  116.   }  
  117.   //以下是需要改写的部分_end  
  118. //以下是hadoop-core中LineReader的源码_start  
  119. public int readLine(Text str, int maxLineLength, int maxBytesToConsume) throws IOException{  
  120.     str.clear();  
  121.     int txtLength = 0;  
  122.     int newlineLength = 0;  
  123.     boolean prevCharCR = false;  
  124.     long bytesConsumed = 0L;  
  125.     do {  
  126.       int startPosn = this.bufferPosn;  
  127.       if (this.bufferPosn >= this.bufferLength) {  
  128.         startPosn = this.bufferPosn = 0;  
  129.         if (prevCharCR)  bytesConsumed ++;  
  130.         this.bufferLength = this.in.read(this.buffer);  
  131.         if (this.bufferLength <= 0)  break;  
  132.       }  
  133.       for (; this.bufferPosn < this.bufferLength; this.bufferPosn ++) {  
  134.         if (this.buffer[this.bufferPosn] == LF) {  
  135.           newlineLength = (prevCharCR) ? 2 : 1;  
  136.           this.bufferPosn ++;  
  137.           break;  
  138.         }  
  139.         if (prevCharCR) {  
  140.           newlineLength = 1;  
  141.           break;  
  142.         }  
  143.         prevCharCR = this.buffer[this.bufferPosn] == CR;  
  144.       }  
  145.       int readLength = this.bufferPosn - startPosn;  
  146.       if ((prevCharCR) && (newlineLength == 0))  
  147.         --readLength;  
  148.       bytesConsumed += readLength;  
  149.       int appendLength = readLength - newlineLength;  
  150.       if (appendLength > maxLineLength - txtLength) {  
  151.         appendLength = maxLineLength - txtLength;  
  152.       }  
  153.       if (appendLength > 0) {  
  154.         str.append(this.buffer, startPosn, appendLength);  
  155.         txtLength += appendLength; }  
  156.     }  
  157.     while ((newlineLength == 0) && (bytesConsumed < maxBytesToConsume));  
  158.     if (bytesConsumed > (long)Integer.MAX_VALUE) throw new IOException("Too many bytes before newline: " + bytesConsumed);  
  159.     return (int)bytesConsumed;  
  160.   }  
  161. //以下是hadoop-core中LineReader的源码_end  
  162. }  

2、已经按主键key排好序了,并保证相同主键key一定是在一起的,假设每条记录的第一个字段为主键,那么如 果沿用上面的LineReader,需要在核心方法readLine中对前后两条记录的id进行equals判断,如果不同才进行split,如果相同继 续下一条记录的判断。代码就不再贴了,但需要注意的地方,依旧是前后两个buffer进行交接的时候,非常有可能一条记录被切成了两半,一半在前一个buffer中,一半在后一个buffer中。

     这种方式的好处在于少去了reduce操作,会大大地提高效率,其实mapper的过程相当的快,费时的通常是reduce。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值