实验环境:
ubuntu 18.04
hadoop 2.7.1
JDK 1.8
spark 2.3.3
scala 2.11.8
目录
一、实验原理
二、用MapReduce实现PageRank
三、用Spark实现PageRank
1. 什么是PageRank
PageRank是一种在搜索引擎中根据网页之间相互的链接关系计算网页排名的技术。
PageRank是Google用来标识网页的等级或重要性的一种方法。其级别从1到10级,PR值越高说明该网页越受欢迎(越重要)。
2. PageRank的基本设计思想和设计原则
被许多优质网页所链接的网页,多半也是优质网页。
一个网页要想拥有较高的PR值的条件:有很多网页链接到它;有高质量的网页链接到它
3. PageRank简化模型
可以把互联网上的各个网页之间的链接关系看成一个有向图。
对于任意网页Pi,它的PageRank值可表示为:
其中Bi为所有链接到网页i的网页集合,Lj为网页j的对外链接
4. 简化模型面临的问题
解决办法:
1.将无出度的节点递归地从图中去掉,待其他节点计算完毕后再加上
2.对无出度的节点添加一条边,指向那些指向它的顶点
5. PageRank随机浏览模型
假定一个上网者从一个随机的网页开始浏览,上网者不断点击当前网页的链接开始下一次浏览;但是,上网者最终厌倦了,开始了一个随机的网页。随机上网者用以上方式访问一个新网页的概率就等于这个网页的PageRank值。这种随机模型更加接近于用户的浏览行为。
则,R=H'R
其中R为列向量,代表PageRank值;H’代表转移矩阵;d代表阻尼因子,通常设为0.85;d即按照超链进行浏览的概率;1-d为随机跳转一个新网页的概率
1) 由于网页数目巨大,网页之间的连接关系的邻接矩阵是一个很大的稀疏矩阵,故采用邻接表来表示网页之间的连接关系
2) 随机浏览模型的PageRank公式:
3) 通过迭代计算得到所有节点的PageRank值。
1. GraphBuilder
目标:分析原始数据,建立各个网页之间的链接关系
Mapper:逐行分析原始数据, 输出
Reducer:直接输出
代码:
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
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.output.FileOutputFormat;
/**
* GraphBUilder:目标:分析原始数据,建立各个网页之间的链接关系
*/
public class GraphBuilder {
public static class GraphBuilderMapper extends Mapper {
/**
* 逐行分析原始数据, 输出
* key:每行的URL,网页标题
* value:PageRank的初始值PR_init和网页的出度链表
*/
public void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
String pagerank = "1.0\t";//初始化网页的PR值
String[] tuple = value.toString().split("\t");//将源数据中的网页和链接到的网页集合分开
Text page=new Text(tuple[0]);//网页
pagerank+=tuple[1];//链接到的网页集合
context.write(page, new Text(pagerank));
}
}
public static class GraphBuilderReducer extends Reducer {
/**
* 直接输出,不需要做任何处理
*/
public void reduce(Text key, Text value, Context context) throws IOException, InterruptedException {
context.write(key, value);
}
}
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
Job job1 = new Job(conf, "GraphBuilder");
job1.setJarByClass(GraphBuilder.class);
job1.setOutputKeyClass(Text.class);
job1.setOutputValueClass(Text.class);
job1.setMapperClass(GraphBuilderMapper.class);
job1.setReducerClass(GraphBuilderReducer.class);
FileInputFormat.addInputPath(job1, new Path(args[0]));
FileOutputFormat.setOutputPath(job1, new Path(args[1]));
job1.waitForCompletion(true);
}
}
2. PageRankIter
目标:迭代计算PageRank数值,直到满足结束条件(即迭代10次)。
Mapper:对于每一个键值对,对value中出度链表link_list中的每一个网页计算原网页对该网页贡献的pr值,输出键值对<当前网页,原网页\t原网页对当前网页贡献的pr值>。同时,由于计算pr值需要多次迭代更新,因此需要传递链接图的结构,在输出的对应value前加“|”来进行标记。
Reducer:统计每个链入网页得到的贡献pr值,将其相加代入公式中即可获得本次迭代过程得到的新pr值,输出键值对<网页,新pr值\t入链网页列表>;为进行下一次的迭代,同样也需要传递链接图信息。
代码:
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
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.output.FileOutputFormat;
/**
* PageRankIter:迭代计算PageRank数值,直到满足结束条件(即迭代10次)。
*/
public class PageRankIter {
private static final double damping = 0.85;
public static class PRIterMapper extends Mapper {
/**
* 输出键值对<当前网页,原网页\t原网页对当前网页贡献的pr值>
* 传递链接图结构
*/
public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String line = value.toString();
String[] tuple = line.split("\t");
String pageKey = tuple[0];//原网页
double pr = Double.parseDouble(tuple[1]);//把数字类型的字符串转换成double类型,得到原网页pr值
if (tuple.length > 2) {
String[] linkPages = tuple[2].split(",");
for (String linkPage : linkPages) {//对于所有的链入网页
String prValue =pageKey + "\t" + String.valueOf(pr / linkPages.length);
context.write(new Text(linkPage), new Text(prValue));//<链入网页,原网页\t原网页对链入网页贡献的pr值>
}
context.write(new Text(pageKey), new Text("|" + tuple[2]));//传递链接图结构,value前加“|”来进行标记
}
}
}
public static class PRIterReducer extends Reducer {
/**
* 输入键值对:<当前网页,原网页\t原网页对当前网页贡献的pr值>,链接图信息
* 输出键值对<网页,新pr值\t入链网页列表>
* 传递链接图信息
*/
public void reduce(Text key, Iterable values, Context context) throws IOException, InterruptedException {
String links = "";
double pagerank = 0;
for (Text value : values) {
String tmp = value.toString();
if (tmp.startsWith("|")) {//如果是传递连接图结构的键值对则存起来继续传递
links = "\t" + tmp.substring(tmp.indexOf("|") + 1);// index从0开始
continue;
}
String[] tuple = tmp.split("\t");
if (tuple.length > 1)
pagerank += Double.parseDouble(tuple[1]);//把所有原网页对链入网页贡献的pr值加起来
}
pagerank = (double) (1 - damping) + damping * pagerank;
context.write(new Text(key), new Text(String.valueOf(pagerank) + links));//输出<网页,新计算的pr值\t入链网页列表>
}
}
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
Job job2 = new Job(conf, "PageRankIter");
job2.setJarByClass(PageRankIter.class);
job2.setOutputKeyClass(Text.class);
job2.setOutputValueClass(Text.class);
job2.setMapperClass(PRIterMapper.class);
job2.setReducerClass(PRIterReducer.class);
FileInputFormat.addInputPath(job2, new Path(args[0]));
FileOutputFormat.setOutputPath(job2, new Path(args[1]));
job2.waitForCompletion(true);
}
}
3. PageRankViewer
目标:将最终结果按照PageRank值从大到小进行顺序输出。
Mapper:从前面最后一次迭代的结果中读出PageRank值和文件名,并以pr值作为key,网页名称作为value,输出键值对
Reducer:对Map的结果进行处理,保留10位小数,并按照要求输出结果。
代码:
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.DoubleWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;
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.output.FileOutputFormat;
/**
* PageRankViewer:将最终结果按照PageRank值从大到小进行顺序输出。
*/
public class PageRankViewer {
public static class PageRankViewerMapper extends Mapper {
/**
* 输出键值对
*/
private Text outPage = new Text();
private DoubleWritable outPr = new DoubleWritable();
public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String[] line = value.toString().split("\t");
String page = line[0];//原网页
double pr = Double.parseDouble(line[1]);//pr值
outPage.set(page);
outPr.set(pr);
context.write(outPr, outPage);
}
}
public static class PageRankViewerReducer extends Reducer {
/**
* 输入键值对:
* 输出键值对:<(URL,PageRank), null>
*/
public void reduce(DoubleWritable key, Iterable values, Context context) throws IOException, InterruptedException {
for (Text value : values) {
String ss = String.format("%.10f", key.get());//保留10位小数
context.write(new Text("("+value+","+ss+")"),null);//按照要求格式进行输出
}
}
}
public static class DescFloatComparator extends DoubleWritable.Comparator {
/**
* 重载key值的比较函数: 从大到小
*/
public float compare(WritableComparator a, WritableComparable b) {
return -super.compare(a, b);
}
public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2) {
return -super.compare(b1, s1, l1, b2, s2, l2);
}
}
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
Job job3 = new Job(conf, "PageRankViewer");
job3.setJarByClass(PageRankViewer.class);
job3.setMapperClass(PageRankViewerMapper.class);
job3.setSortComparatorClass(DescFloatComparator.class);
job3.setReducerClass(PageRankViewerReducer.class);
job3.setOutputKeyClass(DoubleWritable.class);
job3.setOutputValueClass(Text.class);
FileInputFormat.addInputPath(job3, new Path(args[0]));
FileOutputFormat.setOutputPath(job3, new Path(args[1]));
job3.waitForCompletion(true);
}
}
4. PageRank:将上述三个步骤串联起来进行多趟MapReduce。
把上一轮的输出目录设为下一轮的输入目录
代码:
public class PageRank {
private static int times = 10; // 设置迭代次数
public static void main(String[] args) throws Exception {
String[] forGB = { "", args[1] + "/Data0" };
forGB[0] = args[0];
GraphBuilder.main(forGB);
String[] forItr = { "", "" };
for (int i = 0; i < times; i++) {//把上一轮的输出目录设为下一轮的输入目录
forItr[0] = args[1] + "/Data" + i;
forItr[1] = args[1] + "/Data" + String.valueOf(i + 1);
PageRankIter.main(forItr);
}
String[] forRV = { args[1] + "/Data" + times, args[1] + "/FinalRank" };
PageRankViewer.main(forRV);
}
}
代码:
//PageRank.scala
import org.apache.spark.SparkConf
import org.apache.spark.SparkContext
object PageRank {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("Pagerank")//定义一个sparkConf , 提供Spark运行的各种参数,如程序名称、用户名称等
val sc = new SparkContext(conf)//创建Spark的运行环境,并将Spark运行的 参数传入Spark的运行环境中
val lines=sc.textFile("hdfs://master:9000/Experiment_3/.");//调用Spark的读文件函数,输出一个RDD类型的实例:lines。具体类型: RDD[String]
val links=lines.map{s=>
val parts=s.split("\t")
(parts(0),parts(1).split(","))//<原网页,链接网页链表>
}.cache()//存在缓存中
var ranks = links.mapValues(v => 1.0)//初始化网页pr值为1.0
//原RDD中的Key保持不变,与新的Value一起组成新的RDD中的元素
for(i <- 0 until 10){//迭代10次
val contributions = links.join(ranks).flatMap{//按照key进行内连接
case(pageId, (links, rank)) => //对于每个网页获得其对应的pr值和链入表
links.map(link => (link, rank / links.size)) //对链入表的每一个网页计算它的贡献值
}
//flatMap:对集合中每个元素进行操作然后再扁平化。
//<原网页,(链入网页,贡献pr值)>
ranks = contributions
.reduceByKey((x,y) => x+y)//将贡献pr值加起来
.mapValues(v => (0.15 + 0.85*v))//计算新的pr值
}
//按照value递减次序排序保留10位小数且输出为一个文件
ranks.sortBy(_._2,false).mapValues(v=>v.formatted("%.10f").toString()).coalesce(1,true).saveAsTextFile("/user/201600130034/Experiment_3_spark");
/* _._2等价于t => t._2
* map(_._n)表示任意元组tuple对象,后面的数字n表示取第几个数.(n>=1的整数)
* sortBy
* 第一个参数是一个函数,该函数的也有一个带T泛型的参数,返回类型和RDD中元素的类型是一致的;
* 第二个参数是ascending,从字面的意思大家应该可以猜到,是的,这参数决定排序后RDD中的元素是升序还是降序,默认是true,也就是升序;
* 第三个参数是numPartitions,该参数决定排序后的RDD的分区个数,默认排序后的分区个数和排序之前的个数相等,即为this.partitions.size。
* */
/*
* def coalesce(numPartitions:Int, shuffle:Boolean = false)
* 返回一个新的RDD,且该RDD的分区个数等于numPartitions个数。如果shuffle设置为true,则会进行shuffle。
* */
}
}