1、模块二介绍——页面单跳转化率
页面单跳转化率,计算出来以后,还是蛮有用的,蛮有价值的。
产品经理,可以根据这个指标,去尝试分析,整个网站,产品,各个页面的表现怎么样,是不是需要去优化产品的布局;吸引用户最终可以进入最后的支付页面;
数据分析师,可以基于咱们的这个数据,做更深一步的计算和分析
企业管理层,可以看到整个公司的网站,各个页面的之间的跳转的表现,如何?心里有数,可以适当调整公司的经营战略或策略
2、需求分析
基本的需求:
1、接收J2EE系统传入进来的taskid,从mysql查询任务的参数,日期范围、页面流id
2、针对指定范围日期内的用户访问行为数据,去判断和计算,页面流id中,每两个页面组成的页面切片,它的访问量是多少
3、根据指定页面流中各个页面切片的访问量,计算出来各个页面切片的转化率
4、计算出来的转化率,写入mysql数据库中
3、技术方案设计
用户指定的页面流id:
3,5,7,9,10,21
页面3->页面5的转换率是多少;
页面5->页面7的转化率是多少;
页面7->页面9的转化率是多少;
页面3->页面5的访问量是多少;页面5到页面7的访问量是多少;两两相除,就可以计算出来
实现步骤:
1、获取任务的日期范围参数
2、查询指定日期范围内的用户访问行为数据
3、获取用户访问行为中,每个session,计算出各个在指定页面流中的页面切片的访问量;实现,页面单跳切片生成以及页面流匹配的算法;session,3->8->7,3->5->7,是不匹配的;
4、计算出符合页面流的各个切片的pv(访问量)
5、针对用户指定的页面流,去计算各个页面单跳切片的转化率
6、将计算结果持久化到数据库中
数据表设计
4、代码实现
4.1 页面切片生成以及页面流量匹配算法
package cn.ctgu.sparkproject.spark.page;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.api.java.function.PairFlatMapFunction;
import org.apache.spark.api.java.function.PairFunction;
import org.apache.spark.broadcast.Broadcast;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.SQLContext;
import scala.Tuple2;
import com.alibaba.fastjson.JSONObject;
import cn.ctgu.sparkproject.constant.Constants;
import cn.ctgu.sparkproject.dao.ITaskDAO;
import cn.ctgu.sparkproject.dao.factory.DAOFactory;
import cn.ctgu.sparkproject.domain.Task;
import cn.ctgu.sparkproject.util.DateUtils;
import cn.ctgu.sparkproject.util.ParamUtils;
import cn.ctgu.sparkproject.util.SparkUtils;
/**
* 页面单跳转化率模块spark作业
* @author Administrator
*
*/
public class PageOneStepConvertRateSpark {
public static void main(String[] args) {
// 1、构造Spark上下文
SparkConf conf = new SparkConf()
.setAppName(Constants.SPARK_APP_NAME_PAGE);
SparkUtils.setMaster(conf);
JavaSparkContext sc = new JavaSparkContext(conf);
SQLContext sqlContext = SparkUtils.getSQLContext(sc.sc());
// 2、生成模拟数据
SparkUtils.mockData(sc, sqlContext);
// 3、查询任务,获取任务的参数
long taskid = ParamUtils.getTaskIdFromArgs(args, Constants.SPARK_LOCAL_TASKID_PAGE);
ITaskDAO taskDAO = DAOFactory.getTaskDAO();
Task task = taskDAO.findById(taskid);
if(task == null) {
System.out.println(new Date() + ": cannot find this task with id [" + taskid + "].");
return;
}
JSONObject taskParam = JSONObject.parseObject(task.getTaskParam());
// 4、查询指定日期范围内的用户访问行为数据
JavaRDD<Row> actionRDD = SparkUtils.getActionRDDByDateRange(
sqlContext, taskParam);
// 对用户访问行为数据做一个映射,将其映射为<sessionid,访问行为>的格式
// 咱们的用户访问页面切片的生成,是要基于每个session的访问数据,来进行生成的
// 脱离了session,生成的页面访问切片,是么有意义的
// 举例,比如用户A,访问了页面3和页面5
// 用于B,访问了页面4和页面6
// 漏了一个前提,使用者指定的页面流筛选条件,比如页面3->页面4->页面7
// 你能不能说,是将页面3->页面4,串起来,作为一个页面切片,来进行统计呢
// 当然不行
// 所以说呢,页面切片的生成,肯定是要基于用户session粒度的
JavaPairRDD<String, Row> sessionid2actionRDD = getSessionid2actionRDD(actionRDD);
// 对<sessionid,访问行为> RDD,做一次groupByKey操作
// 因为我们要拿到每个session对应的访问行为数据,才能够去生成切片
JavaPairRDD<String, Iterable<Row>> sessionid2actionsRDD = sessionid2actionRDD.groupByKey();
// 最核心的一步,每个session的单跳页面切片的生成,以及页面流的匹配,算法
JavaPairRDD<String, Integer> pageSplitRDD = generateAndMatchPageSplit(
sc, sessionid2actionsRDD, taskParam);
Map<String, Object> pageSplitPvMap = pageSplitRDD.countByKey();
}
/**
* 获取<sessionid,用户访问行为>格式的数据
* @param actionRDD 用户访问行为RDD
* @return <sessionid,用户访问行为>格式的数据
*/
private static JavaPairRDD<String, Row> getSessionid2actionRDD(
JavaRDD<Row> actionRDD) {
return actionRDD.mapToPair(new PairFunction<Row, String, Row>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Row> call(Row row) throws Exception {
String sessionid = row.getString(2);
return new Tuple2<String, Row>(sessionid, row);
}
});
}
/**
* 页面切片生成与匹配算法
* @param sc
* @param sessionid2actionsRDD
* @param taskParam
* @return
*/
private static JavaPairRDD<String, Integer> generateAndMatchPageSplit(
JavaSparkContext sc,
JavaPairRDD<String, Iterable<Row>> sessionid2actionsRDD,
JSONObject taskParam) {
String targetPageFlow = ParamUtils.getParam(taskParam, Constants.PARAM_TARGET_PAGE_FLOW);
final Broadcast<String> targetPageFlowBroadcast = sc.broadcast(targetPageFlow);
return sessionid2actionsRDD.flatMapToPair(
new PairFlatMapFunction<Tuple2<String,Iterable<Row>>, String, Integer>() {
private static final long serialVersionUID = 1L;
@Override
public Iterable<Tuple2<String, Integer>> call(
Tuple2<String, Iterable<Row>> tuple)
throws Exception {
// 定义返回list
List<Tuple2<String, Integer>> list =
new ArrayList<Tuple2<String, Integer>>();
// 获取到当前session的访问行为的迭代器
Iterator<Row> iterator = tuple._2.iterator();
// 获取使用者指定的页面流
// 使用者指定的页面流,1,2,3,4,5,6,7
// 1->2的转化率是多少?2->3的转化率是多少?
String[] targetPages = targetPageFlowBroadcast.value().split(",");
// 这里,我们拿到的session的访问行为,默认情况下是乱序的
// 比如说,正常情况下,我们希望拿到的数据,是按照时间顺序排序的
// 但是问题是,默认是不排序的
// 所以,我们第一件事情,对session的访问行为数据按照时间进行排序
// 举例,反例
// 比如,3->5->4->10->7
// 3->4->5->7->10
// 排序
List<Row> rows = new ArrayList<Row>();
while(iterator.hasNext()) {
rows.add(iterator.next());
}
Collections.sort(rows, new Comparator<Row>() {
@Override
public int compare(Row o1, Row o2) {
String actionTime1 = o1.getString(4);
String actionTime2 = o2.getString(4);
Date date1 = DateUtils.parseTime(actionTime1);
Date date2 = DateUtils.parseTime(actionTime2);
return (int)(date1.getTime() - date2.getTime());
}
});
// 页面切片的生成,以及页面流的匹配
Long lastPageId = null;
for(Row row : rows) {
long pageid = row.getLong(3);
if(lastPageId == null) {
lastPageId = pageid;
continue;
}
// 生成一个页面切片
// 3,5,2,1,8,9
// lastPageId=3
// 5,切片,3_5
String pageSplit = lastPageId + "_" + pageid;
// 对这个切片判断一下,是否在用户指定的页面流中
for(int i = 1; i < targetPages.length; i++) {
// 比如说,用户指定的页面流是3,2,5,8,1
// 遍历的时候,从索引1开始,就是从第二个页面开始
// 3_2
String targetPageSplit = targetPages[i - 1] + "_" + targetPages[i];
if(pageSplit.equals(targetPageSplit)) {
list.add(new Tuple2<String, Integer>(pageSplit, 1));
break;
}
}
lastPageId = pageid;
}
return list;
}
});
}
}
4.2 计算页面流其实页面的pv
/*
* 获取页面流中初始页面的pv
*
* */
private static long getStartPagePv(JSONObject taskParam,
JavaPairRDD<String,Iterable<Row>>sessionid2actionRDD) {
String targetPageFlow=ParamUtils.getParam(taskParam,
Constants.PARAM_TARGET_PAGE_FLOW);
final long startPageId=Long.valueOf(targetPageFlow.split(",")[0]);
JavaRDD<Long>startPageRDD=sessionid2actionRDD.flatMap(
new FlatMapFunction<Tuple2<String,Iterable<Row>>,Long>() {
private static final long serialVersionUID = 1L;
@Override
public Iterable<Long> call(
Tuple2<String, Iterable<Row>> tuple)
throws Exception {
List<Long>list=new ArrayList<Long>();
Iterator<Row>iterator=tuple._2.iterator();
while(iterator.hasNext()) {
Row row=iterator.next();
long pageid=row.getLong(3);
if(pageid==startPageId) {
list.add(pageid);
}
}
return list;
}
});
return startPageRDD.count();
}
4.3 计算页面切片的转化率
/*
* 计算页面切片转化率
*
* */
private static Map<String,Double>computePageSplitConvertRate(
JSONObject taskParam,
Map<String,Object>pageSplitPvMap,
long startPagePv){
Map<String,Double>convertRateMap=new HashMap<String,Double>();
String[] targetPages=ParamUtils.getParam(taskParam,
Constants.PARAM_TARGET_PAGE_FLOW).split(",");
long lastPageSplitPv=0L;
//3,5,2,4,6
//3_5
//转化率:3_5 pv /3 pv
//5_2 rate=5_2 pv/3_5 pv
//通过for循环,获取目标页面流中的各个页面切片(pv)
for(int i=1;i<targetPages.length;i++) {
String targetPageSplit=targetPages[i-1]+"_"+targetPages[i];
long targetPageSplitPv=Long.valueOf(String.valueOf(
pageSplitPvMap.get(targetPageSplit)));
double convertRate=0.0;
if(i==1) {
convertRate=NumberUtils.formatDouble(
(double)targetPageSplitPv/(double)startPagePv, 2);
}else {
convertRate=NumberUtils.formatDouble(
(double)(targetPageSplitPv)/(double)lastPageSplitPv, 2);
}
convertRateMap.put(targetPageSplit, convertRate);
lastPageSplitPv=targetPageSplitPv;
}
return convertRateMap;
}
4.4 将转化率写进MySQL
/*
* 持久化转化率
*
* */
private static void persistConvertRate(long taskid,
Map<String,Double>convertRateMap) {
StringBuffer buffer=new StringBuffer("");
for(Map.Entry<String, Double>convertRateEntry:convertRateMap.entrySet()) {
String pageSplit=convertRateEntry.getKey();
double convertRate=convertRateEntry.getValue();
buffer.append(pageSplit+"="+convertRate+"|");
}
String convertRate=buffer.toString();
convertRate=convertRate.substring(0,convertRate.length()-1);
PageSplitConvertRate pageSplitConvertRate=new PageSplitConvertRate();
pageSplitConvertRate.setTaskid(taskid);
pageSplitConvertRate.setConvertRate(convertRate);
IPageSplitConvertRateDAO pageSplitConvertRateDAO=DAOFactory.getPageSplitConvertRateDAO();
pageSplitConvertRateDAO.insert(pageSplitConvertRate);
}
5、完整代码
package cn.ctgu.sparkproject.spark.page;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.api.java.function.FlatMapFunction;
import org.apache.spark.api.java.function.PairFlatMapFunction;
import org.apache.spark.api.java.function.PairFunction;
import org.apache.spark.broadcast.Broadcast;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.SQLContext;
import com.alibaba.fastjson.JSONObject;
import cn.ctgu.sparkproject.constant.Constants;
import cn.ctgu.sparkproject.dao.IPageSplitConvertRateDAO;
import cn.ctgu.sparkproject.dao.ITaskDAO;
import cn.ctgu.sparkproject.dao.factory.DAOFactory;
import cn.ctgu.sparkproject.domain.PageSplitConvertRate;
import cn.ctgu.sparkproject.domain.Task;
import cn.ctgu.sparkproject.util.DateUtils;
import cn.ctgu.sparkproject.util.NumberUtils;
import cn.ctgu.sparkproject.util.ParamUtils;
import cn.ctgu.sparkproject.util.SparkUtils;
import scala.Tuple2;
/*
* 页面单跳转化率模块
*
*
* */
public class PageOneStepConvertRateSpark {
public static void main(String[] args) {
//1、构造spark上下文
SparkConf conf=new SparkConf()
.setAppName(Constants.SPARK_APP_NAME_PAGE);
SparkUtils.setMaster(conf);
JavaSparkContext sc=new JavaSparkContext();
SQLContext sqlContext=SparkUtils.getSQLContext(sc.sc());
//2、生成模拟数据
SparkUtils.mockData(sc, sqlContext);
//3、查询任务,获取任务的参数
long taskid=ParamUtils.getTaskIdFromArgs(args,Constants.SPARK_LOCAL_TASKID_PAGE);
ITaskDAO taskDAO=DAOFactory.getTaskDAO();
Task task=taskDAO.findById(taskid);
if(task==null) {
System.out.println(new Date()+": cannot find this task with id["+taskid+"].");
return;
}
JSONObject taskParam=JSONObject.parseObject(task.getTaskParam());
//4、查询指定日期范围内的用户访问行为数据
JavaRDD<Row>actionRDD=SparkUtils.getActionRDDByDateRange(sqlContext, taskParam);
/*
* 对用户访问行为数据做一个映射,将其映射为<sessionid,访问行为>的格式
* 咱们的用户访问页面切片的生成是要基于每个session的访问数据来进行生成的
* 脱离了session,生成的页面访问切片是没有意义的
* 比如:用户A访问了页面3和页面5
* 用户B访问了页面4和页面6
* 漏了一个前提,使用者指定的页面筛选条件,比如页面3->页面4->页面7
* 将页面3->页面4穿起来作为一个页面切片来进行统计是不行的
* 所以说页面切片的生成是要基于用户session粒度的
*
* */
JavaPairRDD<String,Row>sessionid2actionRDD=getSessionid2actionRDD(actionRDD);
sessionid2actionRDD=sessionid2actionRDD.cache();//相当于persist(StorageLevl.MEMORY_ONLY)
//对<sessionid,访问行为>RDD,做一次groupByKey操作
//因为要拿到每个session对应的访问行为数据才能够去生成切片
JavaPairRDD<String,Iterable<Row>>sessionid2actionsRDD=sessionid2actionRDD.groupByKey();
//最核心的一步,每个session的单跳页面切片的生成以及页面流的匹配算法
JavaPairRDD<String,Integer>pageSplitRDD=generateAndMatchPageSplit(
sc,sessionid2actionsRDD,taskParam);
Map<String,Object> pageSplitPvMap=pageSplitRDD.countByKey();
//使用者指定的页面流是3,2,5,8,6
//咱们现在拿到的这个pageSplitPvMap就是3->2,2->5,5->8,8->6
long startPagePv=getStartPagePv(taskParam,sessionid2actionsRDD);
//计算目标页面流的各个页面切片的转化率
Map<String,Double>convertRateMap=computePageSplitConvertRate(
taskParam,pageSplitPvMap,startPagePv);
//持久化页面切片转化率
persistConvertRate(taskid, convertRateMap);
}
/*
* 获取<sessionid,用户访问行为>格式的数据
*
* */
private static JavaPairRDD<String,Row> getSessionid2actionRDD(
JavaRDD<Row>actionRDD) {
return actionRDD.mapToPair(new PairFunction<Row,String,Row>(){
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Row> call(Row row) throws Exception {
String sessionid=row.getString(2);
return new Tuple2<String,Row>(sessionid,row);
}
});
}
/*
* 页面切片生成与匹配算法
*
* */
private static JavaPairRDD<String,Integer>generateAndMatchPageSplit(
JavaSparkContext sc,
JavaPairRDD<String,Iterable<Row>>sessionid2actionsRDD,
JSONObject taskParam){
String targetPageFlow=ParamUtils.getParam(taskParam, Constants.PARAM_TARGET_PAGE_FLOW);
final Broadcast<String>targetPageFlowBroadcast=sc.broadcast(targetPageFlow);
return sessionid2actionsRDD.flatMapToPair(
new PairFlatMapFunction<Tuple2<String,Iterable<Row>>,String,Integer>(){
private static final long serialVersionUID = 1L;
@Override
public Iterable<Tuple2<String, Integer>> call(
Tuple2<String, Iterable<Row>> tuple)
throws Exception {
//定义返回list
List<Tuple2<String,Integer>>list=
new ArrayList<Tuple2<String,Integer>>();
//获取到当前session的访问行为的迭代器
Iterator<Row>iterator=tuple._2.iterator();
//获取使用者指定的页面流
//使用者指定的页面流,1,2,3,4,5,6,7
//1->2的转化率是多少?2->3的转化率是多少?
String[] targetPages=targetPageFlowBroadcast.value().split(",");
//这里,我们拿到的session的访问行为,默认情况下是乱序的
//比如说,正常情况下,我们希望拿到的数据是按照时间顺序排序的
//但是问题是,默认是不排序的
//所以需要对session的访问行为数据按照时间进行排序
//比如:访问的session顺序为:3->5->4->10->7
//但是我们拿到的可能是:3->4->5->7->10
//所以需要根据时间进行排序
List<Row>rows=new ArrayList<Row>();
while(iterator.hasNext()) {
rows.add(iterator.next());
}
Collections.sort(rows,new Comparator<Row>() {
@Override
public int compare(Row o1, Row o2) {
String actionTime1=o1.getString(4);
String actionTime2=o2.getString(4);
Date date1=DateUtils.parseTime(actionTime1);
Date date2=DateUtils.parseTime(actionTime2);
return (int)(date1.getTime()-date2.getTime());
}
});
//页面切片的生成和页面流的匹配
Long lastPageId=null;
for(Row row:rows) {
long pageid=row.getLong(3);
if(lastPageId==null) {
lastPageId=pageid;
continue;
}
//生成一个页面切片(页面切片的对象是两个连着的页面)
//比如:访问顺序为 3,5,2,1,8,9
//lastPageId=3
//5,切片为3_5
String pageSplit=lastPageId+"_"+pageid;
//对这个切片判断一下,是否在用户指定的页面流中
for(int i=1;i<targetPages.length;i++) {
//比如说用户指定的页面流是3,2,5,8,1
//遍历的时候,从索引1开始就是从第二个页面开始
//3_2 2_5 5_8
String targetPageSplit=targetPages[i-1]+"_"+targetPages[i];
if(pageSplit.equals(targetPageSplit)) {
//匹配成功
list.add(new Tuple2<String,Integer>(pageSplit,1));
break;
}
}
lastPageId=pageid;
}
return list;
}
});
}
/*
* 获取页面流中初始页面的pv
*
* */
private static long getStartPagePv(JSONObject taskParam,
JavaPairRDD<String,Iterable<Row>>sessionid2actionRDD) {
String targetPageFlow=ParamUtils.getParam(taskParam,
Constants.PARAM_TARGET_PAGE_FLOW);
final long startPageId=Long.valueOf(targetPageFlow.split(",")[0]);
JavaRDD<Long>startPageRDD=sessionid2actionRDD.flatMap(
new FlatMapFunction<Tuple2<String,Iterable<Row>>,Long>() {
private static final long serialVersionUID = 1L;
@Override
public Iterable<Long> call(
Tuple2<String, Iterable<Row>> tuple)
throws Exception {
List<Long>list=new ArrayList<Long>();
Iterator<Row>iterator=tuple._2.iterator();
while(iterator.hasNext()) {
Row row=iterator.next();
long pageid=row.getLong(3);
if(pageid==startPageId) {
list.add(pageid);
}
}
return list;
}
});
return startPageRDD.count();
}
/*
* 计算页面切片转化率
*
* */
private static Map<String,Double>computePageSplitConvertRate(
JSONObject taskParam,
Map<String,Object>pageSplitPvMap,
long startPagePv){
Map<String,Double>convertRateMap=new HashMap<String,Double>();
String[] targetPages=ParamUtils.getParam(taskParam,
Constants.PARAM_TARGET_PAGE_FLOW).split(",");
long lastPageSplitPv=0L;
//3,5,2,4,6
//3_5
//转化率:3_5 pv /3 pv
//5_2 rate=5_2 pv/3_5 pv
//通过for循环,获取目标页面流中的各个页面切片(pv)
for(int i=1;i<targetPages.length;i++) {
String targetPageSplit=targetPages[i-1]+"_"+targetPages[i];
long targetPageSplitPv=Long.valueOf(String.valueOf(
pageSplitPvMap.get(targetPageSplit)));
double convertRate=0.0;
if(i==1) {
convertRate=NumberUtils.formatDouble(
(double)targetPageSplitPv/(double)startPagePv, 2);
}else {
convertRate=NumberUtils.formatDouble(
(double)(targetPageSplitPv)/(double)lastPageSplitPv, 2);
}
convertRateMap.put(targetPageSplit, convertRate);
lastPageSplitPv=targetPageSplitPv;
}
return convertRateMap;
}
/*
* 持久化转化率
*
* */
private static void persistConvertRate(long taskid,
Map<String,Double>convertRateMap) {
StringBuffer buffer=new StringBuffer("");
for(Map.Entry<String, Double>convertRateEntry:convertRateMap.entrySet()) {
String pageSplit=convertRateEntry.getKey();
double convertRate=convertRateEntry.getValue();
buffer.append(pageSplit+"="+convertRate+"|");
}
String convertRate=buffer.toString();
convertRate=convertRate.substring(0,convertRate.length()-1);
PageSplitConvertRate pageSplitConvertRate=new PageSplitConvertRate();
pageSplitConvertRate.setTaskid(taskid);
pageSplitConvertRate.setConvertRate(convertRate);
IPageSplitConvertRateDAO pageSplitConvertRateDAO=DAOFactory.getPageSplitConvertRateDAO();
pageSplitConvertRateDAO.insert(pageSplitConvertRate);
}
}
cn.ctgu.sparkproject.constant.Constants
package cn.ctgu.sparkproject.constant;
//常量接口
public interface Constants {
/*
* 项目配置相关的常量
* */
String JDBC_DRIVER="jdbc.driver";
String JDBC_DATASOURCE_SIZE="jdbc.datasource.size";
String JDBC_URL="jdbc.url";
String JDBC_USER="jdbc.user";
String JDBC_PASSWORD="jdbc.password";
String JDBC_URL_PROD="jdbc.url.prod";
String JDBC_USER_PROD="jdbc.user.prod";
String JDBC_PASSWORD_PROD="jdbc.password.prod";
String SPARK_LOCAL="spark.local";
String SPARK_LOCAL_TASKID_SESSION="spark.local.taskid.session";
String SPARK_LOCAL_TASKID_PAGE="spark.local.taskid.page";
/*
* spark作业相关的常量
* */
String SPARK_APP_NAME_SESSION="UserVisitSessionAnalyzeSpark";
String SPARK_APP_NAME_PAGE="PageOneStepConvertRateSpark";
String FIELD_SESSION_ID="sessionid";
String FIELD_SEARCH_KEYWORDS="searchKeywords";
String FIELD_CLICK_CATEGORY_IDS="clickCategoryIds";
String FIELD_AGE="age";
String FIELD_PROFESSIONAL="professional";
String FIELD_CITY="city";
String FIELD_SEX="sex";
String FIELD_VISIT_LENGTH="visitLength";
String FIELD_STEP_LENGTH="stepLength";
String FIELD_START_TIME="startTime";
String FIELD_CLICK_COUNT="clickCount";
String FIELD_ORDER_COUNT="orderCount";
String FIELD_PAY_COUNT="payCount";
String FIELD_CATEGORY_ID="categoryid";
String SESSION_COUNT = "session_count";
String TIME_PERIOD_1s_3s = "1s_3s";
String TIME_PERIOD_4s_6s = "4s_6s";
String TIME_PERIOD_7s_9s = "7s_9s";
String TIME_PERIOD_10s_30s = "10s_30s";
String TIME_PERIOD_30s_60s = "30s_60s";
String TIME_PERIOD_1m_3m = "1m_3m";
String TIME_PERIOD_3m_10m = "3m_10m";
String TIME_PERIOD_10m_30m = "10m_30m";
String TIME_PERIOD_30m = "30m";
String STEP_PERIOD_1_3 = "1_3";
String STEP_PERIOD_4_6 = "4_6";
String STEP_PERIOD_7_9 = "7_9";
String STEP_PERIOD_10_30 = "10_30";
String STEP_PERIOD_30_60 = "30_60";
String STEP_PERIOD_60 = "60";
/*
* 任务相关的常量
* */
String PARAM_START_DATE="startDate";
String PARAM_END_DATE="endDate";
String PARAM_START_AGE="startAge";
String PARAM_END_AGE="endAge";
String PARAM_PROFESSIONALS="professionals";
String PARAM_CITIES="cities";
String PARAM_SEX="sex";
String PARAM_KEYWORDS="keywords";
String PARAM_CATEGORY_IDS="categoryIds";
String PARAM_TARGET_PAGE_FLOW="targetPageFlow";
}
cn.ctgu.sparkproject.dao.IPageSplitConvertRateDAO
package cn.ctgu.sparkproject.dao;
import cn.ctgu.sparkproject.domain.PageSplitConvertRate;
/*
* 页面切片转化率DAO接口
*
* */
public interface IPageSplitConvertRateDAO {
void insert(PageSplitConvertRate pageSplitConvertRate);
}
cn.ctgu.sparkproject.dao.impl.PageSplitConvertRateDAOImpl
package cn.ctgu.sparkproject.dao.impl;
import cn.ctgu.sparkproject.dao.IPageSplitConvertRateDAO;
import cn.ctgu.sparkproject.domain.PageSplitConvertRate;
import cn.ctgu.sparkproject.jdbc.JDBCHelper;
/*
* 页面切片转化率DAO实现类
*
* */
public class PageSplitConvertRateDAOImpl implements IPageSplitConvertRateDAO{
@Override
public void insert(PageSplitConvertRate pageSplitConvertRate) {
String sql="insert into page_split_convert_rate value(?,?)";
Object[]params=new Object[] {pageSplitConvertRate.getTaskid(),
pageSplitConvertRate.getConvertRate()};
JDBCHelper jdbcHelper=JDBCHelper.getInstance();
jdbcHelper.executeUpdate(sql, params);
}
}
cn.ctgu.sparkproject.dao.factory.DAOFactory
package cn.ctgu.sparkproject.dao.factory;
import cn.ctgu.sparkproject.dao.IPageSplitConvertRateDAO;
import cn.ctgu.sparkproject.dao.ISessionAggrStatDAO;
import cn.ctgu.sparkproject.dao.ISessionDetailDAO;
import cn.ctgu.sparkproject.dao.ISessionRandomExtractDAO;
import cn.ctgu.sparkproject.dao.ITaskDAO;
import cn.ctgu.sparkproject.dao.ITop10CategoryDAO;
import cn.ctgu.sparkproject.dao.ITop10SessionDAO;
import cn.ctgu.sparkproject.dao.impl.PageSplitConvertRateDAOImpl;
import cn.ctgu.sparkproject.dao.impl.SessionAggrStatDAOImpl;
import cn.ctgu.sparkproject.dao.impl.SessionDetailDAOImpl;
import cn.ctgu.sparkproject.dao.impl.SessionRandomExtractDAOImpl;
import cn.ctgu.sparkproject.dao.impl.TaskDAOImpl;
import cn.ctgu.sparkproject.dao.impl.Top10CategoryDAOImpl;
import cn.ctgu.sparkproject.dao.impl.Top10SessionDAOImpl;
/*
* DAO工厂类
*
* */
public class DAOFactory {
/*
* 获取任务管理器DAO
*
* */
public static ITaskDAO getTaskDAO() {
return new TaskDAOImpl();
}
/*
* 获取统计sessionDAO
*
* */
public static ISessionAggrStatDAO getSessionAggrStatDAO() {
return new SessionAggrStatDAOImpl();
}
public static ISessionRandomExtractDAO getSessionRandomExtractDAO() {
return new SessionRandomExtractDAOImpl();
}
public static ISessionDetailDAO getSessionDetailDAO() {
return new SessionDetailDAOImpl();
}
public static ITop10CategoryDAO getTop10CategoryDAO() {
return new Top10CategoryDAOImpl();
}
public static ITop10SessionDAO getTop10SessionDAO() {
return new Top10SessionDAOImpl();
}
public static IPageSplitConvertRateDAO getPageSplitConvertRateDAO() {
return new PageSplitConvertRateDAOImpl();
}
}
cn.ctgu.sparkproject.domain.PageSplitConvertRate
package cn.ctgu.sparkproject.domain;
/*
* 页面切片转化率
*
* */
public class PageSplitConvertRate {
private long taskid;
private String convertRate;
public long getTaskid() {
return taskid;
}
public void setTaskid(long taskid) {
this.taskid = taskid;
}
public String getConvertRate() {
return convertRate;
}
public void setConvertRate(String convertRate) {
this.convertRate = convertRate;
}
}
cn.ctgu.sparkproject.jdbc.JDBCHelper
package cn.ctgu.sparkproject.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.LinkedList;
import java.util.List;
import cn.ctgu.sparkproject.conf.ConfigurationManager;
import cn.ctgu.sparkproject.constant.Constants;
/*
* JDBC辅助组件
*
*
*
* */
public class JDBCHelper {
/*
* 第一步:在静态代码块中,直接加载数据库的驱动
* 加载驱动,不是直接简单的,使用com.mysql.jdbc.Driver就可以了(属于硬编码)
*
*com.mysql.jdbc.Driver只代表了mysql数据的驱动
*那么,如果有一天,项目的底层的数据要进行迁移,比如迁移到SQLServer、DB2
*那么,就必须很费经的在代码中,找到硬编码了的代码并进行修改成其他数据库的驱动类的类名
*
*所以正规项目是不允许硬编码的存在
*
*通常都是使用以一个常量接口中的某个常量来代表一个值
*然后在这个值改变的时候,只要改变常量接口中的变量
*
*项目要尽量做成可配置的,就是说,我们这个数据库驱动,更进一步,也不只是放在常量接口中就可以了
*最后的方式,是放在外部的配置文件中,跟代码彻底分离
*常量接口中只是包含了这个值对应的key的名字
* */
static {
try {
String driver=ConfigurationManager.getProperty(Constants.JDBC_DRIVER);
Class.forName(driver);
}catch(Exception e) {
e.printStackTrace();
}
}
/*
* 第二步,实现JDBCHelper的单例化
* 为什么要实现单例化呢?因为它的内部要封装一个简单的内部的数据连接池
* 为了保证数据库连接池有且仅有一份,所以就通过单例的方式
* 保证JDBCHelper只有一个实例,实例中只有一根数据连接池
* */
private static JDBCHelper instance=null;
/*
* 获取单例
*
* JDBCHelper在整个程序声明周期中,只会创建一次实例
* 在这一次创建实例的过程中,就会调用JDBCHelper()构造方法
* 此时,就可以在构造方法中,去创建自己唯一的数据库连接池
*
* */
public static JDBCHelper getInstance() {
if(instance==null) {
synchronized(JDBCHelper.class){
if(instance==null) {
instance=new JDBCHelper();
}
}
}
return instance;
}
private LinkedList<Connection>datasource=new LinkedList<Connection>();
/*
* 第三步:实现单例的过程中,实现唯一的数据库连接池
* 私有化构造函数
* */
private JDBCHelper() {
//第一步,获取数据库连接池的大小,就是说,数据库连接池中要放多少个数据库连接
//这个,可以通过在配置文件中配置的方式,来灵活的设定
int datasourceSize=ConfigurationManager.getInteger(
Constants.JDBC_DATASOURCE_SIZE);
//然后创建指定数量的数据库连接,并放入数据库连接池中
for(int i=0;i<datasourceSize;i++) {
boolean local=ConfigurationManager.getBoolean(Constants.SPARK_LOCAL);
String url=null;
String user=null;
String password=null;
if(local) {
url=ConfigurationManager.getProperty(Constants.JDBC_URL);
user=ConfigurationManager.getProperty(Constants.JDBC_USER);
password=ConfigurationManager.getProperty(Constants.JDBC_PASSWORD);
}else {
url=ConfigurationManager.getProperty(Constants.JDBC_URL_PROD);
user=ConfigurationManager.getProperty(Constants.JDBC_USER_PROD);
password=ConfigurationManager.getProperty(Constants.JDBC_PASSWORD_PROD);
}
try {
Connection conn=DriverManager.getConnection(url, user, password);
datasource.push(conn);
}catch(Exception e) {
e.printStackTrace();
}
}
}
/*
* 第四步:提供获取数据库连接的方法
* 有可能,你去获取的时候,这个时候连接都被用光了,你暂时获取不到数据库连接
* 所以我们要自己编码实现一个简单的等待机制,去等待获取到数据库连接
*
* 为了防止数据库连接池用完了,其他线程都来判断都来拿数据库连接导致的代码重复判断
* 所以加了一个线程同步只有第一个线程拿到了数据库连接,其他线程才会来判断
* */
public synchronized Connection getConnection() {
while(datasource.size()==0) {
try {
Thread.sleep(10);
}catch(Exception e) {
e.printStackTrace();
}
}
return datasource.poll();
}
/*
* 第五步:开发增删改查的方法
* 1、执行增删改查SQL语句的方法
* 2、执行查询SQL语句的方法
* 3、批量执行SQL语句的方法
*
* */
public int executeUpdate(String sql,Object[]params) {
int rtn=0;
Connection conn=null;
PreparedStatement pstmt=null;
try {
conn=getConnection();
pstmt=conn.prepareStatement(sql);
for(int i=0;i<params.length;i++) {
pstmt.setObject(i+1, params[i]);
}
rtn=pstmt.executeUpdate();
}catch(Exception e) {
e.printStackTrace();
}finally{
if(conn!=null) {
datasource.push(conn);
}
}
return rtn;
}
/*
* 执行查询sql语句
*
* */
public void executeQuery(String sql,Object[]params,
QueryCallback callback) {
Connection conn=null;
PreparedStatement pstmt=null;
ResultSet rs=null;
try {
conn=getConnection();
pstmt=conn.prepareStatement(sql);
for(int i=0;i<params.length;i++) {
pstmt.setObject(i+1, params[i]);
}
rs=pstmt.executeQuery();
callback.process(rs);
}catch(Exception e) {
e.printStackTrace();
}
}
/*
* 批量执行sql语句
* 批量执行SQL语句是JDBC中的一个高级功能
* 默认情况下,每次执行一条SQL语句就会通过网络连接,向MySQL发送一次请求
*
* 但是,如果在短时间内要执行多条结构完全一模一样的SQL,只是参数不同
* 虽然使用PrepareStatement这种方式,可以只编译一次SQL,提高性能,但是,还是对于每次SQL
* 都要向MySQL发送一次网络请求
*
* 可以通过批量执行SQL语句的功能优化这个性能
* 一次性通过PreparedStatement发送多条SQL语句,比如100条、1000条甚至是上万条
* 执行的时候,也仅仅编译一次就可以
* 这种批量执行sql语句的方式,可以大大提升性能
*
* */
public int[]executeBatch(String sql,List<Object[]>paramsList){
int[]rtn=null;
Connection conn=null;
PreparedStatement pstmt=null;
try {
conn=getConnection();
//第一步:使用Connection对象取消自动提交
conn.setAutoCommit(false);
pstmt=conn.prepareStatement(sql);
//第二步:使用PrepareStatement.addBatch()方法加入批量的SQL参数
for(Object[]params:paramsList) {
for(int i=0;i<params.length;i++) {
pstmt.setObject(i+1, params[i]);
}
pstmt.addBatch();
}
//第三步:使用PreparedStatement.executeBatch()方法执行批量的SQL语句
rtn=pstmt.executeBatch();
//最后一步:使用Connection对象,提交批量的SQL语句
conn.commit();
}catch(Exception e) {
e.printStackTrace();
}
return rtn;
}
/*
* 内部类,查询回调接口
*
* */
public static interface QueryCallback{
void process(ResultSet rs) throws Exception;
/*
* 处理查询结果
* */
}
}
my.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.datasource.size=10
jdbc.url=jdbc:mysql://localhost:3306/bigdata
jdbc.user=root
jdbc.password=123456
jdbc.url.prod=jdbc:mysql://172.25.11.100:3306/spark_project
jdbc.user.prod=hive
jdbc.password.prod=hive
spark.local=false
spark.local.taskid.session=2
spark.local.taskid.page=3
cn.ctgu.sparkproject.util.SparkUtils
package cn.ctgu.sparkproject.util;
import org.apache.spark.SparkConf;
import org.apache.spark.SparkContext;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.sql.DataFrame;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.SQLContext;
import org.apache.spark.sql.hive.HiveContext;
import com.alibaba.fastjson.JSONObject;
import cn.ctgu.sparkproject.conf.ConfigurationManager;
import cn.ctgu.sparkproject.constant.Constants;
import cn.ctgu.sparkproject.test.MockData;
/*
* Spark工具类
*
* */
public class SparkUtils {
/*
* 根据当前是否是本地测试的配置
* 决定如何设置sparkConf的master
*
* */
public static void setMaster(SparkConf conf) {
boolean local=ConfigurationManager.getBoolean(Constants.SPARK_LOCAL);
if(local) {
conf.setMaster("local");
}
}
/*
* 生成模拟数据
* 如果是local模式则生成模拟数据,否则不生成
*
* */
public static void mockData(JavaSparkContext sc,SQLContext sqlContext) {
boolean local=ConfigurationManager.getBoolean("local");
if(local) {
MockData.mock(sc, sqlContext);
}
}
/*
* 获取SQLContext
* 如果spark.local设置为true,那么就创建为SQLContext
* 否则创建为HiveContext
*
*
* */
public static SQLContext getSQLContext(SparkContext sc) {
boolean local=ConfigurationManager.getBoolean("local");
if(local) {
return new SQLContext(sc);
}else {
return new HiveContext(sc);
}
}
/*
* 获取指定日期范围内的用户行为数据RDD
*
*
* */
public static JavaRDD<Row>getActionRDDByDateRange(
SQLContext sqlContext,JSONObject taskParam){
String startDate=ParamUtils.getParam(taskParam, Constants.PARAM_START_DATE);
String endDate=ParamUtils.getParam(taskParam, Constants.PARAM_END_DATE);
String sql=
"select *"
+"from user_visit_action"
+"where date>='"+startDate+"'"
+"and date<='"+endDate+"'";
DataFrame actionDF=sqlContext.sql(sql);
return actionDF.toJavaRDD();
}
}
cn.ctgu.sparkproject.util.ParamUtils
package cn.ctgu.sparkproject.util;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import cn.ctgu.sparkproject.conf.ConfigurationManager;
import cn.ctgu.sparkproject.constant.Constants;
/**
* 参数工具类
* @author Administrator
*
*/
public class ParamUtils {
/**
* 从命令行参数中提取任务id
* @param args 命令行参数
* @return 任务id
*/
public static Long getTaskIdFromArgs(String[] args,String taskType) {
boolean local=ConfigurationManager.getBoolean(Constants.SPARK_LOCAL);
if(local) {
return ConfigurationManager.getLong(taskType);
}else {
try {
if(args != null && args.length > 0) {
return Long.valueOf(args[0]);
}
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
/**
* 从JSON对象中提取参数
* @param jsonObject JSON对象
* @return 参数
*/
public static String getParam(JSONObject jsonObject, String field) {
JSONArray jsonArray = jsonObject.getJSONArray(field);
if(jsonArray != null && jsonArray.size() > 0) {
return jsonArray.getString(0);
}
return null;
}
}
cn.ctgu.sparkproject.util.NumberUtils
package cn.ctgu.sparkproject.util;
import java.math.BigDecimal;
/**
* 数字格工具类
* @author Administrator
*
*/
public class NumberUtils {
/**
* 格式化小数
* @param str 字符串
* @param scale 四舍五入的位数
* @return 格式化小数
*/
public static double formatDouble(double num, int scale) {
BigDecimal bd = new BigDecimal(num);
return bd.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}
}
cn.ctgu.sparkproject.util.DateUtils
package cn.ctgu.sparkproject.util;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
/**
* 日期时间工具类
* @author Administrator
*
*/
public class DateUtils {
public static final SimpleDateFormat TIME_FORMAT =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static final SimpleDateFormat DATE_FORMAT =
new SimpleDateFormat("yyyy-MM-dd");
/**
* 判断一个时间是否在另一个时间之前
* @param time1 第一个时间
* @param time2 第二个时间
* @return 判断结果
*/
public static boolean before(String time1, String time2) {
try {
Date dateTime1 = TIME_FORMAT.parse(time1);
Date dateTime2 = TIME_FORMAT.parse(time2);
if(dateTime1.before(dateTime2)) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 判断一个时间是否在另一个时间之后
* @param time1 第一个时间
* @param time2 第二个时间
* @return 判断结果
*/
public static boolean after(String time1, String time2) {
try {
Date dateTime1 = TIME_FORMAT.parse(time1);
Date dateTime2 = TIME_FORMAT.parse(time2);
if(dateTime1.after(dateTime2)) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 计算时间差值(单位为秒)
* @param time1 时间1
* @param time2 时间2
* @return 差值
*/
public static int minus(String time1, String time2) {
try {
Date datetime1 = TIME_FORMAT.parse(time1);
Date datetime2 = TIME_FORMAT.parse(time2);
long millisecond = datetime1.getTime() - datetime2.getTime();
return Integer.valueOf(String.valueOf(millisecond / 1000));
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
/**
* 获取年月日和小时
* @param datetime 时间(yyyy-MM-dd HH:mm:ss)
* @return 结果
*/
public static String getDateHour(String datetime) {
String date = datetime.split(" ")[0];
String hourMinuteSecond = datetime.split(" ")[1];
String hour = hourMinuteSecond.split(":")[0];
return date + "_" + hour;
}
/**
* 获取当天日期(yyyy-MM-dd)
* @return 当天日期
*/
public static String getTodayDate() {
return DATE_FORMAT.format(new Date());
}
/**
* 获取昨天的日期(yyyy-MM-dd)
* @return 昨天的日期
*/
public static String getYesterdayDate() {
Calendar cal = Calendar.getInstance();
cal.setTime(new Date());
cal.add(Calendar.DAY_OF_YEAR, -1);
Date date = cal.getTime();
return DATE_FORMAT.format(date);
}
/**
* 格式化日期(yyyy-MM-dd)
* @param date Date对象
* @return 格式化后的日期
*/
public static String formatDate(Date date) {
return DATE_FORMAT.format(date);
}
/**
* 格式化时间(yyyy-MM-dd HH:mm:ss)
* @param date Date对象
* @return 格式化后的时间
*/
public static String formatTime(Date date) {
return TIME_FORMAT.format(date);
}
/*
* 解析时间字符串
*
* */
public static Date parseTime(String time) {
try {
return TIME_FORMAT.parse(time);
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
}