一、研究背景

  互联网行业越来越重视自家客户的一些行为偏好了,无论是电商行业还是金融行业,基于用户行为可以做出很多东西,电商行业可以归纳出用户偏好为用户推荐商品,金融行业可以把用户行为作为反欺诈的一个点,本文主要介绍其中一个重要的功能点,基于行为日志统计用户行为路径,为运营人员提供更好的运营决策。可以实现和成熟产品如adobe analysis类似的用户行为路径分析。最终效果如图。使用的是开源大数据可视化工具。如图所示,用户行为路径的数据非常巨大,uv指标又不能提前计算好(时间段未定),如果展示5级,一个页面的数据量就是10的5次方,如果有几千个页面,数据量是无法估量的,所以只能进行实时计算,而Spark非常适合迭代计算,基于这样的考虑,Spark是不错的选择。

二、解决方案

1.流程描述

  客户搜索某一起始页面的行为路径明细数据时,RPC请求到后台,调用spark-submit脚本启动spark程序,Spark程序实时计算并返回数据,前端Java解析数据并展现。

2.准备工作

  1.首先要有行为数据啦,用户行为日志数据必须包含必须包含以下四个字段,访问时间、设备指纹、会话id、页面名称,其中页面名称可以自行定义,用来标示一种或者一类页面,每次用户请求的时候上报此字段,服务器端收集并存储,此页面名称最好不要有重复,为后续分析打下基础。

  2.然后对行为日志进行一级清洗(基于Hive),将数据统一清洗成如下格式。设备指纹是我另一个研究的项目,还没时间贴出来。会话id就是可以定义一个会话超时时间,即20分钟用户如果没有任何动作,等20分钟过后再点击页面就认为这是下个一会话id,可通过cookie来控制此会话id。

设备指纹 会话id 页面路径(按时间升序 时间
fpid1 sessionid1 A_B_C_D_E_F_G 2017-01-13

A、B、C代表页面名称,清洗过程采用row_number函数,concat_ws函数,具体用法可以百度。清洗完之后落地到hive表,后续会用到。T+1清洗此数据。

  3.弄清楚递归的定义

  递归算法是一种直接或者间接调用自身函数或者方法的算法。Java递归算法是基于Java语言实现的递归算法。递归算法的实质是把问题分解成规模缩小的同类问题的子问题,然后递归调用方法来表示问题的解。递归算法对解决一大类问题很有效,它可以使算法简洁和易于理解。递归算法,其实说白了,就是程序的自身调用。它表现在一段程序中往往会遇到调用自身的那样一种coding策略,这样我们就可以利用大道至简的思想,把一个大的复杂的问题层层转换为一个小的和原问题相似的问题来求解的这样一种策略。递归往往能给我们带来非常简洁非常直观的代码形势,从而使我们的编码大大简化,然而递归的思维确实很我们的常规思维相逆的,我们通常都是从上而下的思维问题, 而递归趋势从下往上的进行思维。这样我们就能看到我们会用很少的语句解决了非常大的问题,所以递归策略的最主要体现就是小的代码量解决了非常复杂的问题。

  递归算法解决问题的特点:

  1)递归就是方法里调用自身。   

  2)在使用递增归策略时,必须有一个明确的递归结束条件,称为递归出口。    
  3)递归算法解题通常显得很简洁,但递归算法解题的运行效率较低。所以一般不提倡用递归算法设计程序。
  4)在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储。递归次数过多容易造成栈溢出等,所以一般不提倡用递归算法设计程序。

在做递归算法的时候,一定要把握住出口,也就是做递归算法必须要有一个明确的递归结束条件。这一点是非常重要的。其实这个出口是非常好理解的,就是一个条件,当满足了这个条件的时候我们就不再递归了。

  4.多叉树的基本知识

三、Spark处理

流程概述:

1.构建一个多叉树的类,类主要属性描述,name全路径如A_B_C,childList儿子链表,多叉树的构建和递归参考了这里

2.按时间范围读取上一步预处理的数据,递归计算每一级页面的属性指标,并根据页面路径插入到初始化的Node类根节点中。

3.递归遍历上一步初始化的根节点对象,并替换其中的name的id为名称,其中借助Spark DataFrame查询数据。

4.将root对象转化成json格式,返回前端。

附上代码如下。

import java.util

import com.google.gson.Gson
import org.apache.spark.SparkContext
import org.apache.log4j.{Level, Logger => LG}
import org.apache.spark.sql.DataFrame
import org.apache.spark.sql.hive.HiveContext /**
* 用户行为路径实时计算实现
* Created by chouyarn on 2016/12/12.
*/ /**
* 树结构类
*
* @param name 页面路径
* @param visit 访次
* @param pv pv
* @param uv uv
* @param childList 儿子链表
*/
class Node(
var name: String,
var path:Any,
var visit: Any,
var pv: Any,
var uv: Any,
var childList: util.ArrayList[Node]) extends Serializable {
/**
* 添加子节点
*
* @param node 子节点对象
* @return
*/
def addNode(node: Node) = {
childList.add(node)
} /**
* 遍历节点,深度优先
*/
def traverse(): Unit = {
if (childList.isEmpty)
return
// node.
val childNum = childList.size for (i <- 0 to childNum - 1) {
val child: Node = childList.get(i)
child.name = child.name.split("_").last//去除前边绝对路径
child.traverse()
}
} /**
* 遍历节点,深度优先
*/
def traverse(pages:DataFrame): Unit = {
if (childList.isEmpty||childList.size()==0)
return
// node.
val childNum = childList.size for (i <- 0 to childNum - 1) {
val child: Node = childList.get(i)
child.name = child.name.split("_").last
val id =pages.filter("page_id='"+child.name+"'").select("page_name").first().getString(0)//替换id为name
child.name = id
child.traverse(pages)
}
} /**
* 动态插入节点
*
* @param node 节点对象
* @return
*/
def insertNode(node: Node): Boolean = {
val insertName = node.name
if (insertName.stripSuffix("_" + insertName.split("_").last).equals(name)) {
// node.name=node.name.split("_").last
addNode(node)
true
} else {
val childList1 = childList
val childNum = childList1.size
var insetFlag = false
for (i <- 0 to childNum - 1) {
val childNode = childList1.get(i)
insetFlag = childNode.insertNode(node)
if (insetFlag == true)
true
}
false
}
}
} /**
* 处理类
*/
class Path extends CleanDataWithRDD {
LG.getRootLogger.setLevel(Level.ERROR)//控制spark日志输出级别 val sc: SparkContext = SparkUtil.createSparkContextYarn("path")
val hiveContext = new HiveContext(sc) override def handleData(conf: Map[String, String]): Unit = { val num = conf.getOrElse("depth", 5)//路径深度
val pageName = conf.getOrElse("pageName", "")//页面名称
// val pageName = "A_C"
val src = conf.getOrElse("src", "")//标示来源pc or wap val pageType = conf.getOrElse("pageType", "")//向前或者向后路径
val startDate = conf.getOrElse("startDate", "")//开始日期
val endDate = conf.getOrElse("endDate", "")//结束日期
// 保存log缓存以保证后续使用
val log = hiveContext.sql(s"select fpid,sessionid,path " +
s"from specter.t_pagename_path_sparksource " +
s"where day between '$startDate' and '$endDate' and path_type=$pageType and src='$src' ")
.map(s => {
(s.apply(0) + "_" + s.apply(1) + "_" + s.apply(2))
}).repartition(10).persist() val pages=hiveContext.sql("select page_id,page_name from specter.code_pagename").persist()//缓存页面字典表
// 本地测试数据
// val log = sc.parallelize(Seq("fpid1_sessionid1_A_B",
// "fpid2_sessionid2_A_C_D_D_B_A_D_A_F_B",
// "fpid1_sessionid1_A_F_A_C_D_A_B_A_V_A_N"))
var root: Node = null
/**
* 递归将计算的节点放入树结构
*
* @param pageName 页面名称
*/
def compute(pageName: String): Unit = {
val currenRegex = pageName.r //页面的正则表达式
val containsRdd = log.filter(_.contains(pageName)).persist() //包含页面名称的RDD,后续步骤用到
val currentpv = containsRdd.map(s => {//计算pv
currenRegex findAllIn (s)
}).map(_.mkString(","))
.flatMap(_.toString.split(","))
.filter(_.size > 0)
.count() val tempRdd = containsRdd.map(_.split("_")).persist() //分解后的RDD
val currentuv = tempRdd.map(_.apply(0)).distinct().count() //页面uv
val currentvisit = tempRdd.map(_.apply(1)).distinct().count() //页面访次 // 初始化根节点或添加节点
if (root == null) {
root = new Node(pageName,pageName.hashCode, currentvisit, currentpv, currentuv, new util.ArrayList[Node]())
} else {
root.insertNode(new Node(pageName,pageName.hashCode, currentvisit, currentpv, currentuv, new util.ArrayList[Node]()))
} if (pageName.split("_").size == 5||tempRdd.isEmpty()) {//递归出口
return
} else {
// 确定下个页面名称正则表达式
val nextRegex =
s"""${pageName}_[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}""".r
// 本地测试
// val nextRegex =s"""${pageName}_[A-Z]""".r
val nextpvMap = containsRdd.map(s => {//下一级路径的pv数top9
nextRegex findAllIn (s)
}).map(_.mkString(","))
.flatMap(_.toString.split(","))
.filter(_.size > 0)
.map(s => (s.split("_").last, 1))
.filter(!_._1.contains(pageName.split("_")(0)))
.reduceByKey(_ + _).sortBy(_._2, false).take(9).toMap nextpvMap.keySet.foreach(key => {//递归计算
compute(pageName + "_" + key)
})
}
}
//触发计算
compute(pageName)
val gson: Gson = new Gson() root.traverse(pages)
root.name=pages.filter("page_id='"+pageName+"'").select("page_name").first().getString(0)
println(gson.toJson(root))//转化成JSON并打印,Alibaba fsatjson不可用,还是google得厉害。
} override def stop(): Unit = {
sc.stop()
}
} object Path {
def main(args: Array[String]): Unit = {
// println("ss".hashCode)
var num=5
try {
num=args(5).toInt
}catch {
case e:Exception =>
} val map = Map("pageName" -> args(0),
"pageType" -> args(1),
"startDate" -> args(2),
"endDate" -> args(3),
"src" -> args(4),
"depth" -> num.toString)
val path = new Path()
path.handleData(map)
}
}

四、总结

  Spark基本是解决了实时计算行为路径的问题,缺点就是延迟稍微有点高,因为提交Job之后要向集群申请资源,申请资源和启动就耗费将近30秒,后续这块可以优化。据说spark-jobserver提供一个restful接口,为Job预启动容器,博主没时间研究有兴趣的可以研究下啦。

  fastjson在对复杂对象的转换中不如Google 的Gson。

  使用递归要慎重,要特别注意出口条件,若出口不明确,很有可能导致死循环。

基于Spark的用户行为路径分析的更多相关文章

  1. 基于Spark的用户分析系统

    https://blog.csdn.net/ytbigdata/article/details/47154529

  2. 客户流失?来看看大厂如何基于spark+机器学习构建千万数据规模上的用户留存模型 ⛵

    作者:韩信子@ShowMeAI 大数据技术 ◉ 技能提升系列:https://www.showmeai.tech/tutorials/84 行业名企应用系列:https://www.showmeai. ...

  3. 基于Spark ALS构建商品推荐引擎

    基于Spark ALS构建商品推荐引擎   一般来讲,推荐引擎试图对用户与某类物品之间的联系建模,其想法是预测人们可能喜好的物品并通过探索物品之间的联系来辅助这个过程,让用户能更快速.更准确的获得所需 ...

  4. 苏宁基于Spark Streaming的实时日志分析系统实践 Spark Streaming 在数据平台日志解析功能的应用

    https://mp.weixin.qq.com/s/KPTM02-ICt72_7ZdRZIHBA 苏宁基于Spark Streaming的实时日志分析系统实践 原创: AI+落地实践 AI前线 20 ...

  5. 基于 Spark 的文本情感分析

    转载自:https://www.ibm.com/developerworks/cn/cognitive/library/cc-1606-spark-seniment-analysis/index.ht ...

  6. 基于Spark机器学习和实时流计算的智能推荐系统

    概要: 随着电子商务的高速发展和普及应用,个性化推荐的推荐系统已成为一个重要研究领域. 个性化推荐算法是推荐系统中最核心的技术,在很大程度上决定了电子商务推荐系统性能的优劣,决定着是否能够推荐用户真正 ...

  7. 基于Spark的电影推荐系统(电影网站)

    第一部分-电影网站: 软件架构: SpringBoot+Mybatis+JSP 项目描述:主要实现电影网站的展现 和 用户的所有动作的地方 技术选型: 技术 名称 官网 Spring Boot 容器 ...

  8. 基于Spark的电影推荐系统(实战简介)

    写在前面 一直不知道这个专栏该如何开始写,思来想去,还是暂时把自己对这个项目的一些想法 和大家分享 的形式来展现.有什么问题,欢迎大家一起留言讨论. 这个项目的源代码是在https://github. ...

  9. 基于Spark的电影推荐系统(推荐系统~7)

    基于Spark的电影推荐系统(推荐系统~7) 22/100 发布文章 liuge36 第四部分-推荐系统-实时推荐 本模块基于第4节得到的模型,开始为用户做实时推荐,推荐用户最有可能喜爱的5部电影. ...

随机推荐

  1. PCIe固态存储和HDD常见的硬盘性能对比测试

    2周测试后,导致以下结果 MySQL-OLTP测试结果:(50表.每个表1000广域网数据,1000个线程) TPS:MySQL在PCIe固态存储上执行是在HDD上执行的5.63倍 writes:My ...

  2. fatjar eclipse4.4 java项目的jar包一起打包 net.sf.fjep.fatjar_0.0.32.jar

    1.下载net.sf.fjep.fatjar_0.0.32.jar  http://files.cnblogs.com/files/milanmi/net.sf.fjep.fatjar_0.0.32. ...

  3. BackgroundWorker组件使用总结

    首先在窗体拖入一个BackgroundWorker组件,根据功能需要设置BackgroundWorker的属性 WorkerSupportsCancellation = true; 允许取消后台正在执 ...

  4. Linux 安装配置maven3.0 以及搭建nexus私服

    http://carvin.iteye.com/blog/785365 一.软件准备 1.apache-maven-3.0-bin.tar.gz 下载地址:http://www.apache.org/ ...

  5. 脚手架快速搭建springMVC框架项目

    apid-framework脚手架快速搭建springMVC框架项目   rapid-framework介绍:   一个类似ruby on rails的java web快速开发脚手架,本着不重复发明轮 ...

  6. 关于CSS reset的思考

    关于CSS reset的思考 在现在的网站设计中使用reset.css用重置整个站点的标签的CSS属性的做法很常见,但有时候我们已经为了reset而reset,我们经常看到这样的reset代码 div ...

  7. iOS基础 - 类扩展

    一.类扩展(class extension,匿名分类) 1.格式 @interface 类名 () { // 成员变量... } // 方法声明... @end 2.作用 1> 写在.m文件中 ...

  8. discuz 门户功能增加自定义keywords字段

    discuz的门户的“发布文章”功能中,没有自动添加keywords字段,结果在文章页面中的meta的keywords中只显示标题,这样对于seo及其不利,今天整理了添加keywords字段方法. 一 ...

  9. namespace 的作用

    在写CPP的时候,常常要写using namespace std;这么一句话,到底有什么用呢? #include <iostream> namespace first { ; } name ...

  10. ios7上隐藏status bar

    在iOS7上 对于设置status bar 又有了点点的改变 1.对于 UIViewController 加入了动态改变 status bar style的方法 - (UIStatusBarStyle ...