寫在前面: 博主是一名軟件工程系大數據應用開發專業大二的學生,暱稱來源於《愛麗絲夢遊仙境》中的Alice和自己的暱稱。作爲一名互聯網小白,
寫博客一方面是爲了記錄自己的學習歷程,一方面是希望能夠幫助到很多和自己一樣處於起步階段的萌新
。由於水平有限,博客中難免會有一些錯誤,有紕漏之處懇請各位大佬不吝賜教!個人小站:http://alices.ibilibili.xyz/ , 博客主頁:https://alice.blog.csdn.net/
儘管當前水平可能不及各位大佬,但我還是希望自己能夠做得更好,因爲一天的生活就是一生的縮影
。我希望在最美的年華,做最好的自己
!
本篇博客,博主爲大家帶來的是大數據實戰【千億級數倉】的階段六,也就是最後一個階段。
文章目錄
通過在階段一就已經透露出的目標,我們再來回顧一下該階段我們需要實現哪些內容。
- 用戶瀏覽記錄整理分析(點擊流)
確定了我們所需要實現的是一個關於用戶點擊流數據處理的一個功能,那就讓我們愉快地往下看吧~
用戶行爲日誌
1 .日誌數據格式
日誌數據內容樣例
f5dd685d-6b83-4e7d-8c37-df8797812075 222.68.172.190 - - 2018-11-01 14:34:57 "GET /images/my.jpg HTTP/1.1" 200 19939 "http://www.angularjs.cn/A00n" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.66 Safari/537.36"
字段解析:
- 用戶id信息-uid: f5dd685d-6b83-4e7d-8c37-df8797812075
- 訪客ip地址: 222.68.172.190
- 訪客用戶信息: - -
- 請求時間:2018-11-01 14:34:57
- 請求方式:GET
- 請求的url:/images/my.jpg
- 請求所用協議:HTTP/1.1
- 響應碼:200
- 返回的數據流量:19939
- 訪客的來源url:http://www.angularjs.cn/A00n
- 訪客所用瀏覽器:Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.66 Safari/537.36
注意: 所有的數據字段之間的分隔符爲空格
2 .數據倉庫-ETL處理
點擊流概念
點擊流(Click Stream)是指用戶在網站上持續訪問的軌跡。注重用戶瀏覽網站的整個流程。用戶對網站的每次訪問包含了一系列的點擊動作行爲,這些點擊行爲數據就構成了點擊流數據(Click Stream Data),它代表了用戶瀏覽網站的整個流程。
點擊流和網站日誌是兩個不同的概念,點擊流是從用戶的角度出發,注重用戶瀏覽網站的整個流程;而網站日誌是面向整個站點,它包含了用戶行爲數據、服務器響應數據等衆多日誌信息,我們通過對網站日誌的分析可以獲得用戶的點擊流數據。
點擊流模型完全是業務模型,相關概念由業務指定而來。由於大量的指標統計從點擊流模型中更容易得出,所以在預處理階段,可以使用spark程序來生成點擊流模型的數據。
在點擊流模型中,存在着兩種模型數據:PageViews、Visits。
點擊流模型pageviews
Pageviews模型數據專注於用戶每次會話(session)的識別,以及每次session內訪問了幾步和每一步的停留時間。
在日誌數據分析中,通常把前後兩條訪問記錄時間差在30分鐘以內算成一次會話。如果超過30分鐘,則把下次訪問算成新的會話開始。
大致步驟如下:
- 在所有訪問日誌中找出該用戶的所有訪問記錄
- 把該用戶所有訪問記錄按照時間正序排序
- 計算前後兩條記錄時間差是否爲30分鐘
- 如果小於30分鐘,則是同一會話session的延續
- 如果大於30分鐘,則是下一會話session的開始
- 用前後兩條記錄時間差算出上一步停留時間
- 最後一步和只有一步的 業務默認指定頁面停留時間60s
3. 數據入庫
1. 創建ODS層數據表
1.1 原始日誌數據表
drop table if exists itcast_ods.ods_weblog_origin;
create table itcast_ods.ods_weblog_origin(
valid Boolean,
remote_addr string,
remote_user string,
time_local string,
request string,
status string,
body_bytes_sent string,
http_referer string,
http_user_agent string,
guid string)
partitioned by (dt string)
STORED AS PARQUET;
對應的字段
valid Boolean, --判斷數據是否合法
remote_addr string, --記錄客戶端的ip地址
remote_user string, --記錄客戶端用戶名稱,忽略屬性"-"
time_local string, --記錄訪問時間與時區
request string, --記錄請求的url與http協議
status string, --記錄請求狀態;成功是200
body_bytes_sent string, --記錄發送給客戶端文件主體內容大小
http_referer string, --用來記錄從那個頁面鏈接訪問過來的
http_user_agent string, --記錄客戶瀏覽器的相關
guid string) --用戶id信息
注意事項:
parquet中字段數據類型要與hive表字段類型保持一致!!
1.2 點擊流模型
drop table if exists itcast_ods.ods_click_pageviews;
create table itcast_ods.ods_click_pageviews(
session string,
remote_addr string,
time_local string,
request string,
visit_step int,
page_staylong string,
http_referer string,
http_user_agent string,
body_bytes_sent string,
status string)
partitioned by (dt string)
STORED AS PARQUET;
對應的字段
session string, //session
remote_addr string, //ip地址
time_local string, //訪問時間
request string, //請求路徑
visit_step int, //訪問第幾個頁面
page_staylong string, //停留時長
http_referer string, //用來記錄從那個頁面鏈接訪問過來的
http_user_agent string, //記錄客戶瀏覽器的相關
body_bytes_sent string, //記錄發送給客戶端文件主體內容大小
status string //狀態//用戶ID
1.3 點擊流visit模型表
drop table if exist itcast_ods.ods_click_stream_visit;
create table itcast_ods.ods_click_stream_visit(
session string,
remote_addr string,
inTime string,
outTime string,
inPage string,
outPage string,
referal string,
pageVisits int)
partitioned by (dt string)
STORED AS PARQUET;
對應的字段
session string, //session
remote_addr string, //IP地址
inTime string, //進入時間
outTime string, //離開時間
inPage string, //進入的頁面
outPage string, //離開的頁面
referal string, //用來記錄從那個頁面鏈接訪問過來的
pageVisits int) //訪問頁面數量
創建好了表之後,接下來我們需要對數據進行處理了。爲了方便大家理解,如何將原始的數據通過不同的預處理,將數據分別寫入到ods的3個不同功能的表中,送上一張圖來幫助大家理清楚這個流程。
接下來奉上完整的代碼,
object ClicklogApp {
// 程序入口
def main(args: Array[String]): Unit = {
val pages: mutable.HashSet[String] = new mutable.HashSet[String]()
//初始化靜態資源路徑集合
def initlizePages(): Unit = {
pages.add("/about")
pages.add("/black-ip-list/")
pages.add("/cassandra-clustor/")
pages.add("/finance-rhive-repurchase/")
pages.add("/hadoop-family-roadmap/")
pages.add("/hadoop-hive-intro/")
pages.add("/hadoop-zookeeper-intro/")
pages.add("/hadoop-mahout-roadmap/")
}
/*
1、對數據進行預處理,過濾掉無效的數據,將有效的數據寫入到hive表
*/
// 1. 初始化 spark session
val spark: SparkSession = SparkSession.builder().appName("Clicklog").master("local[*]").getOrCreate()
// 獲取到SparkContext
val sc: SparkContext = spark.sparkContext
// 設置日誌級別
sc.setLogLevel("WARN")
// 2. 讀取數據
val logDatas: RDD[String] = sc.textFile("E://2020大數據新學年//BigData//項目//0519//access.log.20181101.dat_new.bak")
// 3. 將每一條數據轉換成WebLogBean
val webLogBeanRDD: RDD[WebLogBean] = logDatas.map(WebLogBean(_))
// 4. 對數據進行過濾(刪除無效數據)
val activeWebLogBeans: RDD[WebLogBean] = webLogBeanRDD.filter(webLogBean => {
// 若bean不爲空,並且數據有效,那麼保留該數據。反之刪除數據
if (webLogBean != null && webLogBean.valid) {
true
} else {
false
}
})
// 初始化靜態數據
initlizePages()
// 5. 過濾靜態數據
val noStaticActiveWebLogBeans: RDD[WebLogBean] = activeWebLogBeans.filter(activeWebLogBean => {
// 靜態數據中包含用戶訪問的路徑,表示這個數據是無效的
if (pages.contains(activeWebLogBean.request)) {
false
} else {
// 表示數據有效
true
}
})
// 導入隱式轉換
import spark.implicits._
// 6. WebLogBean 轉換成 WebLogBeanCase --> 因爲WebLogBeanCase 的結構與最終存儲數據的表結構相同
val weblogBeanCaseRDD: RDD[WeblogBeanCase] = noStaticActiveWebLogBeans.map(webLogBean => {
WeblogBeanCase(webLogBean.valid,
webLogBean.remote_addr,
webLogBean.remote_user,
webLogBean.time_local,
webLogBean.request,
webLogBean.status,
webLogBean.body_bytes_sent,
webLogBean.http_referer,
webLogBean.http_user_agent,
webLogBean.guid)
})
// // 7. 將數據寫入到hive表
// val weblogBeanCaseDF: DataFrame = weblogBeanCaseRDD.toDF()
//
//
// // 將數據寫入到HDFS
// weblogBeanCaseDF.write.mode("overwrite").parquet("hdfs://node01:8020/user/hive/warehouse/itcast_ods.db/ods_weblog_origin/dt=20191212")
/* 下面是點擊流模型的計算 */
//1. 根據用戶 ID 對數據進行分組
val userWebLogList: RDD[(String, Iterable[WeblogBeanCase])] = weblogBeanCaseRDD.groupBy(weblogBeanCase=>weblogBeanCase.guid)
//2. 得到的數據是一個用戶的數據都在一起,按照瀏覽的時間排序 -》 某一個用戶瀏覽的順序數據
val pageViewsBeanCaseRDD: RDD[PageViewsBeanCase] = userWebLogList.flatMap(oneUserWebLog => {
// 一個用戶的數據
// 獲取用戶的 useId
val userId: String = oneUserWebLog._1
// 獲取到用戶的瀏覽記錄(排好序的記錄)
val webLogList: List[WeblogBeanCase] = oneUserWebLog._2.toList.sortBy(_.time_local)
// 初始化Session
var session: String = UUID.randomUUID().toString
// 初始化用戶訪問的第幾步
var setp: Int = 1
//初始化存儲PageViewsBeanCase的list
var pageViewsBeanCaseList: ListBuffer[PageViewsBeanCase] = ListBuffer[PageViewsBeanCase]()
//導入隱式轉化,下面需要使用到continue和break
import scala.util.control.Breaks._
//3. 遍歷每個用戶的數據
for (num <- 0 until (webLogList.size - 1)) {
// 獲取當前的瀏覽記錄
var cruurentWebLog: WeblogBeanCase = webLogList(num)
// 3.1 數據量可能爲一條,瀏覽時間默認爲60s
if (webLogList.size == 1) {
// 封裝PageViewsBeanCase
val pageViewsBeanCase: PageViewsBeanCase = PageViewsBeanCase(session,
cruurentWebLog.remote_addr,
cruurentWebLog.time_local,
cruurentWebLog.request,
setp,
60 + "",
cruurentWebLog.http_referer,
cruurentWebLog.http_user_agent,
cruurentWebLog.body_bytes_sent,
cruurentWebLog.status,
cruurentWebLog.guid )
pageViewsBeanCase
// 將剛計算的 pageViewBeanCase 保存到PageViewsBeanCaseList【最終寫入到HDFS】
pageViewsBeanCaseList += pageViewsBeanCase
//重新生成新的uuid
session = UUID.randomUUID().toString
} else {
// 若沒有進入上面的 if 表示有多條數據
// 3.2 數據量有可能是多條 1 2 3 4 5
// 先獲取第一條數據時間
// 若是第一條數據 我們跳過第一天 進入第二個循環 得到第二天數據
breakable {
if (num == 0) {
// num == 0 表示 這是第一天的數據
// 跳過第一天
break()
}
// 先獲取到上一次記錄的時間(因爲第一天已經跳過,cruurentWebLog 爲第二天條的數據)
val upDataTime: String = webLogList(num - 1).time_local
// 獲取到這一次記錄的時間
val nextDataTime: String = cruurentWebLog.time_local
// 求兩個界面的時間差
// 第二條數據的時間 - 第一條數據的時間 = 第一個頁面的停留時長
val diffTime: Long = DateUtil.getTimeDiff(upDataTime, nextDataTime)
//獲取上一個數據的 WeblogBeanCase
val upWeblogBean: WeblogBeanCase = webLogList(num - 1)
if (diffTime < 30 * 60 * 1000) {
// 3.2.1 兩個數據之間的間隔在30分鐘之內
// 封裝PageViewsBeanCase ,這個PageViewsBeanCase 是第一天數據的PageViewsBeanCase
val beanCase: PageViewsBeanCase = PageViewsBeanCase(session, upWeblogBean.remote_addr, upWeblogBean.time_local, upWeblogBean.request,
setp, diffTime + "", upWeblogBean.http_referer, upWeblogBean.http_user_agent, upWeblogBean.body_bytes_sent,
upWeblogBean.status, upWeblogBean.guid)
// 添加到結果集
pageViewsBeanCaseList += beanCase
// session不需要更新
setp += 1
} else {
// 3.2.2 兩個數據之間的間隔超過30分鐘,換另外一個session會話
//封裝PageViewsBeanCase
val beanCase: PageViewsBeanCase = PageViewsBeanCase(session, upWeblogBean.remote_addr, upWeblogBean.time_local, upWeblogBean.request,
setp, 60 + "", upWeblogBean.http_referer, upWeblogBean.http_user_agent, upWeblogBean.body_bytes_sent,
upWeblogBean.status, upWeblogBean.guid)
//添加到結果集
pageViewsBeanCaseList += beanCase
//爲下一個會話準備數據
//session需要更新
//重新生成session
session = UUID.randomUUID().toString
//sept 歸1
setp = 1
}
//最後一條數據
if (webLogList.size - 1 == num) {
//3.3最後一條數據 瀏覽時間默認60s
//封裝PageViewsBeanCase
val lastViewsBeanCase: PageViewsBeanCase = PageViewsBeanCase(session, cruurentWebLog.remote_addr, cruurentWebLog.time_local, cruurentWebLog.request,
setp, 60 + "", cruurentWebLog.http_referer, cruurentWebLog.http_user_agent, cruurentWebLog.body_bytes_sent,
cruurentWebLog.status, cruurentWebLog.guid
)
//添加到結果集
pageViewsBeanCaseList += lastViewsBeanCase
}
}
}
}
pageViewsBeanCaseList
}
)
//每個用戶的最終
//寫入Hive itcast_ods.ods_click_pageviews
//pageViewsBeanCaseRDD.toDF().write.mode("overwrite").parquet("hdfs://node01:8020/user/hive/warehouse/itcast_ods.db/ods_click_pageviews/dt=20191212/")
/* 下面是點擊流visit模型表的計算 */
// 1. 根據session 對數據進行分組
val sessionGroupDatas: RDD[(String, Iterable[PageViewsBeanCase])] = pageViewsBeanCaseRDD.groupBy(bean=>bean.session)
// 2. 獲取到一個session內有哪些頁面
val visitBeanCaseRDD: RDD[VisitBeanCase] = sessionGroupDatas.map(pageViewsBeanCase => {
// 獲取到session
val session: String = pageViewsBeanCase._1
// 3. 獲取到PageViewsBeanCase的集合,對一個會話內的數據進行排序 [按照步驟排序]
val pageViewsBeanCases: List[PageViewsBeanCase] = pageViewsBeanCase._2.toList.sortBy(_.visit_step)
// 4. 獲取第一個訪問頁面的數據
val firstPageViewsBeanCase: PageViewsBeanCase = pageViewsBeanCases.head
// 5. 獲取最後一個訪問頁面數據
val lastPageViewsBeanCase: PageViewsBeanCase = pageViewsBeanCases.last
// 6. 封裝VisitBeanCase
VisitBeanCase(session, firstPageViewsBeanCase.remote_addr, firstPageViewsBeanCase.time_local, lastPageViewsBeanCase.time_local,
firstPageViewsBeanCase.request, lastPageViewsBeanCase.request, firstPageViewsBeanCase.htp_referer, pageViewsBeanCases.size)
})
// 將數據寫入到Hive
visitBeanCaseRDD.toDF().write.mode("overwrite")
.parquet("hdfs://node01:8020/user/hive/warehouse/itcast_ods.db/ods_click_stream_visit/dt=20191212/")
}
}
在上述所展示的代碼中,從數據的讀取再到預處理,封裝,判斷,輸出。每一步都經過了大量的思考,嚴格按照前面分享的思路。更多的細節大家可以參考詳細的註釋。
另外,僅憑上面的代碼還不能體現出這個任務的難點。我們還需提前準備好兩個包,來方便我們在書寫正式代碼時調用,簡化開發。
該階段的代碼近期準備上傳到GitHub,感興趣的朋友也可以去博主小站👉Alice的技術棧後臺留言哦~
至此,大數據離線數倉項目就暫告一個段落…後期博主會持續分享關於大數據的項目,敬請期待😎
如果以上過程中出現了任何的紕漏錯誤,煩請大佬們指正😅
受益的朋友或對大數據技術感興趣的夥伴記得點贊關注支持一波🙏