综合练习-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)
}
}