spark分组求topN

本文详细介绍了在Spark中实现分组求TopN的多种方法,包括:将数据加载到内存中去TopN、使用自定义分区器取TopN、保存中间结果数据取TopN以及使用TreeSet取TopN。通过实例代码展示了如何处理大规模数据集,并讨论了不同方法的适用场景和优缺点。
摘要由CSDN通过智能技术生成

目录

1. 将数据加载到内存中去TopN

将数据加载到内存和本地中去取TopN

使用自定义分区器取TopN

保存中间结果数据取TopN

使用TreeSet取TopN

使用TreeSet取TopN方式二


在mr、hive中投处理过的操作,分组的topn

​    比如要从10个文件,每个文件都有100w个数字,找出最大的10数字。

​    比如有很多部分,比如研发部、设计部、市场部、行政部等等,要求找出每个部分年龄最小的三个小姐姐。

​    这就是分组TopN的问题。

object _03SparkGroupTopNOps {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setAppName(s"${_01SparkSortOps.getClass.getSimpleName}")
.setMaster(“local[2]”)
val sc = new SparkContext(conf)

    val lines = sc.textFile("file:///E:/data/spark/topn.txt")

    val course2Info:RDD[(String, String)] = lines.map(line => {
        val fields = line.split("\\s+")
        val course = fields(0)
        val name = fields(1)
        val score = fields(2)
        (course, s"$name|$score")
    })
    //就需要将每门课程的所有信息弄到一起才能排序
    val course2Infos:RDD[(String, Iterable[String])] = course2Info.groupByKey()
    /*
        排序
           k,是科目
           v:该科目对应的所有的成绩信息
           经过排序之后返回三个人的成绩信息,还是一个集合
           [k, Iterable[String]] --> [k, Iterable[String]]只不过后面Iterable[String]的size为3
           还是one-2-many的操作
           one-2-one--->map
     */
    val top3:RDD[(String, mutable.TreeSet[String])] = course2Infos.map{case (course, infos) => {
        var top3Infos = mutable.TreeSet[String]()(new Ordering[String](){
            //name|score
            override def compare(x: String, y: String) = {
                val xScore = x.substring(x.indexOf("|") + 1).toInt
                val yScore = y.substring(y.indexOf("|") + 1).toInt
                var ret = xScore.compareTo(yScore)
                if(ret == 0) {
                    1
                } else {
                    ret
                }
            }
        })

        //排序的操作 top3Infos是有序的,但是最后只要3个
// top3Infos.dropRight(top3Infos.size - 3)
for(info <- infos) {
top3Infos.add(info)
if(top3Infos.size > 2) {
top3Infos = top3Infos.dropRight(1)
}
}
(course, top3Infos)
}}

1. 将数据加载到内存中去TopN

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
 * 将数据加载到内存中进项排序取topn
 */
object Teacher01 {
  def main(args: Array[String]): Unit = {
    // 创建配置参数对象,进行参数配置
    val conf = new SparkConf().setAppName(this.getClass.getSimpleName)
    var isLocal:Boolean = args(0).toBoolean
    if(isLocal){
      conf.setMaster("local[*]")
    }
    // 创建Spark入口
    val sc = new SparkContext(conf)
    // 读取文件
    val lines: RDD[String] = sc.textFile(args(1))
    // 对数据进行切分聚合处理
    val subTeacherAndOne: RDD[((String, String), Int)] = lines.map(line => {
      val strArr = line.split("/")
      val url: String = strArr(2)
      val teacher: String = strArr(3)
      val subject = url.substring(0, url.indexOf("."))
      ((subject, teacher), 1)
    }).reduceByKey(_ + _)
    // 因为SortBy是全局进行排序,所以不能直接使用,先要进行进行分组,在进行组内排序
    val grouped: RDD[(String, Iterable[((String, String), Int)])] = subTeacherAndOne.groupBy(_._1._1)
    // 组内进行排序
    val res: RDD[(String, List[(String, Int)])] = grouped.mapValues(it => {
      // 将数据加载到内存中
      it.toList.sortBy(-_._2).take(2).map(t => (t._1._2, t._2))
    })
    // 将结果收集到客户端,打印
    val buffer = res.collect().toBuffer
    println(buffer)
    // 释放资源
    sc.stop()
  }
}

将数据加载到内存和本地中去取TopN

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
 * 代码优化,如果将数据全部加载到内存中进行处理,如果数据量过大,可能会导致
 * 内存溢出,数据丢失,使用RDD的sortBy方法进行排序可以将数据加载到内存和
 * 磁盘中
 */
object Teacher02 {
  def main(args: Array[String]): Unit = {
    // 创建配置参数对象,进行参数配置
    val conf = new SparkConf().setAppName(this.getClass.getSimpleName)
    var isLocal:Boolean = args(0).toBoolean
    if(isLocal){
      conf.setMaster("local[*]")
    }
    // 创建Spark入口
    val sc = new SparkContext(conf)
    // 读取文件
    val lines: RDD[String] = sc.textFile(args(1))
    // 对数据进行切分聚合处理
    val subTeacherAndOne: RDD[((String, String), Int)] = lines.map(line => {
      val strArr = line.split("/")
      val url: String = strArr(2)
      val teacher: String = strArr(3)
      val subject = url.substring(0, url.indexOf("."))
      ((subject, teacher), 1)
    }).reduceByKey(_ + _)
    // 创建学科数组用于分组排序
    val subjects = Array("bigdata", "javaee", "php")
    // 对应数组中的学科学科进行过滤排序(等同于分组排序)
    for (sub <- subjects){
      // 按照学科将数据进行过滤出来
      val filtered: RDD[((String, String), Int)] = subTeacherAndOne.filter(t => t._1._1.equals(sub))
      // RDD的takeOrdered方法,可以设置排序规则,并指定取前几个值
      val res: Array[((String, String), Int)] = filtered.takeOrdered(2)((a, b) => a._2 - b._2)
      // 打印结果
      println(res.toBuffer)
    }
    sc.stop()
  }
}

使用自定义分区器取TopN

import org.apache.spark.{Partitioner, SparkConf, SparkContext}
import org.apache.spark.rdd.RDD

import scala.collection.mutable

/**
 * 使用自定义分区器取topn,可以将较大的数据按照分区加载到内存中进行排序取topn,这样减小了
 * 内存的压力,同时也加快了数据,但是如果某个分区中的数据量过大的话,这个方法就不适用了
 */
object Teacher03 {
  def main(args: Array[String]): Unit = {
    // 创建配置参数对象,进行参数配置
    val conf = new SparkConf().setAppName(this.getClass.getSimpleName)
    var isLocal:Boolean = args(0).toBoolean
    if(isLocal){
      conf.setMaster("local[*]")
    }
    // 创建Spark入口
    val sc = new SparkContext(conf)
    // 读取文件
    val lines: RDD[String] = sc.textFile(args(1))
    // 对数据进行切分聚合处理
    val subTeacherAndOne: RDD[((String, String), Int)] = lines.map(line => {
      val strArr = line.split("/")
      val url: String = strArr(2)
      val teacher: String = strArr(3)
      val subject = url.substring(0, url.indexOf("."))
      ((subject, teacher), 1)
    }).reduceByKey(_ + _)
    // 将学科信息收集到客户端
    val subjects: Array[String] = subTeacherAndOne.map(_._1._1).distinct().collect()
    // 根据自定义的分区器进行分区,有几个学科就有几个分区
    val partitioned: RDD[((String, String), Int)] = subTeacherAndOne.partitionBy(new SubjectPartitioner(subjects))
    // 使用mapPartition进行区内排序取topn
    val res: RDD[((String, String), Int)] = partitioned.mapPartitions(t => {
      val it = t.toList.sortBy(_._2).take(2).iterator
      it
    })
    // 打印结果
    println(res.collect().toBuffer)
    // 释放资源
    sc.stop()
  }
}

/**
 * 自定义分区器在创建好对象后,task会去执行里面的方法,getPartition(key: Any)这个方法
 * 中的key,task会根据自己设定的key.asInstanceOf的类型,进行匹配对应的类型
 * @param subjects
 */
class SubjectPartitioner(subjects : Array[String]) extends Partitioner {
  // 设置分区规则,按照学科指定分区
  private val ruels = new mutable.HashMap[String, Int]()
  var i:Int = 0
  // 将指定每个学科归属哪个分区
  for (sub <- subjects){
    ruels(sub) = i
    i += 1
  }

  // 重写指定分区数量的方法
  override def numPartitions: Int = subjects.length

  // 重写获取分区的方法
  override def getPartition(key: Any): Int = {
    val subject: String = key.asInstanceOf[(String, String)]._1
    ruels(subject)
  }
}

保存中间结果数据取TopN

import org.apache.spark.rdd.RDD
import org.apache.spark.{Partitioner, SparkConf, SparkContext}

import scala.collection.mutable

/**
 *使用自定义分区器取topn,上一种写法存在着不足,因为如果中间结果数据比较珍贵,并且是经过了多天的计算得出的结果
 *那么在计算过程中,如果计算机出了问题,那就会导致多天的计算毁于一旦,所以可以将中间结果数据保存到hfds中,这样
 *可以增加数据的安全性
 */
object TeacherTopN4 {

  def main(args: Array[String]): Unit = {

    val isLocal = args(0).toBoolean


    val conf = new SparkConf().setAppName(this.getClass.getSimpleName)

    if(isLocal) {
      conf.setMaster("local[*]")
    }

    val sc = new SparkContext(conf)

    //指定以后从哪里读取数据创建RDD
    val lines = sc.textFile(args(1))

    //对数据进行切分整理
    val subjectTeacherAndOne: RDD[((String, String), Int)] = lines.map(line => {
      val fields = line.split("/")
      val url = fields(2)
      val teacher = fields(3)
      val subject = url.substring(0, url.indexOf("."))
      ((subject, teacher), 1)
    })

    //先触发一次Action,将学科的数量计算出来,将计算好的结果收集到Driver端
    val subjects: Array[String] = subjectTeacherAndOne.map(_._1._1).distinct().collect()

    val reduced = subjectTeacherAndOne.reduceByKey(new SubjectPartitioner2(subjects), _ + _)
    // 将数据保存到hdfs中
    reduced.saveAsTextFile(args(2))

    val res: RDD[((String, String), Int)] = reduced.mapPartitions(it => {
      it.toList.sortBy(-_._2).take(2).iterator
    })
    println(res.collect().toBuffer)

    sc.stop()
  }

}

class SubjectPartitioner2(val subjects: Array[String]) extends Partitioner {

  //学科名称 -> 分区编号
  val rules = new mutable.HashMap[String, Int]()
  var i = 0
  for (sub <- subjects) {
    rules(sub) = i
    i += 1
  }

  override def numPartitions: Int = subjects.length

  override def getPartition(key: Any): Int = {
    val subject = key.asInstanceOf[(String, String)]._1
    rules(subject)
  }
}

使用TreeSet取TopN

import org.apache.spark.rdd.RDD
import org.apache.spark.{Partitioner, SparkConf, SparkContext}

import scala.collection.mutable

object TeacherTopN5 {

  def main(args: Array[String]): Unit = {

    val isLocal = args(0).toBoolean

    val conf = new SparkConf().setAppName(this.getClass.getSimpleName)

    if(isLocal) {
      conf.setMaster("local[*]")
    }

    val sc = new SparkContext(conf)

    //指定以后从哪里读取数据创建RDD
    val lines = sc.textFile(args(1))

    //
    val topN = args(2).toInt

    //对数据进行切分整理
    val subjectTeacherAndCount = lines.map(line => {
      val fields = line.split("/")
      val url = fields(2)
      val teacher = fields(3)
      val subject = url.substring(0, url.indexOf("."))
      ((subject, teacher), 1)
    }).reduceByKey(_+_)

    //按照学科进行分组
    val grouped: RDD[(String, Iterable[((String, String), Int)])] = subjectTeacherAndCount.groupBy(_._1._1)

    //对迭代器中的数据进行排序
    val res = grouped.mapValues(it => {
        implicit val rules = Ordering[Int].on[((String, String), Int)](t => -t._2)
      val sorter = new mutable.TreeSet[((String, String), Int)]()

      it.foreach(e => {
        //从迭代器中取出来放入到TreeSet
        sorter.add(e)
        if(sorter.size > topN) {
          //移除最小的
          val smallest = sorter.last
          sorter -= smallest
        }
      })
      //保存到treeSet中最终的数据就是想要的结果
      sorter
    })
    println(res.collect().toBuffer)
    sc.stop()
  }
}

使用TreeSet取TopN方式二

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

import scala.collection.mutable

object TeacherTopN6 {

  def main(args: Array[String]): Unit = {

    val isLocal = args(0).toBoolean

    val conf = new SparkConf().setAppName(this.getClass.getSimpleName)

    if(isLocal) {
      conf.setMaster("local[*]")
    }

    val sc = new SparkContext(conf)

    //指定以后从哪里读取数据创建RDD
    val lines = sc.textFile(args(1))

    val topN = args(2).toInt

    //对数据进行切分整理
    val subjectTeacherAndCount = lines.map(line => {
      val fields = line.split("/")
      val url = fields(2)
      val teacher = fields(3)
      val subject = url.substring(0, url.indexOf("."))
      ((subject, teacher), 1)
    }).reduceByKey(_+_)

    //按照学科进行分组
    val grouped: RDD[(String, Iterable[((String, String), Int)])] = subjectTeacherAndCount.groupBy(_._1._1)

    //对迭代器中的数据进行排序
    val res = grouped.mapValues(it => {

      val sorter = new mutable.TreeSet[OrderingBean]()

      it.foreach(e => {
        //从迭代器中取出来放入到TreeSet
        sorter += new OrderingBean(e._1._1, e._1._2, e._2)
        if(sorter.size > topN) {
          //移除最小的
          val smallest = sorter.last
          sorter -= smallest
        }
      })
      //保存到treeSet中最终的数据就是想要的结果
      sorter
    })
    println(res.collect().toBuffer)
    sc.stop()
  }
}

以上就是spark中取TopN的多种方式,每个方式都各有特点,具体使用哪一个方式要根据实际情况进行使用,比如说数据量的大小,或者是否对运算速度有明确的要求,具体还是要根据场景进行使用的.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值