action 算子案例实操
计算出每个科目最受欢迎的/点击率最高的前两位老师的视频
1 数据准备
-
http://javaee.bilibili.com/laoyang
-
http://javaee.bilibili.com/laoyang
-
http://php.bilibili.com/laoli
-
http://php.bilibili.com/laoliu
-
http://php.bilibili.com/laoli
-
http://php.bilibili.com/laoli
-
http://bigdata.bilibili.com/laozhang
-
http://bigdata.bilibili.com/laozhang
-
http://bigdata.bilibili.com/laozhao
-
http://bigdata.bilibili.com/laozhao
-
.........
代码实现
2.1 方案一
1) 创建 SparkContext ,获得 sc ,创建原始的 RDD ,获得数据 ,对数据进行切割处理 ,得到想要的字段 ,然后进行组装 ((科目,老师) , 1) ,科目和老师关联在一起 ,作为key为一组 ,1 作为value ,然后调用reduceByKey方法 ,根据key进行分组 ,value进行聚合,得到一个新的RDD(科目和老师 ,点击总次数);
2) 然后根据新RDD的(科目和老师)进行分组和排序即得到结果(groupBy和sortBy和take(2)--这些方法会产生大量的shuffle ,并不是最好的方法)
object FavTeacher01 {
def main(args: Array[String]): Unit = {
--让运行变得更灵活 ,根据main方法中插入的参数true 或者false 来决定是否在本地运行
val isLocal: Boolean = args(0).toBoolean
val conf = new SparkConf().setAppName(this.getClass.getSimpleName)
--如果main 方法index为0 的位置传入的参数是true ,就在本地运行
if(isLocal){
conf.setMaster("local[*]")
}
val sc = new SparkContext(conf)
--创建原始的RDD
val lines: RDD[String] = sc.textFile(args(1))
--对一行行的数据进行整理 ,数据为 : http://bigdata.bilibili.com/laozhang
--切割,取出需要的元素然后组装 ,再然后根据key ,对 value进行聚合
val reduced: RDD[((String, String), Int)] = lines.map(m => {
--切割数据时因为有的是 // ,有的是 / ,所以处理的时候用 /+ ,表示可以对单 / ,也可以对双 / 进行切割
val strings = m.split("/+")
val url = strings(1)
val teacher = strings(2)
val subject = url.split("[.]")(0)
((subject, teacher), 1)
}).reduceByKey(_ + _)
--对聚合的元素进行排序 ,因为需求是计算出每一个科目点击率最高 ,最受学生欢迎的前两个老师
--如果直接调用 sortBy 方法是对全局进行排序 ,而非单个科目排序 ,所以不能使用sortBy
--所以首先先按照学科进行分组
val grouped: RDD[(String, Iterable[((String, String), Int)])] = reduced.groupBy(_._1._1)
--然后根据value进行数据的处理 ,这种方法会产生大量的shuffle
val subAndTeacherCoun: RDD[(String, List[((String, String), Int)])] = grouped.mapValues(it => it.toList.sortBy(-_._2).take(2))
--将结果打印出来
println(subAndTeacherCoun.collect().toBuffer)
--结果为 :ArrayBuffer((javaee,List(((javaee,laoyang),210000), ((javaee,xiaoxu),140000))),
-- (php,List(((php,laoli),60000), ((php,laoliu),20000))),
-- (bigdata,List(((bigdata,laozhao),370000), ((bigdata,laoduan),140000))))
sc.stop()
}
}
方案二
1) 创建 SparkContext ,获得 sc ,创建原始的 RDD ,获得数据 ,对数据进行切割处理 ,得到想要的字段 ,然后进行组装 ((科目,老师) , 1) ,科目和老师关联在一起 ,作为key为一组 ,1 作为value ,然后调用reduceByKey方法 ,根据key进行分组 ,value进行聚合,得到一个新的RDD(科目和老师 ,点击总次数);
2) 定义一个数组 ,里面的元素是所有的科目 ,for循环遍历数组 ,从新的RDD(科目和老师 ,点击总次数),然后根据(科目和老师)这个key ,将科目都过滤出来 ,每一个科目都是分开的(分组的功能) ; (这里没有使用groupBy进行分组)
3) 自定义排序规则或者是导入隐式转换的排序规则将以上结果进行排序 ,然后调用takeOrdered(2)方法就可以得到最终结果.(也没有调用sortBy方法进行排序)
object FavTeacher02 {
def main(args: Array[String]): Unit = {
val isLocal = args(0).toBoolean
val subjects = Array("bigdata","javaee","php")
val conf = new SparkConf().setAppName(this.getClass.getSimpleName)
if(isLocal){
conf.setMaster("local[*]") -- * 代表多个进程
}
--创建 sparkcontext
val sc = new SparkContext(conf)
val lines: RDD[String] = sc.textFile(args(1))
--对读取的一行行数据进行切割处理 ,根据需求拿到想要的元素并组装 ,然后进行全局聚合
val reduced: RDD[((String, String), Int)] = lines.map(m => {
val strings = m.split("/+")
val url = strings(1)
val subject = url.split("[.]")(0)
val teachers = strings(2)
((subject, teachers), 1)
}).reduceByKey(_ + _)
--需求是计算出每科最受学生欢迎的老师 ,所以需要对每一个科目进行排序处理
--先将一个学科的数据进行过滤
for(sb <- subjects){
--第一轮先将其中一个科目过滤出来,然后对这个科目进行排序取值
val subjRDD: RDD[((String, String), Int)] = reduced.filter(_._1._1.equals(sb))
-----------取出一个组里面最大的两个元素--takeOrdered --降序取值--------------
---第一种方法:直接在后面new 一个Ordering,重写排序规则(匿名内部类 ,参考java 的形式)----
val res: Array[((String, String), Int)] = subjRDD.takeOrdered(2)(new Ordering[((String, String), Int)] {
override def compare(x: ((String, String), Int), y: ((String, String), Int)): Int = {
-(x._2 - y._2)
}
})
---第二种方法 : 自定义一个隐式参数 ,重写里面的排序规则,降序排序------------
implicit val ord:Ordering[((String, String), Int)] = Ordering[Int].on[((String, String), Int)](t => -t._2)
--然后去降序排序后的前2个元素 隐式参数可以导入,也可以不导入,如果不导入,代码会自己去上下文中查找
val res = subjRDD.takeOrdered(2)(ord)
println(res.toBuffer)
--结果为:ArrayBuffer(((bigdata,laozhang),60000), ((bigdata,laoduan),140000))
--ArrayBuffer(((javaee,xiaoxu),140000), ((javaee,laoyang),210000))
--ArrayBuffer(((php,laoliu),20000), ((php,laoli),60000))
}
sc.stop()
}
}
2.3 方案三
1) 创建 SparkContext ,获得 sc ,创建原始的 RDD ,获得数据 ,对数据进行切割处理 ,得到想要的字段 ,然后进行组装 ((科目,老师) , 1) ,科目和老师关联在一起 ,作为key为一组 ,1 作为value ,然后调用reduceByKey方法 ,根据key进行分组 ,value进行聚合,得到一个新的RDD(科目和老师 ,点击总次数);
2) 定义一个分区器自定义分区规则 ,需要知道有多少科目 ,然后将科目放到自定义的分区器中得到每个科目对应的唯一的分区号(一个科目一个分区号) ,然后新的RDD(科目和老师 ,点击总次数)就根据得到的分区号将对应的科目放到对应的分区里面;
3) 从上得到很多个分区, 一个区里面装的都是一个类型的科目的数据 ,有多少个科目就有多少个分区 ,一个分区一个分区的取值 ,导入隐式转换排序规则(降序排序) ,定义一个TreeSet集合 ,将排序的结果放到TreeSet集合里面 ;
4) 取出一个区的数据 ,一个区一个迭代器,然后一条一条的取出来放到 TreeSet集合里面 ,根据隐式转换的降序规则 ,对 TreeSet集合里面的数据进行降序排序 ,只要集合里面的元素的个数大于2 ,就将集合里面三个元素中最小的那个移除掉,直到一个区里面的元素都取出比对完,最后留在 treeSet 集合中的两个元素就是同一个科目点击率最高的两位老师 .
object FavTeacher03 {
def main(args: Array[String]): Unit = {
val isLocal = args(0).toBoolean ----main方法参数一 : true
--创建SPARKCONF
val conf = new SparkConf().setAppName(this.getClass.getSimpleName)
--判断传入的参数是true还是false,是否需要在本地运行
if(isLocal){
conf.setMaster("local[*]")
}
--创建 sparkcontext
val sc = new SparkContext(conf)
--创建原始RDD
val lines: RDD[String] = sc.textFile(args(1)) ----main方法参数二 : 待处理文件输入路径
--对读取的行数据进行处理,先按一定规则进行切割,得到想要的数据,然后将其进行组装,然后按照key进行分组聚合
val reduced: RDD[((String, String), Int)] = lines.map(line => {
val strings = line.split("/+")
val url = strings(1)
val subject = url.split("\\.")(0)
val teacher = strings(2)
((subject, teacher), 1)
}).reduceByKey(_ + _)
val topN = args(2).toInt ----main方法参数三 : 2
--先计算数据有多少个学科,触发一次Action ,将数据计算好收集到Driver端
--((String, String), Int) 算出来有多少个科目
val subjects = reduced.map(_._1._1).distinct().collect()
--针对科目创建一个分区器, 确保每个科目划分到自己的分区去
val subjectPartitioner = new SubjectPatitionner(subjects)
--按照指定的分区器进行分区 ((String, String), Int)
--得出的结果数据,就是按照科目划分好了的数据.也就是php,java等的数据都在各自的一个区里面
val partitionedRDD: RDD[((String, String), Int)] = reduced.partitionBy(subjectPartitioner)
--排序,每个分区内的数据就是对应科目的数据,没有其他科目的数据 ((subject, teacher), 1)
val sorted: RDD[((String, String), Int)] = partitionedRDD.mapPartitions(it => {
--定义一个可排序的集合
implicit val ord = Ordering[Int].on[((String, String), Int)](t => -t._2)
val sorter: mutable.Set[((String, String), Int)] = new mutable.TreeSet[((String, String), Int)](ord)
--将迭代器中的每一条数据一次遍历处理
it.foreach(t => {
sorter += t
if (sorter.size > topN) {
--移除小的
sorter -= sorter.last
}
})
sorter.iterator
})
println(sorted.collect().toBuffer)
/**
* 结果为 : ArrayBuffer(((javaee,laoyang),210000), ((javaee,xiaoxu),140000),
* ((php,laoli),60000), ((php,laoliu),20000),
* ((bigdata,laozhao),370000), ((bigdata,laoduan),140000))
*/
sc.stop()
}
}
--自定义分区规则
class SubjectPatitionner(val subjects: Array[String]) extends Partitioner{
--定义的规则,一个科目对应唯一的编号
--定义一个map集合 ,装科目和其对应的唯一的编号
val subjectToIndex = new mutable.HashMap[String, Int]()
--初始编号为0
var index = 0
for(sb <- subjects){
--循环一次,将科目和对应的编号装到map集合里面
subjectToIndex(sb) = index
--然后编号加1,然后再循环
index += 1
}
--根据科目的数量,决定分区的数量
override def numPartitions: Int = subjects.size
--根据科目,到map集合里面得到对应的唯一的分区编号
override def getPartition(key: Any): Int = {
--参数key 是 reduced: RDD[((String, String), Int)]中的key ,key的1号元素就是科目
val sb = key.asInstanceOf[(String, String)]._1
--根据科目,取出其对应的编号
subjectToIndex(sb)
}
}