spark小案例---根据IP计算归属地

今天学习了一个根据IP统计归属地的小案例,在此记录一下。

在电商网站后台都会记录用户的浏览日志,然后根据这些日志文件就可以做数据分析,比如统计用户的地址,喜好,这样就可以给用户推荐商品了。
那么怎样进行统计呢,首先我们要有一份各个省份的IP规则,然后要有一份日志文件,我们从日志文件中切分出IP字段,然后与IP规则进行对比,就可以匹配到是哪个地区的了。

我们先来写一下这个小案例的需求;

根据访问日志的IP地址计算出访问者的归属地,并且按照省份,计算出访问次数,然后将计算好的结果写入到MySQL中
1.整理数据,切分出IP字段,然后将IP地址转换成十进制
2.加载IP规则,整理规则,取出有用的字段,然后将数据缓存到内存中(Executor中的内存中)
3.将访问log与IP规则进行匹配(二分法查找)
4.取出对应的省份名称,然后将其和1组合在一起
5.按省份进行聚合
6.将聚合后的数据写入到MySQL中

难点:
首先我们要理解spark提交任务的机制以及RDD创建的机制,在此不在过多的阐述,可以查看博客:
RDD详解:https://blog.csdn.net/weixin_43866709/article/details/88623920
RDD之collect方法执行的过程:https://blog.csdn.net/weixin_43866709/article/details/88666080

难点就在于我们更好的使用IP规则这份数据

接下来我们一步一步的实现这个小需求

工具:spark集群,hdfs集群,MySQL,idea

1.加载IP规则,整理规则,取出有用的字段,然后将数据缓存到内存中(Executor中的内存中)

首先我们要将IP规则读取到hdfs中,这样可以保证IP规则这份数据不易丢失

val rulesLines: RDD[String] = sc.textFile(args(0))

然后整理IP规则,只取出有用的数据,比如用于比较的IP范围,还有对应的省份;
但是这里有一个问题,整理IP规则的是Task,是在Executor端执行的,这样每个Executor只是整理了部分的数据,后面得比较也是在Executor端执行的,这样会出现比较的错误。所以我们要将Executor处理完的IP规则收集到Driver端,这时Driver端的IP规则数据就是完整的了,再将Driver端的数据广播到Executor端,这样Executor端的数据就也是完整的了,就可以进行正确的比较了。

//整理ip规则数据
    //这里是在Executor中执行的,每个Executor只计算部分的IP规则数据
    val ipRulesRDD: RDD[(Long, Long, String)] = rulesLines.map(line => {
      val fields = line.split("[|]")
      val startNum = fields(2).toLong
      val endNum = fields(3).toLong
      val province = fields(6)
      (startNum, endNum, province)
    })

    //需要将每个Executor端执行完的数据收集到Driver端
    val rulesInDriver: Array[(Long, Long, String)] = ipRulesRDD.collect()

    //再将Driver端的完整的数据广播到Executor端
    //生成广播数据的引用
    val broadcastRef: Broadcast[Array[(Long, Long, String)]] = sc.broadcast(rulesInDriver)

2.整理数据,切分出IP字段,然后将IP地址转换成十进制
3.将访问log与IP规则进行匹配(二分法查找)
4.取出对应的省份名称,然后将其和1组合在一起

首先我们先写一个小算法,用于将IP地址转换成十进制数字(这样更加便于比较)

TestIp.scala

//将IP转化为十进制
  def ip2Long(ip: String): Long = {
    val fragments = ip.split("[.]")
    var ipNum = 0L
    for (i <- 0 until fragments.length){
      ipNum =  fragments(i).toLong | ipNum << 8L
    }
    ipNum
  }

再写一个小算法,用于IP地址的比较,因为IP规则是一个IP字段的范围,也就是说一个范围对应一个省份,要拿日志文件中的IP地址与这个范围进行比较,而且IP规则中的数据是排好序的,所以使用二分法查找会更加快捷:

TestIp.scala

//二分法查找
  def binarySearch(lines: Array[(Long, Long, String)], ip: Long) : Int = {
    var low = 0
    var high = lines.length - 1
    while (low <= high) {
      val middle = (low + high) / 2
      if ((ip >= lines(middle)._1) && (ip <= lines(middle)._2))
        return middle
      if (ip < lines(middle)._1)
        high = middle - 1
      else {
        low = middle + 1
      }
    }
    -1
  }

然后我们开始整理日志文件的数据,取出IP地址,转换成十进制,然后与IP规则进行比较:

//整理日志文件的数据,取出ip,转换成十进制,与IP规则进行比较(采用二分法)
    val provinceAndOne: RDD[(String, Int)] = accessLines.map(line => {
      val fields = line.split("[|]")
      val ip = fields(1)
      //将ip转换成十进制
      val ipNum = TestIp.ip2Long(ip)
      //让Executor通过广播数据的引用拿到广播的数据
      //Task是在Driver端生成的,广播变量的引用是伴随着Task被发送到Executor端的
      val rulesInExecutor: Array[(Long, Long, String)] = broadcastRef.value
      //查找
      var province = "未知"
      val index: Int = TestIp.binarySearch(rulesInExecutor, ipNum)
      if (index != -1) {
        province = rulesInExecutor(index)._3
      }
      (province, 1)
    })

5.按省份进行聚合

val reduced: RDD[(String, Int)] = provinceAndOne.reduceByKey(_+_)

6.将聚合后的数据写入到MySQL中

我们也提前将写入MySQL的规则写好:

def data2MySQL(it: Iterator[(String, Int)]): Unit = {
    //一个迭代器代表一个分区,分区中有多条数据
    //先获得一个JDBC连接
    val conn: Connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/bigdata?characterEncoding=UTF-8", "用户", "密码")
    //将数据通过Connection写入到数据库
    val pstm: PreparedStatement = conn.prepareStatement("INSERT INTO access_log VALUES (?, ?)")
    //将分区中的数据一条一条写入到MySQL中
    it.foreach(tp => {
      pstm.setString(1, tp._1)
      pstm.setInt(2, tp._2)
      pstm.executeUpdate()
    })
    //将分区中的数据全部写完之后,在关闭连接
    if(pstm != null) {
      pstm.close()
    }
    if (conn != null) {
      conn.close()
    }
  }

在这里我们最好使用foreachPartition方法,一次拿出一个分区进行处理,这样一个分区使用一个jdbc连接,会更加节省资源。

reduced.foreachPartition(it => TestIp.data2MySQL(it))

到这里就处理完了,下面是完整的代码:

TestIp.scala

package XXX

import java.sql.{Connection, DriverManager, PreparedStatement}

import scala.io.{BufferedSource, Source}

object TestIp {

  //将IP转化为十进制
  def ip2Long(ip: String): Long = {
    val fragments = ip.split("[.]")
    var ipNum = 0L
    for (i <- 0 until fragments.length){
      ipNum =  fragments(i).toLong | ipNum << 8L
    }
    ipNum
  }

  //定义读取ip.txt规则,只要有用的数据
  def readRules(path:String):Array[(Long,Long,String)] = {
    //读取ip.txt
    val bf: BufferedSource = Source.fromFile(path)
    //对ip.txt进行整理
    val lines: Iterator[String] = bf.getLines()
    //对ip进行整理,并放入内存
    val rules: Array[(Long, Long, String)] = lines.map(line => {
      val fileds = line.split("[|]")
      val startNum = fileds(2).toLong
      val endNum = fileds(3).toLong
      val province = fileds(6)
      (startNum, endNum, province)
    }).toArray
    rules
  }

  //二分法查找
  def binarySearch(lines: Array[(Long, Long, String)], ip: Long) : Int = {
    var low = 0
    var high = lines.length - 1
    while (low <= high) {
      val middle = (low + high) / 2
      if ((ip >= lines(middle)._1) && (ip <= lines(middle)._2))
        return middle
      if (ip < lines(middle)._1)
        high = middle - 1
      else {
        low = middle + 1
      }
    }
    -1
  }

  def data2MySQL(it: Iterator[(String, Int)]): Unit = {
    //一个迭代器代表一个分区,分区中有多条数据
    //先获得一个JDBC连接
    val conn: Connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/bigdata?characterEncoding=UTF-8", "用户", "密码")
    //将数据通过Connection写入到数据库
    val pstm: PreparedStatement = conn.prepareStatement("INSERT INTO access_log VALUES (?, ?)")
    //将分区中的数据一条一条写入到MySQL中
    it.foreach(tp => {
      pstm.setString(1, tp._1)
      pstm.setInt(2, tp._2)
      pstm.executeUpdate()
    })
    //将分区中的数据全部写完之后,在关闭连接
    if(pstm != null) {
      pstm.close()
    }
    if (conn != null) {
      conn.close()
    }
  }

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

    //数据是在内存中
    val rules: Array[(Long, Long, String)] = readRules("E:/Spark视频/小牛学堂-大数据24期-06-Spark安装部署到高级-10天/spark-04-Spark案例讲解/课件与代码/ip/ip.txt")

    //将ip地址转换成十进制
    val ipNum = ip2Long("1.24.6.56")

    //查找
    val index = binarySearch(rules,ipNum)

    //根据脚标到rules中查找对应的数据
    val tp = rules(index)

    val province = tp._3

    println(province)

  }

}

IpLocation.scala

package XXXX



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

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

    val conf = new SparkConf().setAppName("IpLocation2").setMaster("local[4]")
    val sc = new SparkContext(conf)

    //将ip.txt读取到HDFS中
    val rulesLines: RDD[String] = sc.textFile(args(0))

    //整理ip规则数据
    //这里是在Executor中执行的,每个Executor只计算部分的IP规则数据
    val ipRulesRDD: RDD[(Long, Long, String)] = rulesLines.map(line => {
      val fields = line.split("[|]")
      val startNum = fields(2).toLong
      val endNum = fields(3).toLong
      val province = fields(6)
      (startNum, endNum, province)
    })

    //需要将每个Executor端执行完的数据收集到Driver端
    val rulesInDriver: Array[(Long, Long, String)] = ipRulesRDD.collect()

    //再将Driver端的完整的数据广播到Executor端
    //生成广播数据的引用
    val broadcastRef: Broadcast[Array[(Long, Long, String)]] = sc.broadcast(rulesInDriver)


    //接下来开始读取访问日志数据
    val accessLines: RDD[String] = sc.textFile(args(1))

    //整理日志文件的数据,取出ip,转换成十进制,与IP规则进行比较(采用二分法)
    val provinceAndOne: RDD[(String, Int)] = accessLines.map(line => {
      val fields = line.split("[|]")
      val ip = fields(1)
      //将ip转换成十进制
      val ipNum = TestIp.ip2Long(ip)
      //让Executor通过广播数据的引用拿到广播的数据
      //Task是在Driver端生成的,广播变量的引用是伴随着Task被发送到Executor端的
      val rulesInExecutor: Array[(Long, Long, String)] = broadcastRef.value
      //查找
      var province = "未知"
      val index: Int = TestIp.binarySearch(rulesInExecutor, ipNum)
      if (index != -1) {
        province = rulesInExecutor(index)._3
      }
      (province, 1)
    })

    //聚合
    val reduced: RDD[(String, Int)] = provinceAndOne.reduceByKey(_+_)

    reduced.foreachPartition(it => TestIp.data2MySQL(it))

    //释放资源
    sc.stop()


  }

}

//这种方法是通过HDFS读取IP规则(ip.txt),在收集到Driver端,然后再广播到Executor端
//优点:IP规则更加安全,不容易丢失,而且不用和Driver在同一台机器
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Spark是一个开源的大数据处理框架,可以处理大规模的数据集。而Spark SQL是Spark中的一个模块,用于处理结构化数据,支持SQL查询和DataFrame API。Spark SQL可以将结构化数据存储在分布式的列式存储系统中,并提供了高效的查询引擎,可以在大规模数据集上进行快速的查询和分析。Spark SQL还支持多种数据源,包括Hive、JSON、Parquet等。 ### 回答2: Spark是一个开源的大数据处理工具,它主要的特点是速度快、易于扩展和支持多种语言。Spark可以用于批处理、实时处理、机器学习、图处理、流处理等多个领域,并且可以在大多数基础设施上运行,如Hadoop、Mesos、Kubernetes等,为企业提供了更加便利灵活的大数据处理方案。 Spark SQL是Spark中的一个模块,它提供了一个基于SQL的接口以及齐全的支持,让用户可以方便地在Spark上进行结构化数据处理,如数据查询、聚合、过滤等。Spark SQL的优势在于其性能优异,它可以在不同的数据源上运行,包括Hive、Avro、Parquet等。 Spark SQL模块的核心组件为Catalyst,它是一个基于规则的优化器,可以自动优化查询计划,提高整体查询速度。Spark SQL支持多种查询API,包括SQL、DataFrame API和DataSet API,用户可以根据自己的需求选择不同的API来处理数据。 另外,Spark SQL支持数据格式的灵活转换,可以将不同数据源中的数据转换为DataFrame或DataSet格式,并且提供了丰富的数据源集成插件,如JDBC、MongoDB、Cassandra等。 总之,Spark以其快速、灵活、易于扩展的特性帮助企业更好地管理和处理大规模结构化数据,而Spark SQL则是Spark的重要组成部分,它提供SQL接口和优化器,让用户可以更加方便地处理和分析结构化数据。 ### 回答3: Spark是一个开源分布式数据处理框架,可以快速处理大规模数据集,支持多种数据源和数据处理方式,具有高效的计算能力和可扩展性。Spark SQL是Spark中的一个子项目,提供了一种基于SQL的接口,可以将结构化数据集集成到Spark中,支持SQL查询、数据聚合、连接和过滤等操作。Spark SQL支持多种数据源,包括Hive表、JSON、Parquet和CSV格式等,同时也支持DataFrame和Dataset等高级数据结构。Spark SQL还提供了Java、Scala和Python等多种编程语言接口,以便各种开发人员利用Spark SQL进行数据处理和分析。 Spark SQL的一个重要特性是DataFrame,它是一个分布式的数据集合,类似于关系型数据库中的表格,但是可以横向扩展到大规模数据集。DataFrame提供了Schema(数据结构)的定义和数据类型的推导,可以简化代码编写和数据处理过程。在Spark SQL中,DataFrame可以通过API进行操作,也可以通过SQL查询进行操作。Spark SQL还支持多种数据格式之间的转换,例如从JSON格式转换为Parquet格式,从CSV格式转换为Hive表格等。 Spark SQL的一个优势是可以与其他Spark组件集成,例如Spark Streaming、MLlib和GraphX等,使得数据处理和分析更加高效和简洁。Spark Streaming可以将实时数据处理与批处理数据处理相结合,使得数据处理和分析更加完整和有力。MLlib提供了机器学习算法的实现,可以快速进行数据挖掘和模型训练。GraphX提供了图形计算的能力,可以进行大规模的网络分析和数据可视化。 总之,Spark SQL是一个强大的数据处理框架,可以快速处理各种结构化数据集,并且可以与其他组件集成,实现高效的数据分析和挖掘。随着大数据时代的到来,Spark SQL将成为数据科学和工程师的必备工具之一。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值