代码编写使用的是scala
1.首先定义两个方法
- ip2Long:将ip地址转为十进制的Long
- binarySearch:二分查找
object Utils {
/**
* 将 String 类型的 ip 转为 Long 类型的 十进制ip
* @param ip
* @return
*/
def ip2Long(ip: String): Long = {
//将数据按照 . 分割开
//192.168.5.1
val splited: Array[String] = ip.split("[.]")
//遍历取出的每一条ip,按照特定算法算出 十进制Long 的值
var ipNum = 0L
for (i <- splited.indices) {
ipNum = splited(i).toLong | ipNum << 8L
}
ipNum
}
/**
* 使用二分查找,查询输入的 ip 所对应的地址,并返回该地址对应的Array的index
* @param ip
* @return
*/
def binarySearch(ipArray: Array[(Long,Long,String,String)], ip: Long): Int = {
//定义一个开始下标
var startIndex = 0
//定义一个结束下标
var endIndex = ipArray.length -1
//二分查找
while(startIndex <= endIndex){
var midIndex = (startIndex + endIndex)/2
if (ip>=ipArray(midIndex)._1 && ip<=ipArray(midIndex)._2 ){
return midIndex //此处一定要写return,这样才会结束该方法,返回midIndex的值
}else if(ip < ipArray(midIndex)._1){
endIndex = midIndex - 1
}else{
startIndex = midIndex + 1
}
}
-1 //没有找到就返回 -1
}
}
2.读取数据文件,进行逆地理位置查找
首先要讲解广播变量:
- 广播变量就是将Driver端的一个数据(这个数据可以从多个Executor拉取来组成,也可以直接从HDFS读取),分片发送给所有的Executor,Driver端有一个 BroadcastManager,由于Worker的反向注册信息,Driver会得到所有的Executor的数据,因此也知道总共有多少个Executor,BroadcastManager根据Executor的个数将数据进行分片,然后将每一片数据分别发送给一个Executor,这样,Driver就只需要给每个Executor发送一次数据,并且Driver端就只发送了一整个文件大小的数据。后续各个Executor之间会进行通信,会将各自拥有的分片数据进行互传,这样所有的Executor就会全部拥有完整的数据内容
- broadcastManager是一个同步方法,只能等所有数据传输完成后,才会进行Driver下一步的程序操作
- 广播变量是只读的,且不可变的,广播之后就不可改变了
广播变量的实质:相当于将广播数据提前缓存到了Executor所在的机器中,也就是实现了MapSide Join
为什么要广播数据:为了关联数据,将较小的数据事先进行缓存
注意:当广播变量数据很大时,内存中放不下,此时可将广播数据存放到外部的数据库中,如HBASE、Redis等,来一条数据查询一次外部的数据库
object ipSearchLocation {
def main(args: Array[String]): Unit = {
//获取SparkConf,工具类中定义为本地执行
val conf: SparkConf = Utils.getConf()
val sc: SparkContext = Utils.getSparkContext(conf)
//读取ip规则数据,数据路径自行修改
val ipLines: RDD[String] = sc.textFile("C:\\Users\\logan\\Desktop\\testData\\ip.txt")
//取出每条数据,解析数据
val maped: Array[(Long, Long, String, String)] = ipLines.map(line => {
//按照 | 切分数据
val splited: Array[String] = line.split("[|]")
//获取起始ip值与结束ip值
val start: Long = splited(2).toLong
val end: Long = splited(3).toLong
//获取省份和城市
val province: String = splited(6)
val city: String = splited(7)
//返回tuple4
(start, end, province, city)
}).sortBy(_._1) //按照start进行排序,因为后续要进行二分查找,因此数据必须有序
.collect() //使用 collect() 将完整数据收集到Driver端
//注意:广播 broadcast是 SparkContext的一个方法,调用broadcast方法
//将上面得到的 maped 数据进行 广播
val broadcast: Broadcast[Array[(Long, Long, String, String)]] = sc.broadcast(maped)
//接着读取实际日志数据
val logLines: RDD[String] = sc.textFile("C:\\Users\\logan\\Desktop\\testData\\ipaccess.log")
//解析每一条 日志数据
val result: RDD[((String, String), Int)] = logLines.map(line => {
//使用 | 切分数据
val splited: Array[String] = line.split("[|]")
//获取ip地址
val ipString: String = splited(1)
//得到每条日志的 ip地址 ,调用工具类将 String 类型的ip地址 转换为 Long类型的ip地址
val ipLong: Long = Utils.ip2Long(ipString)
//查找之前,先要取出广播变量的值。调用其 value 方法
val ipMatchRule: Array[(Long, Long, String, String)] = broadcast.value
//得到long 类型的ip地址
//调用二分查找,开始查找该ip地址的地址下标
val index: Int = Utils.binarySearch(ipMatchRule, ipLong)
//index是返回的对应 数据的下标,用于取出某ip地址对应的省份和城市
//若 index = -1 则代表没有找到对应的值
//定义province 和 city 的初始值
var province = "null"
var city = "null"
//判断 index 的值
if (index > -1) {
//若有值,就将对应Array中的 省份和城市取出来
province = ipMatchRule(index)._3
city = ipMatchRule(index)._4
}
//保存为k-v形式,方便后续计算各省份各城市的次数
((province, city), 1)
}).reduceByKey(_ + _) //直接进行聚合计算,局部聚合与全局聚合是相同的逻辑
//打印结果
println(result.collect().toBuffer)
//关闭SparkContext
sc.stop()
}
}
3.结果展示
部分数据如下:
ArrayBuffer(((云南,昆明),126), ((北京,北京),1535), ((河北,石家庄),383), ((null,null),1), ((陕西,西安),1824), ((重庆,重庆),868))