Hadoop笔试题: 找出不同人的共同好友(要考虑数据去重)
例子:
张三:李四,王五,赵六
李四:张三,田七,王五
实际工作中,数据去重用的还是挺多的,包括空值的过滤等等,本文就 数据去重 与 倒排索引 详细讲解一下.
一、数据去重[模拟某运营商呼叫详单去重]
项目中统计数据集的种类个数、网站日志文件计算访问地等案例都会涉及到数据去重,重复数据删除等都是经常使用的存储数据缩减技术.通过一个简单案例来说明MapReduce怎么实现数据去重.
①原始模拟数据[c呼出,b呼入]
13711111111 c
13611111111 b
13711111111 b
13722222222 c
13611111111 c
13711111111 b
13611111111 b
13711111111 b
13722222222 b
13611111111 c
将同一条数据所有记录交给一台Reduce,最终结果输出一次即可.
Map阶段采用Hadoop默认作业输入方式后,输入的value作为输出的key.
//Mapper任务
static class DDMap extends Mapper<LongWritable,Text,Text,Text>{
private static Text line = new Text();
protected void map(LongWritable k1,Text v1,Context context){
line = v1;
Text text = new Text(“”);
try {
context.write(line,text);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
};
}
//Reducer任务
static class DDReduce extends Reducer<Text,Text,Text,Text>{
protected void reduce(Text k2,Iterable<Text> v2s,Context context){
Text text = new Text(“”);
try {
context.write(k2, text);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
};
}
}
//初始化参数
public static final String HOST_PATH=”hdfs://v:9000″;
//读取文件路径【需要手动创建】
public static final String INPUT_PATH=HOST_PATH+”/DDin”;
//输出文件路径
public static final String OUTPUT_PATH=HOST_PATH+”/DDout”;
//执行mapreduce任务驱动
public static void main (String[] args) throws Exception{
final Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(new URI(HOST_PATH), conf);
if(fs.exists(new Path(OUTPUT_PATH))){
fs.delete(new Path(OUTPUT_PATH), true);
}
//创建job对象
final Job job = new Job(conf);
//通知job文件输入路径
FileInputFormat.setInputPaths(job, INPUT_PATH);
//通知job文件输出路径
FileOutputFormat.setOutputPath(job, new Path(OUTPUT_PATH));
//通知job将输入文件解析成键值对的方式【默认可省略】
job.setInputFormatClass(TextInputFormat.class);
//调用自定义的Mapper函数
job.setMapperClass(DDMap.class);
//设置k2,v2类型,如果<k2,v2><k3,v3>类型一致,可以省略
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(Text.class);
//调用自定义的Reducer函数
job.setReducerClass(DDReduce.class);
//设置k3,v3类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
//通知job将<k3,v3>写入HDFS中的方式[默认值,可省略]
job.setOutputFormatClass(TextOutputFormat.class);
//执行job
job.waitForCompletion(true);
}
13611111111 b
13611111111 c
13711111111 b
13711111111 c
13722222222 b
13722222222 c
二、倒排索引(Inverted Index)
倒排索引是文档检索系统最常用的数据结构,广泛用于 全文搜索引擎 .
主要用来存储某个单词或词组在某个文档或一组文档中存储位置的映射,即提供了一种根据内容来查找文档的方式.
因为不是根据文档来确定文档内容,而是进行相反操作,所以叫做倒排索引.
实际应用中, 每个文档对应一个权重值,此用来指每个文档与搜索内容的相关度.
最常用使用词频作为权重值 ,即记录单词在文档中出现的次数.
更复杂的权重或还要记录单词在多个文档中出现过,以实现TF-IDF(Term Frequency-Inverse Document Frequency)算法,或考虑单词在文档中的位置等等.
File1:MapReduce is simple
File2:MapReduce is powerful is simple
File3:Hello MapReduce bye MapReduce
关注的信息:单词、文档URL、词频.
<key,value>类似:<”MapReduce” File1.txt 1>
<key,value>对只能有两个值,根据需求需要将 File1.txt 1合并作为value .
单词作为key的好处 :利用MR框架默认的排序,将同一文档的相同单词的词频组成列表,传递给Combine过程.
URL与词频合并为value的好处 :利用MR框架默认的HashPartitioner类完成Shuffle过程,将相同单词的所有记录发送给同一个Reducer处理.
通过一个Reduce无法同时完成词频统计与生成文档列表,需要添加Combine过程完成词频统计.
Combine过程将key值相同的value值累加,获取该key(单词)在本文档中的词频数.
将相同key的value组合成倒排索引文件所需的格式即可.
单词文件不宜过大,要保证每个文件对应一个split,否则由于Reduce过程没有进一步统计词频,最终结果可能会出现词频未统计完全的单词,可通过重写InputFormat类将每个文件作为一个split.还可利用复合键值对等实现包含更多信息的倒排索引.
package sort;
import java.io.IOException;
import java.net.URI;
import java.util.StringTokenizer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
public class Test {
// File1:MapReduce is simple
// File2:MapReduce is powerful is simple
// File3:Hello MapReduce bye MapReduce
// Hello File3.txt:1;
// MapReduce File3.txt:2;File1.txt:1;File2.txt:1;
// bye File3.txt:1;
// is File1.txt:1;File2.txt:2;
// powerful File2.txt:1;
// simple File2.txt:1;File1.txt:1;
static class IIMap extends Mapper<LongWritable, Text, Text, Text> {
private static Text key = new Text();// 单词和URL
private static Text value = new Text();// 词频
private FileSplit fileSplit;// Split对象
protected void map(LongWritable k1, Text v1, Context context) {
// 获取<k1,v1>对所属的FileSplit对象
FileSplit fileSplit = (FileSplit) context.getInputSplit();
StringTokenizer stringTokenizer = new StringTokenizer(v1.toString());
while (stringTokenizer.hasMoreTokens()) {
int indexOf = fileSplit.getPath().toString().indexOf("File");
key.set(stringTokenizer.nextToken() + ":"
+ fileSplit.getPath().toString().substring(indexOf));
value.set("1");
try {
context.write(key, value);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
}
// Combiner任务
static class IICombiner extends Reducer<Text, Text, Text, Text> {
private Text text = new Text();
protected void reduce(Text key, Iterable<Text> values, Context context) {
// 统计词频
int sum = 0;
for (Text value : values) {
sum += Integer.parseInt(value.toString());
}
int splitIndex = key.toString().indexOf(":");
// 重设value值 URL词频合并
text.set(key.toString().substring(splitIndex + 1) + ":" + sum);
// 重设key为单词
key.set(key.toString().substring(0, splitIndex));
try {
context.write(key, text);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
};
}
// Reducer任务
static class IIReduce extends Reducer<Text, Text, Text, Text> {
private Text v3 = new Text();
protected void reduce(Text k2, Iterable<Text> v2s, Context context) {
// 生成文档列表
String fileList = new String();
for (Text value : v2s) {
fileList += value.toString() + ";";
}
v3.set(fileList);
try {
context.write(k2, v3);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
};
}
// 初始化参数
public static final String HOST_PATH = "hdfs://v:9000";
// 读取文件路径【需要手动创建】
public static final String INPUT_PATH = HOST_PATH + "/IIin";
// 输出文件路径
public static final String OUTPUT_PATH = HOST_PATH + "/IIout";
// 执行mapreduce任务驱动
public static void main(String[] args) throws Exception {
final Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(new URI(HOST_PATH), conf);
if (fs.exists(new Path(OUTPUT_PATH))) {
fs.delete(new Path(OUTPUT_PATH), true);
}
// 创建job对象
final Job job = new Job(conf);
// 通知job文件输入路径
FileInputFormat.setInputPaths(job, INPUT_PATH);
// 通知job文件输出路径
FileOutputFormat.setOutputPath(job, new Path(OUTPUT_PATH));
// 通知job将输入文件解析成键值对的方式【默认可省略】
job.setInputFormatClass(TextInputFormat.class);
// 调用自定义的Mapper函数
job.setMapperClass(IIMap.class);
// 设置k2,v2类型,如果<k2,v2><k3,v3>类型一致,可以省略
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(Text.class);
job.setCombinerClass(IICombiner.class);
// 调用自定义的Reducer函数
job.setReducerClass(IIReduce.class);
// 设置k3,v3类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
// 通知job将<k3,v3>写入HDFS中的方式[默认值,可省略]
job.setOutputFormatClass(TextOutputFormat.class);
// 执行job
job.waitForCompletion(true);
}
}
Hello File3.txt:1;
MapReduce File3.txt:2;File1.txt:1;File2.txt:1;
bye File3.txt:1;
is File1.txt:1;File2.txt:2;
powerful File2.txt:1;
simple File2.txt:1;File1.txt:1;