spark--IP经纬度热力图分析-★★★★★

需求

  • 在互联网中,我们经常会见到城市热点图这样的报表数据,例如在百度统计中,会统计今年的热门旅游城市、热门报考学校等,并将这样的信息显示在热点图中。
  • 因此,我们需要通过日志信息(运行商或者网站自己生成)和城市ip段信息来判断用户的ip段,统计热点经纬度、热门城市等指标。
  • 接下来我们使用spark来实现上述功能。
    • 经纬度, 城市, 数量
    • 北纬xx 西经 xx 北京 100
    • 北纬xx 西经 xx 天津 200
      在这里插入图片描述

数据

  • 20190121000132.394251.http.format
  • ip.txt
    在这里插入图片描述

思路分析

  • 1.创建执行环境
  • 2.加载日志文件和ip规则文件
  • 3.取出需要的ip字段和ip规则开始结束/城市信息/经纬度信息等字段
  • 4.将日志文件中的ip去ip规则文件中进行匹配,匹配上就将该城市/经纬度记为1(城市/经纬度,1)
  • 5.对(城市/经纬度,1)进行分组聚合,得到(城市/经纬度,数量)

工具类-IP地址字符串转数字

http://www.ab126.com/system/2859.html

需求:IP字符串格式转换数字格式

  • 分析:切分成每段组成的数组,遍历,拿着每个段|(之前的左移八位)后 ipNum = i | (ipNum << 8)
package cn.hanjiaxiaozhi.exercise

/**
 * Author hanjiaxiaozhi
 * Date 2020/7/27 16:12
 * Desc 
 */
object IPUtils {
    
    
  //将字符串格式的ip地址转为数字格式Long并返回
  //ip地址的不同表现形式:
  //http://www.ab126.com/system/2859.html
  //字符串:192.168.100.110
  //十进制 = 3232261230
  //二进制 = 11000000 10101000 01100100 01101110
  //我们的目标是将字符串格式的ip地址转为十进制返回
  //但是需要借助二进制,因为字符串格式中的每一段就是8位二进制的十进制表示,如192.168.100.110底层就是 11000000 10101000 01100100 01101110
  //那么如果我们能将192.168.100.110还原为11000000 10101000 01100100 01101110,再返回给Long,那就可以完成字符串格式的ip地址转为十进制
  //因为二进制到Long会自动转
  def ipToLong(ipStr:String):Long={
    
    
    var ipNum = 0L //先声明要返回的数字格式的ip变量为0,后续计算好重新赋值即可
    //取出字符串格式的ip中的每一段
    val ipArr: Array[Int] = ipStr.split("[.]").map(i=>i.toInt)//[192,168,100,110]
    for (i <- ipArr){
    
     //这个i就是192.168.100.110中的每一段,从左边开始进入循环
      //第一次:
      // i为192,底层二进制也就是11000000
      // ipNum为0
      // ipNum << 8还是0 :  00000000 00000000 00000000 00000000
      // i | (ipNum << 8)为:
      //00000000 00000000 00000000 11000000 |或
      //00000000 00000000 00000000 00000000
      //00000000 00000000 00000000 11000000
      //所以第一次结束,ipNum为:00000000 00000000 00000000 11000000 ,也就是192
      
      //第二次:
      //i是168,底层二进制10101000
      //ipNum为       00000000 00000000 00000000 11000000
      //ipNum << 8为: 00000000 00000000 11000000 00000000
      //i | (ipNum << 8):
      //00000000 00000000 00000000 10101000 |或
      //00000000 00000000 11000000 00000000
      //00000000 00000000 11000000 10101000 ,也就是 192 168
      
      //第三次:
      //i是100,底层二进制01100100
      //ipNum为       00000000 00000000 11000000 10101000
      //ipNum << 8为: 00000000 11000000 10101000 00000000
      //i | (ipNum << 8):
      //00000000 00000000 00000000 01100100 |或
      //00000000 11000000 10101000 00000000
      //00000000 11000000 10101000 01100100 ,也就是192 168 100
      
      //第四次:
      //i是110,底层二进制01101110
      //ipNum为       00000000 11000000 10101000 01100100
      //ipNum << 8为: 11000000 10101000 01100100 00000000
      //i | (ipNum << 8):
      //00000000 00000000 00000000 01101110
      //11000000 10101000 01100100 00000000
      //11000000 10101000 01100100 01101110 也就是192 168 100 110

      ipNum = i | (ipNum << 8)
    }
    ipNum
  }

  def main(args: Array[String]): Unit = {
    
    
    val ipNum: Long = ipToLong("192.168.100.110")
    println(ipNum)//十进制 3232261230
    println(ipNum.toBinaryString)//二进制 11000000 10101000 01100100 01101110
  }
}

工具类-二分查找

需求:拿着ipNum在有序数组中查找对应的索引,所以有则返回,没有返回-1

  • 分析:二分法查找,先设置开始(0)和结束索引(.length-1),当开始小于结束时,一直循环(直到要找的数>=开始索引并且<=结束索引,跳出循环),循环中定义中间索引,如果要找的数<startIp,说明应该去上面找,结束索引=中间索引-1;如果要找的数> endIp,说明应该去下面找,结束索引=中间索引+1
 //使用二分查找,拿着ipNum在有序数组中查找对应的索引,所以有则返回,没有返回-1
  def binarySearch(ipNum: Long, ipRuleTupleArr: Array[(String, String, String, String, String)]): Int = {
    
    
    var start: Int = 0;
    var end: Int = ipRuleTupleArr.length - 1
    while (start <= end) {
    
     //只要开始索引<=结束索引就一直找
      var middle: Int = (start + end) / 2
      val t = ipRuleTupleArr(middle) //取出索引为middle的元素
      val startIp: Long = t._1.toLong
      val endIp: Long = t._2.toLong
      //如果要找的ipNum正好>=startIp 而<=endIp,那就表示找着了
      if (ipNum >= startIp && ipNum <= endIp) {
    
    
        return middle
      } else if (ipNum < startIp) {
    
     //ipNum<startIp,说明应该去上面找
        end = middle - 1
      } else if (ipNum > endIp) {
    
    
        start = middle + 1
      }
    }
    -1 //如果循环结束都还没有找着,说明没有,没有则返回-1
  }

代码实现-1-RDD实现-10273毫秒

  • 分析:
    • 1.创建执行环境
    • 2.加载日志文件和ip规则文件
    • 3.取出需要的ip字段和ip规则开始结束/城市信息/经纬度信息等字段
    • 4.将日志文件中的ip去ip规则文件中进行匹配,匹配上就将该城市/经纬度记为1(城市/经纬度,1)
    • 5.对(城市/经纬度,1)进行分组聚合,得到(城市/经纬度,数量)
  • 优化:缓存/持久化,checkpoint
package cn.hanjiaxiaozhi.exercise

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

/**
 * Author hanjiaxiaozhi
 * Date 2020/7/27 15:34
 * Desc IP经纬度热力图分析-代码实现-1-RDD实现
 * 思路分析
 * 1.创建执行环境
 * 2.加载日志文件和ip规则文件
 * 3.取出需要的ip字段和ip规则开始结束/城市信息/经纬度信息等字段
 * 4.将日志文件中的ip去ip规则文件中进行匹配,匹配上就将该城市/经纬度记为1(城市/经纬度,1)
 * 5.对(城市/经纬度,1)进行分组聚合,得到(城市/经纬度,数量)
 */
object IpMapAnalysis {
    
    
  def main(args: Array[String]): Unit = {
    
    
    // * 1.创建执行环境
    val conf: SparkConf = new SparkConf().setAppName("IpMapAnalysis").setMaster("local[*]")
    val sc: SparkContext = new SparkContext(conf)
    sc.setLogLevel("WARN")

    // * 2.加载日志文件和ip规则文件
    val logFileRDD: RDD[String] = sc.textFile("file:///D:\\data\\spark\\20190121000132.394251.http.format")
    val ipRuleFileRDD: RDD[String] = sc.textFile("file:///D:\\data\\spark\\ip.txt")

    // * 3.取出需要的ip字段和ip规则开始结束/城市信息/经纬度信息等字段
    //logFileRDD.map(_.split("[|]")).map(arr=>arr(1))
    //3.1取出日志数据中的ip
    val ipStrRDD: RDD[String] = logFileRDD.map(_.split("[|]")(1))//[符号],表示使用该符号本身的正则含义
    //3.2取出ip规则数据中的数字格式ip开始(2),ip结束(3),城市信息(4,5,6,7,8),经度(13),纬度(14)
    val ipRuleTupleRDD: RDD[(String, String, String, String, String)] = ipRuleFileRDD
      .map(_.split("[|]"))
      .map(arr => (arr(2), arr(3), arr(4) + arr(5) + arr(6) + arr(7) + arr(8), arr(13), arr(14)))
    //3.3查看一下数据长啥样
    //ipStrRDD.take(5).foreach(println)
    /*
    125.213.100.123
    117.101.215.133
    117.101.222.68
    115.120.36.118
    123.197.64.247
     */
    //ipRuleTupleRDD.take(5).foreach(println)
    /*
    (16777472,16778239,亚洲中国福建福州,119.306239,26.075302)
    (16779264,16781311,亚洲中国广东广州,113.280637,23.125178)
    (16785408,16793599,亚洲中国广东广州,113.280637,23.125178)
    (16842752,16843007,亚洲中国福建福州,119.306239,26.075302)
    (16843264,16844799,亚洲中国福建福州,119.306239,26.075302)
     */
    // * 4.将日志文件中的ip去ip规则文件中进行匹配,匹配上就将该城市/经纬度记为1(城市/经纬度,1)
    //问题:字符串格式的ip和数字格式的ip如何匹配?--需要将字符串格式的ip转为数字格式然后再和ip规则文件中的数字格式ip进行匹配
    //注意:ipv4版本的ip地址本来就是4段8位二进制,本身就可以转换数字如long型
    //4.1将ipStrRDD中的字符串格式的ip转为数字格式ip,String-->Long,底层要用到二进制
    //ipStrRDD.map(str=>{IPUtils.ipToLong(str)})
    //ipStrRDD.map(IPUtils.ipToLong(_))
    val ipNumRDD: RDD[Long] = ipStrRDD.map(IPUtils.ipToLong)
    //ipNumRDD.take(5).foreach(println)
    /*
    2111136891
    1969608581
    1969610308
    1937253494
    2076524791
     */

    //4.2将转为数字的ip去和ip规则文件中的数字格式的开始和结束进行匹配,如果比开始大,比结束小就匹配上
    //上面的日志中的ip地址已经转换为数字了而ip规则数据中的ip数字是有序的
    //那么接下来认为就是拿着日志中的ip数字去有序的集合中匹配/寻找,那么可以使用二分查找
    val ipRuleTupleArr: Array[(String, String, String, String, String)] = ipRuleTupleRDD.collect()    //将RDD收集为本地集合
    val cityInfoAndOne: RDD[((String, String, String), Int)] = ipNumRDD.map(ipNum => {
    
    
      //拿着ip数字ipNum去有序数组中查询查询对应的索引
      val index: Int = IPUtils.binarySearch(ipNum, ipRuleTupleArr)
      val t: (String, String, String, String, String) = ipRuleTupleArr(index)
      ((t._3, t._4, t._5), 1)
    })
    //cityInfoAndOne.take(5).foreach(println)
    /*
    ((亚洲中国重庆重庆长寿,107.08166,29.85359),1)
    ((亚洲中国北京北京,116.405285,39.904989),1)
    ((亚洲中国北京北京,116.405285,39.904989),1)
    ((亚洲中国陕西西安,108.948024,34.263161),1)
    ((亚洲中国河北石家庄,114.502461,38.045474),1)
     */

    // * 5.对(城市/经纬度,1)进行分组聚合,得到(城市/经纬度,数量)
    val cityInfoAndCount: RDD[((String, String, String), Int)] = cityInfoAndOne.reduceByKey(_+_)
    val result: RDD[((String, String, String), Int)] = cityInfoAndCount.sortBy(_._2,false)

    //6.输出
    result.take(10).foreach(println)
    /*
    ((亚洲中国陕西西安,108.948024,34.263161),1824)
    ((亚洲中国北京北京,116.405285,39.904989),1535)
    ((亚洲中国重庆重庆,106.504962,29.533155),400)
    ((亚洲中国河北石家庄,114.502461,38.045474),383)
    ((亚洲中国重庆重庆江北,106.57434,29.60658),177)
    ((亚洲中国云南昆明,102.712251,25.040609),126)
    ((亚洲中国重庆重庆九龙坡,106.51107,29.50197),91)
    ((亚洲中国重庆重庆武隆,107.7601,29.32548),85)
    ((亚洲中国重庆重庆涪陵,107.39007,29.70292),47)
    ((亚洲中国重庆重庆合川,106.27633,29.97227),36)
     */
  }
}

代码实现-2-RDD实现-优化-10189毫秒

  • 注意:以下优化在本地运行效果不明显,在真正的集群和大数据量下,性能提升很明显
  • 但是广播的变量也不能太大,毕竟放在内存中
    在这里插入图片描述
package cn.hanjiaxiaozhi.exercise

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

/**
 * Author hanjiaxiaozhi
 * Date 2020/7/27 15:34
 * Desc IP经纬度热力图分析-代码实现-2-RDD实现-优化
 * 思路分析
 * 1.创建执行环境
 * 2.加载日志文件和ip规则文件
 * 3.取出需要的ip字段和ip规则开始结束/城市信息/经纬度信息等字段
 * 4.将日志文件中的ip去ip规则文件中进行匹配,匹配上就将该城市/经纬度记为1(城市/经纬度,1)
 * 5.对(城市/经纬度,1)进行分组聚合,得到(城市/经纬度,数量)
 */
object IpMapAnalysis2 {
    
    
  def main(args: Array[String]): Unit = {
    
    
    // * 1.创建执行环境
    val conf: SparkConf = new SparkConf().setAppName("IpMapAnalysis").setMaster("local[*]")
    val sc: SparkContext = new SparkContext(conf)
    sc.setLogLevel("WARN")

    // * 2.加载日志文件和ip规则文件
    val logFileRDD: RDD[String] = sc.textFile("file:///D:\\data\\spark\\20190121000132.394251.http.format")
    val ipRuleFileRDD: RDD[String] = sc.textFile("file:///D:\\data\\spark\\ip.txt")

    // * 3.取出需要的ip字段和ip规则开始结束/城市信息/经纬度信息等字段
    //logFileRDD.map(_.split("[|]")).map(arr=>arr(1))
    //3.1取出日志数据中的ip
    val ipStrRDD: RDD[String] = logFileRDD.map(_.split("[|]")(1))//[符号],表示使用该符号本身的正则含义
    //3.2取出ip规则数据中的数字格式ip开始(2),ip结束(3),城市信息(4,5,6,7,8),经度(13),纬度(14)
    val ipRuleTupleRDD: RDD[(String, String, String, String, String)] = ipRuleFileRDD
      .map(_.split("[|]"))
      .map(arr => (arr(2), arr(3), arr(4) + arr(5) + arr(6) + arr(7) + arr(8), arr(13), arr(14)))
    //3.3查看一下数据长啥样
    //ipStrRDD.take(5).foreach(println)
    /*
    125.213.100.123
    117.101.215.133
    117.101.222.68
    115.120.36.118
    123.197.64.247
     */
    //ipRuleTupleRDD.take(5).foreach(println)
    /*
    (16777472,16778239,亚洲中国福建福州,119.306239,26.075302)
    (16779264,16781311,亚洲中国广东广州,113.280637,23.125178)
    (16785408,16793599,亚洲中国广东广州,113.280637,23.125178)
    (16842752,16843007,亚洲中国福建福州,119.306239,26.075302)
    (16843264,16844799,亚洲中国福建福州,119.306239,26.075302)
     */
    // * 4.将日志文件中的ip去ip规则文件中进行匹配,匹配上就将该城市/经纬度记为1(城市/经纬度,1)
    //问题:字符串格式的ip和数字格式的ip如何匹配?--需要将字符串格式的ip转为数字格式然后再和ip规则文件中的数字格式ip进行匹配
    //注意:ipv4版本的ip地址本来就是4段8位二进制,本身就可以转换数字如long型
    //4.1将ipStrRDD中的字符串格式的ip转为数字格式ip,String-->Long,底层要用到二进制
    //ipStrRDD.map(str=>{IPUtils.ipToLong(str)})
    //ipStrRDD.map(IPUtils.ipToLong(_))
    val ipNumRDD: RDD[Long] = ipStrRDD.map(IPUtils.ipToLong)
    //ipNumRDD.take(5).foreach(println)
    /*
    2111136891
    1969608581
    1969610308
    1937253494
    2076524791
     */

    //4.2将转为数字的ip去和ip规则文件中的数字格式的开始和结束进行匹配,如果比开始大,比结束小就匹配上
    //上面的日志中的ip地址已经转换为数字了而ip规则数据中的ip数字是有序的
    //那么接下来认为就是拿着日志中的ip数字去有序的集合中匹配/寻找,那么可以使用二分查找
    /*
    优化:
    之前下面的ipNumRDD.map中用到了ipRuleTupleArr,是Driver端收集到的Array数据,人,然后给ipNumRDD.map中使用
    ipNumRDD.map中的函数在执行的时候是在各个机器的各个Task上去执行,那么有多少个Task就会被远程传输多少次
    所以可以进行如下的优化:
    将ipRuleTupleArr进行广播,发送给各个机器的Executor,那么各个Task要使用的时候从各自的Executor中获取即可,减少了ipRuleTupleArr远程传输的次数
     */
    //注意:接下来要将ipRuleTupleArr进行广播,注意不能直接广播RDD
    val ipRuleTupleArr: Array[(String, String, String, String, String)] = ipRuleTupleRDD.collect()
    //====================================================================
    //将ipRuleTupleArr广播到各个Executor,并起个名字叫ipRuleTupleArrBroadcast
    val ipRuleTupleArrBroadcast: Broadcast[Array[(String, String, String, String, String)]] = sc.broadcast(ipRuleTupleArr)//--1.广播
    val cityInfoAndOne: RDD[((String, String, String), Int)] = ipNumRDD.map(ipNum => {
    
    
      //这里要使用广播变量中的数据,得从ipRuleTupleArrBroadcast里面获取
      val array: Array[(String, String, String, String, String)] = ipRuleTupleArrBroadcast.value//--2.获取
      //拿着ip数字ipNum去有序数组中查询查询对应的索引
      val index: Int = IPUtils.binarySearch(ipNum, array)//--3.使用
      val t: (String, String, String, String, String) = array(index)//--3.使用
      //====================================================================
      ((t._3, t._4, t._5), 1)
    })
    //cityInfoAndOne.take(5).foreach(println)
    /*
    ((亚洲中国重庆重庆长寿,107.08166,29.85359),1)
    ((亚洲中国北京北京,116.405285,39.904989),1)
    ((亚洲中国北京北京,116.405285,39.904989),1)
    ((亚洲中国陕西西安,108.948024,34.263161),1)
    ((亚洲中国河北石家庄,114.502461,38.045474),1)
     */

    // * 5.对(城市/经纬度,1)进行分组聚合,得到(城市/经纬度,数量)
    val cityInfoAndCount: RDD[((String, String, String), Int)] = cityInfoAndOne.reduceByKey(_+_)
    val result: RDD[((String, String, String), Int)] = cityInfoAndCount.sortBy(_._2,false)

    //6.输出
    result.take(10).foreach(println)
    /*
    ((亚洲中国陕西西安,108.948024,34.263161),1824)
    ((亚洲中国北京北京,116.405285,39.904989),1535)
    ((亚洲中国重庆重庆,106.504962,29.533155),400)
    ((亚洲中国河北石家庄,114.502461,38.045474),383)
    ((亚洲中国重庆重庆江北,106.57434,29.60658),177)
    ((亚洲中国云南昆明,102.712251,25.040609),126)
    ((亚洲中国重庆重庆九龙坡,106.51107,29.50197),91)
    ((亚洲中国重庆重庆武隆,107.7601,29.32548),85)
    ((亚洲中国重庆重庆涪陵,107.39007,29.70292),47)
    ((亚洲中国重庆重庆合川,106.27633,29.97227),36)
     */
  }
}

代码实现-3-SparkSQL实现-37251毫秒

在这里插入图片描述

package cn.hanjiaxiaozhi.exercise

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

/**
 * Author hanjiaxiaozhi
 * Date 2020/7/27 15:34
 * Desc IP经纬度热力图分析-代码实现-3-SparkSQL实现
 * 思路分析
 * 1.创建执行环境
 * 2.加载日志文件和ip规则文件
 * 3.取出需要的ip字段和ip规则开始结束/城市信息/经纬度信息等字段
 * 4.将日志文件中的ip去ip规则文件中进行匹配,匹配上就将该城市/经纬度记为1(城市/经纬度,1)
 * 5.对(城市/经纬度,1)进行分组聚合,得到(城市/经纬度,数量)
 */
object IpMapAnalysis3 {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val startTime: Long = System.currentTimeMillis()
    // * 1.创建执行环境
    val spark: SparkSession = SparkSession.builder().appName("IpMapAnalysis").master("local[*]").getOrCreate()
    val sc: SparkContext = spark.sparkContext
    sc.setLogLevel("WARN")
    import spark.implicits._//导入隐式转换,方便后面使用


    // * 2.加载日志文件和ip规则文件
    val logFileRDD: RDD[String] = sc.textFile("file:///D:\\data\\spark\\20190121000132.394251.http.format")
    val ipRuleFileRDD: RDD[String] = sc.textFile("file:///D:\\data\\spark\\ip.txt")

    // * 3.取出需要的ip字段和ip规则开始结束/城市信息/经纬度信息等字段
    //logFileRDD.map(_.split("[|]")).map(arr=>arr(1))
    //3.1取出日志数据中的ip
    val ipStrRDD: RDD[String] = logFileRDD.map(_.split("[|]")(1))//[符号],表示使用该符号本身的正则含义
    //3.2取出ip规则数据中的数字格式ip开始(2),ip结束(3),城市信息(4,5,6,7,8),经度(13),纬度(14)
    val ipRuleTupleRDD: RDD[(String, String, String, String, String)] = ipRuleFileRDD
      .map(_.split("[|]"))
      .map(arr => (arr(2), arr(3), arr(4) + arr(5) + arr(6) + arr(7) + arr(8), arr(13), arr(14)))
    //3.3查看一下数据长啥样
    //ipStrRDD.take(5).foreach(println)
    /*
    125.213.100.123
    117.101.215.133
    117.101.222.68
    115.120.36.118
    123.197.64.247
     */
    //ipRuleTupleRDD.take(5).foreach(println)
    /*
    (16777472,16778239,亚洲中国福建福州,119.306239,26.075302)
    (16779264,16781311,亚洲中国广东广州,113.280637,23.125178)
    (16785408,16793599,亚洲中国广东广州,113.280637,23.125178)
    (16842752,16843007,亚洲中国福建福州,119.306239,26.075302)
    (16843264,16844799,亚洲中国福建福州,119.306239,26.075302)
     */
    // * 4.将ipStrRDD中的字符串格式的ip转为数字格式ip,String-->Long,底层要用到二进制
    val ipNumRDD: RDD[Long] = ipStrRDD.map(IPUtils.ipToLong)
    //ipNumRDD.take(5).foreach(println)
    /*
    2111136891
    1969608581
    1969610308
    1937253494
    2076524791
     */

    //5.将RDD转为DataFrame/DataSet,前面已经完成类型相关的操作,所以这里直接转为DataFrame即可
    val ipNumDF: DataFrame = ipNumRDD.toDF("ipNum")
    //ipNumDF.show(10,false)
    //ipNumDF.printSchema()
    /*
     * +----------+
     * |ipNum     |
     * +----------+
     * |2111136891|
     * |1969608581|
     * |1969610308|
     *  .......
     *
     * root
     * |-- ipNum: string (nullable = false)
     */

    val ipRuleDF: DataFrame = ipRuleTupleRDD.toDF("ipStart","ipEnd","cityInfo","longitude","latitude")
    //ipRuleDF.show(10,false)
    //ipRuleDF.printSchema()
    /**
     * +--------+--------+------------------+----------+---------+
     * |ipStart |ipEnd   |cityInfo         |longitude |latitude |
     * +--------+--------+-----------------+----------+---------+
     * |16777472|16778239|亚洲中国福建福州  |119.306239|26.075302|
     * |16779264|16781311|亚洲中国广东广州  |113.280637|23.125178|
     * |16785408|16793599|亚洲中国广东广州  |113.280637|23.125178|
     *  ...................
     *
     * root
     * |-- ipStart: string (nullable = true)
     * |-- ipEnd: string (nullable = true)
     * |-- cityInfo: string (nullable = true)
     * |-- longitude: string (nullable = true)
     * |-- latitude: string (nullable = true)
     */


    //6.根据ipNumDF和ipRuleDF统计(城市信息经纬度,数量)
    //6.1注册表
    ipNumDF.createOrReplaceTempView("t_ipNum")
    ipRuleDF.createOrReplaceTempView("t_ipRule")

    //6.2编写sql
    val sql:String =
      """
        |select cityInfo,longitude,latitude,count(*) count
        |from t_ipNum
        |join t_ipRule
        |on ipNum >= ipStart and ipNum <= ipEnd
        |group by cityInfo,longitude,latitude
        |order by count
        |""".stripMargin

    //6.3执行sql
    spark.sql(sql).show(10,false)

    //注意:该写法,写的简单,但是效率低,因为多表直接join,产生笛卡尔积,再过滤ipNum >= ipStart and ipNum <= ipEnd,效率较低

    //Thread.sleep(Long.MaxValue)//26 s
    val endTime: Long = System.currentTimeMillis()
    println(endTime-startTime)

  }
}

代码实现-4-SparkSQL实现-优化-15288毫秒

  • SQL风格
    • 优化:避免多表join,自定义udf:只将要查的数据(单列)创建表,将被查的数据变成数组,查到后将该列变成多列返回
package cn.hanjiaxiaozhi.exercise

import org.apache.spark.SparkContext
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{
    
    DataFrame, SparkSession}

/**
 * Author hanjiaxiaozhi
 * Date 2020/7/27 15:34
 * Desc IP经纬度热力图分析-代码实现-4-SparkSQL实现-优化
 * 思路分析
 * 1.创建执行环境
 * 2.加载日志文件和ip规则文件
 * 3.取出需要的ip字段和ip规则开始结束/城市信息/经纬度信息等字段
 * 4.将日志文件中的ip去ip规则文件中进行匹配,匹配上就将该城市/经纬度记为1(城市/经纬度,1)
 * 5.对(城市/经纬度,1)进行分组聚合,得到(城市/经纬度,数量)
 */
object IpMapAnalysis4 {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val startTime: Long = System.currentTimeMillis()
    // * 1.创建执行环境
    val spark: SparkSession = SparkSession.builder().appName("IpMapAnalysis").master("local[*]").getOrCreate()
    val sc: SparkContext = spark.sparkContext
    sc.setLogLevel("WARN")
    import spark.implicits._ //导入隐式转换,方便后面使用


    // * 2.加载日志文件和ip规则文件
    val logFileRDD: RDD[String] = sc.textFile("file:///D:\\data\\spark\\20190121000132.394251.http.format")
    val ipRuleFileRDD: RDD[String] = sc.textFile("file:///D:\\data\\spark\\ip.txt")

    // * 3.取出需要的ip字段和ip规则开始结束/城市信息/经纬度信息等字段
    //logFileRDD.map(_.split("[|]")).map(arr=>arr(1))
    //3.1取出日志数据中的ip
    val ipStrRDD: RDD[String] = logFileRDD.map(_.split("[|]")(1)) //[符号],表示使用该符号本身的正则含义
    //3.2取出ip规则数据中的数字格式ip开始(2),ip结束(3),城市信息(4,5,6,7,8),经度(13),纬度(14)
    val ipRuleTupleRDD: RDD[(String, String, String, String, String)] = ipRuleFileRDD
      .map(_.split("[|]"))
      .map(arr => (arr(2), arr(3), arr(4) + arr(5) + arr(6) + arr(7) + arr(8), arr(13), arr(14)))
    //3.3查看一下数据长啥样
    //ipStrRDD.take(5).foreach(println)
    /*
    125.213.100.123
    117.101.215.133
    117.101.222.68
    115.120.36.118
    123.197.64.247
     */
    //ipRuleTupleRDD.take(5).foreach(println)
    /*
    (16777472,16778239,亚洲中国福建福州,119.306239,26.075302)
    (16779264,16781311,亚洲中国广东广州,113.280637,23.125178)
    (16785408,16793599,亚洲中国广东广州,113.280637,23.125178)
    (16842752,16843007,亚洲中国福建福州,119.306239,26.075302)
    (16843264,16844799,亚洲中国福建福州,119.306239,26.075302)
     */
    // * 4.将ipStrRDD中的字符串格式的ip转为数字格式ip,String-->Long,底层要用到二进制
    val ipNumRDD: RDD[Long] = ipStrRDD.map(IPUtils.ipToLong)
    //ipNumRDD.take(5).foreach(println)
    /*
    2111136891
    1969608581
    1969610308
    1937253494
    2076524791
     */

    //5.将RDD转为DataFrame/DataSet,前面已经完成类型相关的操作,所以这里直接转为DataFrame即可
    val ipNumDF: DataFrame = ipNumRDD.toDF("ipNum")
    //ipNumDF.show(10,false)
    //ipNumDF.printSchema()
    /*
     * +----------+
     * |ipNum     |
     * +----------+
     * |2111136891|
     * |1969608581|
     * |1969610308|
     *  .......
     *
     * root
     * |-- ipNum: string (nullable = false)
     */

    //6.将ipRuleTupleRDD收集为数组然后广播
    val ipRuleTupleArr: Array[(String, String, String, String, String)] = ipRuleTupleRDD.collect()
    val ipRuleTupleArrBroadcast: Broadcast[Array[(String, String, String, String, String)]] = sc.broadcast(ipRuleTupleArr)


    //7.最终的目标:将ipNumDF中的数字格式的ip转为(城市信息经纬度,数量)
    //首先需要将ipNumDF中的数字格式的ip和ipRuleTupleArrBroadcast广播变量中的数组中的数据进行匹配(二分查找)
    //然后匹配上了就可以进行分组聚合排序
    //7.1注册表
    ipNumDF.createOrReplaceTempView("t_ipNum")
    //7.2自定义UDF-SQL风格
    //ipNum2CityInfo(ipNum)该函数需要单独定义,功能是ipNum在广播变量中的数组中进行匹配(二分查找)
    spark.udf.register("ipNum2CityInfo", (ipNum: Long) => {
    
    
      val array: Array[(String, String, String, String, String)] = ipRuleTupleArrBroadcast.value
      val index: Int = IPUtils.binarySearch(ipNum, array)
      val t: (String, String, String, String, String) = array(index)
      t._3 + "-" + t._4 + "-" + t._5
    })
    //7.3编写写一条sql,完成上面的功能
    val sql: String =
      """
        |select ipNum2CityInfo(ipNum) cityInfo,count(*) count
        |from t_ipNum
        |group by cityInfo
        |order by count desc
        |""".stripMargin
    //7.4执行sql
    spark.sql(sql).show(10, false)

    val endTime: Long = System.currentTimeMillis()
    println(endTime-startTime)
  }
}

代码实现-5-SparkSQL实现-优化-15736毫秒

  • DSL风格
package cn.hanjiaxiaozhi.exercise

import org.apache.spark.SparkContext
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.expressions.UserDefinedFunction
import org.apache.spark.sql.{
    
    DataFrame, SparkSession}

/**
 * Author hanjiaxiaozhi
 * Date 2020/7/27 15:34
 * Desc IP经纬度热力图分析-代码实现-5-SparkSQL实现-优化-DSL
 * 思路分析
 * 1.创建执行环境
 * 2.加载日志文件和ip规则文件
 * 3.取出需要的ip字段和ip规则开始结束/城市信息/经纬度信息等字段
 * 4.将日志文件中的ip去ip规则文件中进行匹配,匹配上就将该城市/经纬度记为1(城市/经纬度,1)
 * 5.对(城市/经纬度,1)进行分组聚合,得到(城市/经纬度,数量)
 */
object IpMapAnalysis5 {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val startTime: Long = System.currentTimeMillis()
    // * 1.创建执行环境
    val spark: SparkSession = SparkSession.builder().appName("IpMapAnalysis").master("local[*]").getOrCreate()
    val sc: SparkContext = spark.sparkContext
    sc.setLogLevel("WARN")
    import spark.implicits._ //导入隐式转换,方便后面使用


    // * 2.加载日志文件和ip规则文件
    val logFileRDD: RDD[String] = sc.textFile("file:///D:\\data\\spark\\20190121000132.394251.http.format")
    val ipRuleFileRDD: RDD[String] = sc.textFile("file:///D:\\data\\spark\\ip.txt")

    // * 3.取出需要的ip字段和ip规则开始结束/城市信息/经纬度信息等字段
    //logFileRDD.map(_.split("[|]")).map(arr=>arr(1))
    //3.1取出日志数据中的ip
    val ipStrRDD: RDD[String] = logFileRDD.map(_.split("[|]")(1)) //[符号],表示使用该符号本身的正则含义
    //3.2取出ip规则数据中的数字格式ip开始(2),ip结束(3),城市信息(4,5,6,7,8),经度(13),纬度(14)
    val ipRuleTupleRDD: RDD[(String, String, String, String, String)] = ipRuleFileRDD
      .map(_.split("[|]"))
      .map(arr => (arr(2), arr(3), arr(4) + arr(5) + arr(6) + arr(7) + arr(8), arr(13), arr(14)))
    //3.3查看一下数据长啥样
    //ipStrRDD.take(5).foreach(println)
    /*
    125.213.100.123
    117.101.215.133
    117.101.222.68
    115.120.36.118
    123.197.64.247
     */
    //ipRuleTupleRDD.take(5).foreach(println)
    /*
    (16777472,16778239,亚洲中国福建福州,119.306239,26.075302)
    (16779264,16781311,亚洲中国广东广州,113.280637,23.125178)
    (16785408,16793599,亚洲中国广东广州,113.280637,23.125178)
    (16842752,16843007,亚洲中国福建福州,119.306239,26.075302)
    (16843264,16844799,亚洲中国福建福州,119.306239,26.075302)
     */
    // * 4.将ipStrRDD中的字符串格式的ip转为数字格式ip,String-->Long,底层要用到二进制
    val ipNumRDD: RDD[Long] = ipStrRDD.map(IPUtils.ipToLong)
    //ipNumRDD.take(5).foreach(println)
    /*
    2111136891
    1969608581
    1969610308
    1937253494
    2076524791
     */

    //5.将RDD转为DataFrame/DataSet,前面已经完成类型相关的操作,所以这里直接转为DataFrame即可
    val ipNumDF: DataFrame = ipNumRDD.toDF("ipNum")
    //ipNumDF.show(10,false)
    //ipNumDF.printSchema()
    /*
     * +----------+
     * |ipNum     |
     * +----------+
     * |2111136891|
     * |1969608581|
     * |1969610308|
     *  .......
     *
     * root
     * |-- ipNum: string (nullable = false)
     */

    //6.将ipRuleTupleRDD收集为数组然后广播
    val ipRuleTupleArr: Array[(String, String, String, String, String)] = ipRuleTupleRDD.collect()
    val ipRuleTupleArrBroadcast: Broadcast[Array[(String, String, String, String, String)]] = sc.broadcast(ipRuleTupleArr)


    //7.最终的目标:将ipNumDF中的数字格式的ip转为(城市信息经纬度,数量)
    //首先需要将ipNumDF中的数字格式的ip和ipRuleTupleArrBroadcast广播变量中的数组中的数据进行匹配(二分查找)
    //然后匹配上了就可以进行分组聚合排序
    //也就是直接将ipNumDF中的ipNum转换为匹配(二分查找)后的结果:cityInfo,然后再分组聚合排序即可
    //7.1自定义UDF-DSL风格
    //完成将ipNum转换为匹配(二分查找)后的结果:cityInfo
    import org.apache.spark.sql.functions._
    val ipNum2CityInfo: UserDefinedFunction = udf((ipNum:Long)=>{
    
    
      val array: Array[(String, String, String, String, String)] = ipRuleTupleArrBroadcast.value
      val index: Int = IPUtils.binarySearch(ipNum,array)
      val t: (String, String, String, String, String) = array(index)
      t._3+"-"+t._4+"-"+t._5
    })

    //7.2编写DSL查询完成分组聚合排序
    //ipNumDF.select(ipNum2CityInfo($"ipNum") as "cityInfo")
    ipNumDF.select(ipNum2CityInfo('ipNum).as("cityInfo"))
      .groupBy($"cityInfo")
      .count()
      .orderBy($"count".desc)
      .show(10,false)

    val endTime: Long = System.currentTimeMillis()
    println(endTime-startTime)
  }
}

猜你喜欢

转载自blog.csdn.net/qq_46893497/article/details/114004069
今日推荐