MR中自定义outputFormat
outputFormat接口的实现类
OoutputFormat时MR输出时的基类,所有mr输出都实现了OF接口,一下时几种常见的OF实现类:
- TextOutputFormat 文本输出:
- 为默认输出格式,将每条记录写为文本行,键和值可以是任意类型,因为会调用它们的toSTring方法
- sequenceFileOutputFormat :
- 将SequenceFileOutputFormat的输出作为后续 MapReduce任务的输入,这便是一种好的输出格式,因为它的格式紧凑,很容易被压缩。
- 自定义OutputFormat:
- 即接下来要实现的内容,可以根据需求自定义实现输出
自定义OutputFormat
自定义OutputFormat的步骤
- 自定义一个类继续FileOutputFormat
- 自定义RecordWriter,改写输出数据的方法write
通过过滤网页日志案例实操自定义OutputFormat
-
需求:
过滤输入的log日志,将包含TOUHOU的网站记录输出到TOUHOU.log中,不包含的输出到other.log中
-
输入数据为:
net.log
内容为:
http://www.baidu.com http://www.google.com http://cn.bing.com http://www.TOUHOU.only.com http://www.sohu.com http://www.sina.com http://www.sin2a.com http://www.TOUHOU.com http://www.sindsafa.com http://www.google.com http://cn.bing.com http://www.TOUHOU.com http://www.TOUHOU.com http://www.sina.com
-
预期输出:
含有TOUHOU字样的条目和其他条目分开输出
-
代码实现:
CustomOF(自定义OF
import org.apache.hadoop.io.NullWritable; import org.apache.hadoop.mapreduce.RecordWriter; import org.apache.hadoop.mapreduce.TaskAttemptContext; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import java.io.IOException; /** * 继承FileOutputFormat * 实现getRecordWriter方法,返回自定义的RecordWriter */ public class CustomOF extends FileOutputFormat<String, NullWritable> { public RecordWriter<String, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException { return new CustomRecordWriter(job); } }
CustomRecordWriter:
import org.apache.hadoop.fs.FSDataOutputStream; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.IOUtils; import org.apache.hadoop.io.NullWritable; import org.apache.hadoop.mapreduce.RecordWriter; import org.apache.hadoop.mapreduce.TaskAttemptContext; import java.io.IOException; /** * 自定义写出器,负责将数据写出到磁盘中 * 继承RecordWriter,实现其中write方法 * 另外实现其初始化和资源关闭 */ public class CustomRecordWriter extends RecordWriter<String, NullWritable> { //两个输出流和文件系统 //通过两个输出流实现输出到不同文件中 private FSDataOutputStream THOS; private FSDataOutputStream otherOS; private FileSystem fs; /** * 构造方法,通过传入的context创建输出流 * 初始化输出流 */ public CustomRecordWriter(TaskAttemptContext job) throws IOException { fs = FileSystem.get(job.getConfiguration()); //两个输出路径 Path TOUHOUPath = new Path( "e:/work/test/output1/TOUHOU.log"); Path otherPath = new Path( "e:/work/test/output1/other.log"); //创建输出流 THOS = fs.create(TOUHOUPath); otherOS = fs.create(otherPath); } /** * 写出的业务实现 * 同其他自定义组件那样 * 只要掌握了业务的内核以及实现的步骤 * 本身的业务代码很简单,就是一个if判断 */ public void write(String key, NullWritable value) throws IOException, InterruptedException { //判断网址中是否包含TOUHOU字样,如包含输出到TOUHOU.log中 if (key.contains("TOUHOU")){ //TOUHOU THOS.write(key.getBytes()); }else { //other otherOS.write(key.getBytes()); } } /** * 负责在最后关闭资源 */ public void close(TaskAttemptContext context) throws IOException, InterruptedException { if(THOS!=null){ IOUtils.closeStream(THOS); } if(otherOS!=null){ IOUtils.closeStream(otherOS); } if(fs!=null){ fs.close(); } } }
Mapper:
import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.NullWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Mapper; import java.io.IOException; /** * mapper类中只要读取数据并输出即可 * 本次业务没有reduce阶段,所以mr会直接把mapper阶段的输出作为mr的输出数据处理 */ public class CustomMapper extends Mapper<LongWritable, Text, String, NullWritable> { //输出的key private String k_out; /** * 只要将输入数据的value、也即是一行网址作为key输出即可 */ @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { k_out=value.toString(); //记得给输出加上换行,不然输出数据会只有一行 context.write(k_out+"\r\n",NullWritable.get()); } }dd
driver(部分代码)
//将reduceTask数量设置为0就可以跳过reduce阶段 job.setNumReduceTasks(0); //设置使用自定义的outputFormat job.setOutputFormatClass(CustomOF.class);
-
运行结果:
我们可以在mr的输出路径看到成功标识文件:
在recordwriter中指定的输出路径中可以找到输出文件:
other.log:
http://www.baidu.com
http://www.google.com
http://cn.bing.com
http://www.sohu.com
http://www.sina.com
http://www.sin2a.com
http://www.sindsafa.com
http://www.google.com
http://cn.bing.com
http://www.sina.com
TOUHOU.log:
http://www.TOUHOU.only.com
http://www.TOUHOU.com
http://www.TOUHOU.com
http://www.TOUHOU.com
MR中的JOIN操作
reduce Join
- 原理:
- map端将来自不通过表或文件的k-v对打标签区别不通来源的记录。然后将连接字段作为key,其余部分作为value,最后输出
- reduce端组已经将数据以连接字段作为key分组完成了,我们只需要在每一个分组当中将那些来源不同的文件记录分开,最后合并即可。
- 该法的缺点:
- 很明显会导致shuffle阶段出现大量的数据传输,效率很低
reduce Join案例实操
-
需求:
-
订单数据表:
id pid amount 1001 01 1 1002 02 2 1003 03 3 -
商品信息表:
pid pname 01 小米 02 华为 03 格力 -
最终期望输出的表:
id pname amount 1001 小米 1 1004 小米 4 1002 华为 2 1005 华为 5 1003 格力 3 1006 格力 6
-
-
分析:
- 通过将关联条件作为map输出的key将两表满足join条件的额数据并携带数据来源的文件信息,法网同一个reduceTask,在reduce中进行数据的串联。
-
代码实现
封装类TableBean(setter getter略
import org.apache.hadoop.io.Writable; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; /** * 利用flag来标识来自哪个文件 */ public class TableBean implements Writable { private String order_id; private String p_id; private String p_name; private String amount; private String flag; /** * 改写toString方法,使其输出我们想要的数据 */ @Override public String toString() { return order_id+"\t"+p_name+"\t"+amount; } public void write(DataOutput out) throws IOException { out.writeUTF(order_id); out.writeUTF(p_id); out.writeUTF(p_name); out.writeUTF(amount); out.writeUTF(flag); } public void readFields(DataInput in) throws IOException { order_id=in.readUTF(); p_id=in.readUTF(); p_name=in.readUTF(); amount=in.readUTF(); flag=in.readUTF(); } }
mapper
import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Mapper; import org.apache.hadoop.mapreduce.lib.input.FileSplit; import java.io.IOException; /** * 读取文件后先判断数据来自哪个文件 * 区分文件对数据进行不通的封装处理 */ public class JoinMapper extends Mapper<LongWritable, Text, IntWritable, TableBean> { private IntWritable k_out = new IntWritable(); private TableBean v_out =new TableBean(); private String name; /** * setup方法在map方法前调用一次 * 可以用于初始化一些参数 * 这里用于判断数据来自哪个文件 */ @Override protected void setup(Context context) throws IOException, InterruptedException { FileSplit file = (FileSplit) context.getInputSplit(); name = file.getPath().getName(); } /** * map方法中要将数据封装到tableBean对象中 * 然后写出 */ @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] words = value.toString().split("\t"); if ("order.txt".equals(name)){ //来自order文件,即订单文件 v_out.setOrder_id(words[0]); v_out.setP_id(words[1]); v_out.setP_name("nodata"); v_out.setAmount(words[3]); v_out.setFlag("order"); }else { //来自p_id文件,即商品文件 v_out.setOrder_id("nodata"); v_out.setP_id(words[0]); v_out.setP_name(words[1]); v_out.setAmount("nodata"); v_out.setFlag("pd"); } //封装输出的键,即连接字段p_id(商品id k_out.set(Integer.parseInt(v_out.getP_id())); context.write(k_out,v_out); } }
reducer
import org.apache.commons.beanutils.BeanUtils; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.NullWritable; import org.apache.hadoop.mapreduce.Reducer; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; /** * 利用map阶段对数据进行的标识完成分类 * 将pd文件中读取到的数据于order文件中读取到的数据连接 * 最后的到的结果写出 */ public class JoinReducer extends Reducer<IntWritable, TableBean, TableBean, NullWritable> { @Override protected void reduce(IntWritable key, Iterable<TableBean> values, Context context) throws IOException, InterruptedException { //用于存放数据的列表 ArrayList<TableBean> tableBeans = new ArrayList<TableBean>(); //新建一个类用于存放pd表中的数据 TableBean pdBean = new TableBean(); //遍历输入的values,将来自两张表的数据作区分 for (TableBean bean : values) { if ("order".equals(bean.getFlag())){ TableBean orderBean = new TableBean(); try { BeanUtils.copyProperties(orderBean,bean); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } //如果来自order文件,加入list中 tableBeans.add(orderBean); }else { try { //如果来自pd表,将其数据拷贝到提前准备好的对象中 BeanUtils.copyProperties(pdBean,bean); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } } //遍历连接表,并输出数据 for (TableBean tableBean : tableBeans) { tableBean.setP_name(pdBean.getP_name()); context.write(tableBean,NullWritable.get()); } } }
driver
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.NullWritable; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import java.io.IOException; public class TableDriver { public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException { //输入输出路径 Path inputPath = new Path( "e:/work/test/input/join"); Path outputPath = new Path( "e:/work/test/output"); FileSystem fs = FileSystem.get(new Configuration()); if (fs.exists(outputPath)){ fs.delete(outputPath,true); } fs.close(); //获取job Configuration conf = new Configuration(); Job job = Job.getInstance(conf); job.setJarByClass(TableDriver.class); job.setMapperClass(JoinMapper.class); job.setReducerClass(JoinReducer.class); job.setMapOutputKeyClass(IntWritable.class); job.setMapOutputValueClass(TableBean.class); job.setOutputKeyClass(TableBean.class); job.setOutputValueClass(NullWritable.class); FileOutputFormat.setOutputPath(job,outputPath); FileInputFormat.setInputPaths(job,inputPath); boolean result = job.waitForCompletion(true); System.out.println(result?1:-1); } }
输出结果为:
1004 小米 4 1001 小米 1 1005 华为 5 1002 华为 2 1006 格力 6 1003 格力 3
结果正确。
map Join
- 两种实现方法
- 在mapper的setup阶段将文件读取到缓存中。
- 在驱动函数中加载缓存
- 适用场景
- 适用于一张表非常小,另一张表很大的时候。
- 优点
- 在map端缓存多张表,提前处理业务逻辑,这样增加map端业务,减少reduce端数据的压力。
map join案例实操
-
数据同reduce join的数据,需求同
-
代码实现:
mapper
import org.apache.commons.lang.StringUtils; import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.NullWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Mapper; import java.io.*; import java.net.URI; import java.util.HashMap; import java.util.Map; public class JoinMapper extends Mapper<LongWritable, Text, Text, NullWritable> { private Map<String, String> pdMap = new HashMap<String, String>(); private Text k_out = new Text(); /** * 负责在map开始前将pd表加入内存中 */ @Override protected void setup(Context context) throws IOException { URI[] cacheFiles = context.getCacheFiles(); for (URI cacheFile : cacheFiles) { BufferedReader reader = new BufferedReader( new FileReader(new File(cacheFile))); String line = ""; while (StringUtils.isNotBlank(line = reader.readLine())){ String[] words = line.split("\t"); pdMap.put(words[0],words[1]); } reader.close(); } } /** * 只要简单的切割数据 * 然后连接封装就好 */ @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { //切割输入 String[] words = value.toString().split("\t"); //将数据连接封装 k_out.set(words[0]+"\t"+pdMap.get(words[1])+"\t"+words[2]); //写出 context.write(k_out,NullWritable.get()); } }
driver
//注意这里的uri的路径和平日写的路径不同 job.addCacheFile(new URI("file:///e:/work/test/input/pd.txt")); //设置reduce任务数为0 job.setNumReduceTasks(0);
输出结果为
1001 小米 1 1002 华为 2 1003 格力 3 1004 小米 4 1005 华为 5 1006 格力 6
符合要求