(一)Spark常用算子:map,mapPartition,mapPartitionIndex,flatMap,filter

首先来看下spark的wordcount的top5

package org.example.spark

import java.security.MessageDigest

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

object WordCount {
  import org.apache.spark.rdd.RDD
  def main(args: Array[String]): Unit = {

    // 这里的下划线"_"是占位符,代表数据文件的根目录
    val file: String = "D:\\testCode\\words.txt"
    // 读取文件内容
    //设置spark的配置文件信息
    val sparkConf: SparkConf = new SparkConf().setAppName("WordCount")
        .setMaster("local[2]")
    //构建sparkcontext上下文对象,它是程序的入口,所有计算的源头
    val sc: SparkContext = new SparkContext(sparkConf)
    //读取文件
    val lineRDD: RDD[String] = sc.textFile(file)
    // 以行为单位做分词val
    val words: RDD[String] = lineRDD.flatMap(line => line.split(" "))
    // 过滤掉空字符串
    val cleanWordRDD: RDD[String] = words.filter(word => !word.equals(""))
    // 把RDD元素转换为(Key,Value)的形式
    val kvRDD: RDD[(String, Int)] = cleanWordRDD.map(word => (word, 1))
    // 按照单词做分组计数
    val wordCounts: RDD[(String, Int)] = kvRDD.reduceByKey((x, y) => x + y)
    // 打印词频最高的5个词汇
    println(wordCounts.count())
    /*6*/
    //反转kv然后sortByKey排序取出前五  
    val tuples = wordCounts.map { 
      case (k, v) => (v, k) }.sortByKey(false).take(5)
      println(tuples.toString)
      tuples.foreach{ line=>
      println("word="+line._2+" ,num="+line._1)   /*top5*/
    }
  }
}

结果  

map:以元素为粒度的数据转换

 从原来的对单词计数,改为对单词的哈希值计数
  /*对cleanWordRDD 从原来的对单词计数,改为对单词的哈希值计数*/
  val hashkvRDD: RDD[(String, Int)] = cleanWordRDD.map{ word =>
      // 获取MD5对象实例
      val md5 = MessageDigest.getInstance("MD5")
      // 使用MD5计算哈希值
      val hash = md5.digest(word.getBytes).mkString // 返回哈希值与数字1的Pair
       (hash, 1)
  }
  val hashwordCounts: RDD[(String, Int)] = hashkvRDD.reduceByKey((x, y) => x + y)
  hashwordCounts.foreach{ line=>
         println("word="+line._1+" ,num="+line._2)
  }

mapPartitions:以数据分区为粒度的数据转换

        由于 map(f) 是以元素为单元做转换的,那么对于 RDD 中的每一条数据记录,我们都需要实例化一个 MessageDigest 对象来计算这个元素的哈希值。 在工业级生产系统中,一个 RDD 动辄包含上百万甚至是上亿级别的数据记录, 如果处理每条记录都需要事先创建 MessageDigest,那么实例化对象的开销就会聚沙成塔, 不知不觉地成为影响执行效率的罪魁祸首。

        那么问题来了 ,有没有什么办法,能够让 Spark 在更粗的数据粒度上去处理数据呢?还真有, mapPartitions 和 mapPartitionsWithIndex 这对“孪生兄弟”就是用来解决类似的问题。 相比 mapPartitions,mapPartitionsWithIndex 仅仅多出了一个数据分区索引, 因此接下来我们把重点放在 mapPartitions 上面

       解释:以数据分区为单位,实例化对象的操作只需要执行一次,而同一个数据分区中所有的数据记录,都可以共享该 MD5 对象,从而完成单词到哈希值的转换

      以数据分区为单位,mapPartitions 只需实例化一次 MD5 对象,而 map 算子却需要实例化多次,具体的次数则由分区内数据记录的数量来决定

// 映射的过程中需要频繁创建额外的对象,使用mapPartitions要比map高效的多

复用一个partition内的对象

  val kvpartitionRDD: RDD[(String, Int)] = cleanWordRDD.mapPartitions(
      partition => {
      // 注意!这里是以数据分区为粒度,获取MD5对象实例
      val md5 = MessageDigest.getInstance("MD5")
      val newPartition = partition.map( word => {
        // 在处理每一条数据记录的时候,可以复用同一个Partition内的MD5对象
        (md5.digest(word.getBytes()).mkString,1)
      })
      newPartition
  })
  val hashpartitionCounts: RDD[(String, Int)] = kvpartitionRDD.reduceByKey((x, y) => x + y)
  hashpartitionCounts.foreach{ line=>
      println("word="+line._1+" ,num="+line._2)
  }

 结果为

 

mapPartitionsWithIndex:分区为粒度操作,还会有分区索引                 

      mapPartitionsWithIndex,不过提供了两个参数,第一个参数为分区的索引,索引默认0开始,当你的业务逻辑中

    val rdd = sc.makeRDD(List(12,3,4,5,6,4))
    //分成两个区
    val newrdd = rdd.repartition(2)

    val rdd2 = newrdd.mapPartitionsWithIndex((i:Int,it:Iterator[Int])=> {
      it.map(t=> s"p=$i,v=$t")
    })
    rdd2.collect().toBuffer.foreach(println)

需要使用到分区编号的时候,不妨考虑使用这个算子来实现代码

flatMap:从元素到集合、再从集合到元素

       flatMap 映射函数 f 的类型,是(元素) => (集合),即元素到集合(如数组、列表等)

       假设,再次改变 Word Count 的计算逻辑,由原来统计单词的计数,改为统计相邻单词共现的次数,如下图所示

 实现代码如下


// 读取文件内容
val lineRDD: RDD[String] = _ // 请参考第一讲获取完整代码
// 以行为单位提取相邻单词
val wordPairRDD: RDD[String] = lineRDD.flatMap( line => {
  // 将行转换为单词数组
  val words: Array[String] = line.split(" ")
  // 将单个单词数组,转换为相邻单词数组
  for (i <- 0 until words.length - 1) yield words(i) + "-" + words(i+1)
})

结果为

看看wordcount中的flatMap作用:map会产生元素的一对一映射,flatMap会产生元素的一对多映射

 //读取文件
    val lineRDD: RDD[String] = sc.textFile(file)
    lineRDD.foreach(println)
    // 以行为单位做分词val
    val words: RDD[String] = lineRDD.flatMap(line => line.split(" "))
    words.foreach(println)

 读取的文件内容输出如下

 flatMap后输出如下,根据函数内的split产生了一对多映射

filter:过滤 RDD

        filter,顾名思义,这个算子的作用,是对 RDD 进行过滤。就像是 map 算子依赖其映射函数一样,filter 算子也需要借助一个判定函数 f,才能实现对 RDD 的过滤转换。所谓判定函数,它指的是类型为(RDD 元素类型) => (Boolean)的函数。

        可以看到,判定函数 f 的形参类型,必须与 RDD 的元素类型保持一致,而 f 的返回结果,只能是 True 或者 False。在任何一个 RDD 之上调用 filter(f),其作用是保留 RDD 中满足 f(也就是 f 返回 True)的数据元素,而过滤掉不满足 f(也就是 f 返回 False)的数据元素。

       老规矩,我们还是结合示例来讲解 filter 算子与判定函数 f。在上面 flatMap 例子的最后,我们得到了元素为相邻词汇对的 wordPairRDD,它包含的是像“Spark-is”、“is-cool”这样的字符串。为了仅保留有意义的词对元素,我们希望结合标点符号列表,对 wordPairRDD 进行过滤。

       例如,我们希望过滤掉像“Spark-&”、“|-data”这样的词对。掌握了 filter 算子的用法之后,要实现这样的过滤逻辑,我相信你很快就能写出如下的代码实现:


// 定义特殊字符列表
val list: List[String] = List("&", "|", "#", "^", "@")
 
// 定义判定函数f
def f(s: String): Boolean = {
val words: Array[String] = s.split("-")
val b1: Boolean = list.contains(words(0))
val b2: Boolean = list.contains(words(1))
return !b1 && !b2 // 返回不在特殊字符列表中的词汇对
}
 
// 使用filter(f)对RDD进行过滤
val cleanedPairRDD: RDD[String] = wordPairRDD.filter(f)

猜你喜欢

转载自blog.csdn.net/someInNeed/article/details/121406796
今日推荐