大数据经典实战:商品热门品类TOP10-(spark实战)

 需求:各区域热门商品Top3

这里的热门商品是从点击量的维度来看的,计算各个区域前三大热门商品,并备注上每个商品在主要城市中的分布比例,超过两个城市用其他显示。

例如:

地区

商品名称

点击次数

城市备注

华北

商品A

100000

北京21.2%,天津13.2%,其他65.6%

华北

商品P

80200

北京63.0%,太原10%,其他27.0%

华北

商品M

40000

北京63.0%,太原10%,其他27.0%

东北

商品J

92000

大连28%,辽宁17.0%,其他 55.0%

 

思路分析

  • 使用 sql 来完成,碰到复杂的需求,可以使用 udf 或 udaf
  • 查询出来所有的点击记录,并与 city_info 表连接,得到每个城市所在的地区,与 product_info 表连接得到产品名称
  • 按照地区和商品名称分组,统计出每个商品在每个地区的总点击次数
  • 每个地区内按照点击次数降序排列
  • 只取前三名,并把结果保存在数据库中
  • 城市备注需要自定义 UDAF 函数

数据库的创建与导入

我们这次Spark-sql操作所有的数据均来自 Hive,首先在Hive中创建表,并导入数据。一共有3张表: 1张用户行为表,1张城市表,1 张产品表

CREATE TABLE `user_visit_action`(
  `date` string,
  `user_id` bigint,
  `session_id` string,
  `page_id` bigint,
  `action_time` string,
  `search_keyword` string,
  `click_category_id` bigint,
  `click_product_id` bigint,
  `order_category_ids` string,
  `order_product_ids` string,
  `pay_category_ids` string,
  `pay_product_ids` string,
  `city_id` bigint)
row format delimited fields terminated by '\t';
load data local inpath '/opt/dwdatas/sparkdata/user_visit_action.txt' into table user_visit_action;


CREATE TABLE `product_info`(
  `product_id` bigint,
  `product_name` string,
  `extend_info` string)
row format delimited fields terminated by '\t';
load data local inpath '/opt/dwdatas/sparkdata/product_info.txt' into table product_info;


CREATE TABLE `city_info`(
  `city_id` bigint,
  `city_name` string,
  `area` string)
row format delimited fields terminated by '\t';
load data local inpath '/opt/dwdatas/sparkdata/city_info.txt' into table city_info;

spark实现代码

import org.apache.spark.SparkConf
import org.apache.spark.sql.expressions.Aggregator
import org.apache.spark.sql.{DataFrame, Encoder, Encoders, SparkSession, functions}

import scala.collection.mutable
import scala.collection.mutable.ListBuffer

object sCityUDF {

/**
 *
 * user_visit_action
 *     date                   string  日期
 *     user_id                bigint  用户id
 *     session_id             string  会话id
 *     page_id                bigint  页面id
 *     action_time            string  活动时间
 *     search_keyword         string  搜索关键词
 *     click_category_id      bigint  点击类别id
 *     click_product_id       bigint  点击产品id
 *     order_category_ids     string  订单类别
 *     order_product_ids      string  订单产品
 *     pay_category_ids       string  完成订单类别ids
 *     pay_product_ids        string  完成订单产品ids
 *     city_id                bigint  城市id
 *
 *
 * product_info
 *    product_id              bigint  产品id
 *    product_name            string  产品名
 *    extend_info             string  产品拓展信息
 *
 *
 * city_info
 *    city_id                 bigint  城市id
 *    city_name               string  城市名
 *    area                    string  地区名
 *
 *
 * */

  /**
   * 计算各个区域热门商品Top3,并存储到hive中
   *    现在我们需要先把三张表通过相关信息链接起来,然后再进一步查询
   *    user_visit_info.click_product_id = product_info.product_id
   *    user_visit_info.city_id = city_info.city_id
   *
   * */

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

    // 配置SparkSession对象的基本信息
    val SC = new SparkConf().setMaster("local[*]").setAppName("sCityUDF")   // 运行环境本地,工作名称sCityUDF
    val spark = SparkSession
      .builder()    // 创建环境
      .enableHiveSupport()    // 获取hive支持
      .config(SC)   // 基本配置信息
      .config("hive.metastore.uris", "thrift://192.168.79.132:9083")    // 设置hive元数据
      .getOrCreate()   // 获取或创建一个SparkSession对象。如果已经存在一个SparkSession对象,则返回该对象,否则创建一个新的对象

    // 利用SparkSession对象,进入mydb数据库
    spark.sql("use mydb")

    // 查询基本数据,存储为临时表TV1
    // 这里通过inner join on 根据相关信息,连接了三张表
    // TV1结构为   地区 产品名 城市名
    spark.sql(
      """
        |select c.area, p.product_name, c.city_name
        | from user_visit_action u inner
        | join product_info p on u.click_product_id = p.product_id
        | join city_info c on u.city_id = c.city_id
        |""".stripMargin).createTempView("TV1")

//     //展示TV1
//     spark.sql("select * from TV1").show

    // 根据地区、商品名进行聚合
    //  这段代码是将自定义的聚合函数 CityRemarkUDAF 注册为 Spark 的 UDF (User-Defined Function) ,并命名为 “cityRemark”。
    //  通过调用 spark.udf.register() 方法,将该 UDF 注册到 Spark 中,以便在后续的 Spark SQL 查询中使用。
    //  这样,我们可以在 SQL 中直接调用 “cityRemark” 函数,对数据进行聚合操作
    spark.udf.register("cityRemark", functions.udaf(new CityRemarkUDAF))
    spark.sql(
      """
        |select area, product_name,count(*) clickCnt, cityRemark(city_name) city_remark from
        | TV1 group by area, product_name
        | order by count(*) desc
        |""".stripMargin).createTempView("TV2")

     展示TV2
//     spark.sql("select * from TV2").show

    // 根据地区排名
    spark.sql(
      """
        |select area, product_name, clickCnt, city_remark, row_number() over(partition by area order by clickCnt desc) r
        | from TV2
        |""".stripMargin).createTempView("TV3")

    // 取前三名
    spark.sql(
      """
        |select area, product_name, clickCnt, city_remark from TV3 where r <=3
        |""".stripMargin).createTempView("TV4")

    // 显示查询
    spark.sql("select * from TV4").show(false)    // 设置truncate为false防止因超过20个字符导致显示截取
    // 转换为DataFrame
    val df: DataFrame = spark.sql("select * from TV4")
    // 将数据写入hive中,保存的表名为click_product_hot_top3
    df.write.format("hive").mode("overwrite").saveAsTable("click_product_hot_top3")
    // 关闭资源
    spark.stop()
  }

  // 定义自定义集合,实现城市占比备注功能
  /*
  * 继承Aggregator,定义泛型
  * IN:城市名
  * BUF:Buffer => [总点击数量, Map[(city, cnt), (city, cnt), ......]]
  * OUT:占比备注
  * */

  // 单独定义样例类型 总点击数(长整型), Map[(城市名, 点击数), (城市名, 点击数)]  mutable是scala中的可变类型
  case class Buffer(var total:Long, var cityMap:mutable.Map[String, Long])
  // 定义UDF函数类,继承Aggregator聚合类
  class CityRemarkUDAF extends Aggregator[String, Buffer, String]{

    // 缓冲区初始化
    override def zero: Buffer = {
      // 设置buffer的初始值为0,可变类型map集合为空
      Buffer(0, mutable.Map[String, Long]())
    }

    // 更新缓冲区数据 将更新的buffer返回 buffer来自参数BUF,city来自参数IN
    override def reduce(buffer: Buffer, city: String): Buffer = {
      // 每条数据的总点击数量加1
      buffer.total += 1
      // 在buffer中根据cityMap的key来获取value,如果有值,那就可以得到这个值,如果没有就会得到一个默认值
      val newCount: Long = buffer.cityMap.getOrElse(city, 0L) + 1
      // 更新buffer中cityMap的值
      buffer.cityMap.update(city, newCount)
      // 将buffer返回
      buffer
    }

    // 合并缓冲区数据
    override def merge(b1: Buffer, b2: Buffer): Buffer = {
      //
      b1.total += b2.total

      val map1: mutable.Map[String, Long] = b1.cityMap
      val map2: mutable.Map[String, Long] = b2.cityMap

//      // 两个map的合并操作,方法一
//      b1.cityMap = map1.foldLeft(map2){
//        case (map, (city, cnt)) => {
//          val newCount: Long = map.getOrElse(city, 0L) + cnt
//          map.update(city, newCount)
//          map
//        }
//      }

      // 两个map的合并操作,方法二
      // 用map2遍历
      map2.foreach{
        case (city, cnt) => {
          // 如果发现将相同city的点击数量(cnt)相加,如果map1中没有map2中的city,则设置点击量为0,加上map2的cnt
          val newCount = map1.getOrElse(city, 0L) + cnt
          // 更新map1中的店家量信息
          map1.update(city, newCount)
        }
      }
      // 更新b1中的cityMap信息
      b1.cityMap = map1
      // 将结果返回
      b1
    }

    // 将统计的结果生成字符串信息
    override def finish(reduction: Buffer): String = {
      val remarkList: ListBuffer[String] = ListBuffer[String]()
      val totalCount: Long = reduction.total
      val cityMap: mutable.Map[String, Long] = reduction.cityMap
      // 转化为list进行排序
      val cityCntList: List[(String, Long)] = cityMap.toList.sortWith(
        (i, j) => {
          i._2 > j._2
        }).take(2)    // take(2),只取前两个
      val hasMore: Boolean = cityMap.size > 2
      var rSum = 0L
      cityCntList.foreach{
        case (city, cnt) => {
          val r: Long = cnt * 100 / totalCount
          remarkList.append(s"${city} ${r}%")
          rSum += r
        }
      }
      if (hasMore){
        remarkList.append(s"其它 ${100 - rSum}%")
      }
      remarkList.mkString(",")
    }

    // 固定写法
    override def bufferEncoder: Encoder[Buffer] = Encoders.product

    override def outputEncoder: Encoder[String] = Encoders.STRING
  }


}

你可能感兴趣的:(spark实战,大数据,spark,hive)