DWM层: 跳出明细
1. 需求分析与思路
1.1 什么是跳出
跳出就是用户成功访问了网站的入口页面
(例如首页)后就退出,不再继续访问网站的其它页面
。
跳出率计算公式:跳出率=访问一个页面后离开网站的次数 / 总访问次数
观察关键词的跳出率就可以得知用户对网站内容的认可,或者说你的网站是否对用户有吸引力。而网站的内容是否能够对用户有所帮助留住用户也直接可以在跳出率中看出来,所以跳出率是衡量网站内容质量的重要标准。
关注跳出率,可以看出引流过来的访客是否能很快的被吸引,渠道引流过来的用户之间的质量对比,对于应用优化前后跳出率的对比也能看出优化改进的成果。
1.2 计算跳出率的思路
首先要把识别哪些是跳出行为
,要把这些跳出的访客最后一个访问的页面识别出来。那么要抓住几个特征:
- 该页面是用户近期访问的第一个页面
这个可以通过该页面是否有上一个页面(last_page_id)来判断,如果这个表示为空,就说明这是这个访客这次访问的第一个页面。 - 首次访问之后很长一段时间(自己设定),用户没继续再有其他页面的访问。
这个访问的判断,其实有点麻烦,首先这不是用一条数据就能得出结论的,需要组合判断,要用一条存在的数据和不存在的数据进行组合判断。而且要通过一个不存在的数据求得一条存在的数据。更麻烦的他并不是永远不存在,而是在一定时间范围内不存在。那么如何识别有一定失效的组合行为呢?
最简单的办法就是Flink自带的CEP技术
。这个CEP非常适合通过多条数据组合来识别某个事件。
用户跳出事件,本质上就是一个条件事件加一个超时事件的组合。
1.3 具体实现代码
- 确认是否添加了CEP的依赖包,
- 使用Event-time
- 按照mid分组: 所有行为肯定是基于相同的mid来计算
- 定义模式: 首次进入, 在30s内跟着一个多个访问记录
取出那些超时的数据就是我们想要的
import com.alibaba.fastjson.JSONObject;
import com.atguigu.realtime.app.BaseApp;
import com.atguigu.realtime.util.MyKafkaUtil;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.cep.CEP;
import org.apache.flink.cep.PatternFlatSelectFunction;
import org.apache.flink.cep.PatternFlatTimeoutFunction;
import org.apache.flink.cep.PatternStream;
import org.apache.flink.cep.pattern.Pattern;
import org.apache.flink.cep.pattern.conditions.SimpleCondition;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.util.Collector;
import org.apache.flink.util.OutputTag;
import java.time.Duration;
import java.util.List;
import java.util.Map;
public class DWMUserJumpDetailApp extends BaseApp {
public static void main(String[] args) {
new DWMUserJumpDetailApp().init(1, "DWMUserJumpApp", "dwd_page_log");
}
@Override
protected void run(StreamExecutionEnvironment env,
DataStreamSource<String> sourceStream) {
/*sourceStream =
env.fromElements(
"{\"common\":{\"mid\":\"101\"},\"page\":{\"page_id\":\"home\"},\"ts\":10000} ",
"{\"common\":{\"mid\":\"102\"},\"page\":{\"page_id\":\"home\"},\"ts\":10000}",
"{\"common\":{\"mid\":\"102\"},\"page\":{\"page_id\":\"good_list\",\"last_page_id\":" +
"\"home\"},\"ts\":39999} ",
"{\"common\":{\"mid\":\"102\"},\"page\":{\"page_id\":\"good_list\",\"last_page_id\":" +
"\"detail\"},\"ts\":50000} "
);*/
// 1. 数据封装, 添加水印, 按照mid分组
final KeyedStream<JSONObject, String> jsonObjectKS = sourceStream
.map(JSONObject::parseObject)
.assignTimestampsAndWatermarks(
WatermarkStrategy
.<JSONObject>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner((element, recordTimestamp) -> element.getLong("ts"))
)
.keyBy(obj -> obj.getJSONObject("common").getString("mid"));
// 2. 定义模式
final Pattern<JSONObject, JSONObject> pattern = Pattern
.<JSONObject>begin("go_in")
.where(new SimpleCondition<JSONObject>() {
// 条件1: 进入第一个页面 (没有上一个页面)
@Override
public boolean filter(JSONObject value) throws Exception {
final String lastPageId = value.getJSONObject("page").getString("last_page_id");
return lastPageId == null || lastPageId.length() == 0;
}
})
.next("next")
.where(new SimpleCondition<JSONObject>() {
// 条件2: 一个或多个访问记录
@Override
public boolean filter(JSONObject value) throws Exception {
final String pageId = value.getJSONObject("page").getString("page_id");
return pageId != null && pageId.length() > 0;
}
})
.within(Time.seconds(30)); // 30s后跟上条件2
// 3. 把模式应用到流上
final PatternStream<JSONObject> patternStream = CEP.pattern(jsonObjectKS, pattern);
// 4. 获取匹配到的结果: 提前超时数据
final OutputTag<String> timeoutTag = new OutputTag<String>("timeout") {};
final SingleOutputStreamOperator<Object> resultStream = patternStream.flatSelect(
timeoutTag,
new PatternFlatTimeoutFunction<JSONObject, String>() {
@Override
public void timeout(Map<String, List<JSONObject>> pattern,
long timeoutTimestamp,
Collector<String> out) throws Exception {
// 超时的数据放入侧输出流
final List<JSONObject> objList = pattern.get("go_in");
for (JSONObject obj : objList) {
out.collect(obj.toJSONString());
}
}
},
new PatternFlatSelectFunction<JSONObject, Object>() {
@Override
public void flatSelect(Map<String, List<JSONObject>> pattern,
Collector<Object> out) throws Exception {
// 正常数据不需要, 所以此处代码省略
}
});
// 5. 把侧输出流的结果写入到dwm_user_jump_detail
final DataStream<String> jumpStream = resultStream.getSideOutput(timeoutTag);
jumpStream.addSink(MyKafkaUtil.getKafkaSink("dwm_user_jump_detail"));
}
}