教程目錄0x00 教程內(nèi)容0x01 項(xiàng)目分析1. 項(xiàng)目回顧2. 項(xiàng)目目標(biāo)0x02 編程實(shí)現(xiàn)1. 按cookie進(jìn)行分組2. 按user進(jìn)行分組3. 按時(shí)排序日志4. 切割會(huì)話5. 生成會(huì)話6. 查看當(dāng)前結(jié)果7. 實(shí)現(xiàn)do ** in_label字段8. 實(shí)現(xiàn)cookie_label字段9. 保存統(tǒng)計(jì)結(jié)果10. 解決報(bào)錯(cuò)0x03 結(jié)果展示0xFF 總結(jié)
0x00 教程內(nèi)容項(xiàng)目分析編程實(shí)現(xiàn)結(jié)果展示上一個(gè)教程:網(wǎng)站用戶行為分析項(xiàng)目會(huì)話切割(1) 我們做了很多準(zhǔn)備,包括最后一步是過(guò)濾非法數(shù)據(jù)。現(xiàn)在讓我們回顧一下我們的數(shù)據(jù)變化過(guò)程。
0x01 項(xiàng)目分析1. 項(xiàng)目回顧數(shù)據(jù)流程回顧(原始數(shù)據(jù))=> rawRDD => parsedLogRDD)a. 原始數(shù)據(jù):
#type|server time|cookie|ip|urlpageview|2017-09-04 12:00:00|cookie1|127.0.0.3|https:// ** .baidu.comclick|2017-09-04 12:00:02|cookie1|127.0.0.3|https:// ** .baidu.compageview|2017-09-04 12:00:01|cookie2|127.0.0.4|https:// ** .baidu.comclick|2017-09-04 12:00:04|cookie1|127.0.0.3|https:// ** .baidu.compageview|2017-09-04 12:00:02|cookie2|127.0.0.4|http://news.baidu.comclick|2017-09-04 12:00:03|cookie2|127.0.0.4|http://news.baidu.compageview|2017-09-04 12:00:04|cookie2|127.0.0.4|http://music.baidu.com/?fr=tiebapageview|2017-09-04 12:45:01|cookie1|127.0.0.3|https://tieba.baidu.com/index.htmlclick|2017-09-04 12:45:02|cookie1|127.0.0.3|https://tieba.baidu.com/index.htmlclick|2017-09-04 12:45:03|cookie1|127.0.0.3|https://tieba.baidu.com/index.htmlhhhh|2017-09-04 12:45:03|cookie1|127.0.0.3|https://tieba.baidu.com/index.html3333ss|2017-09-04 12:45:03|cookie1|127.0.0.3|https://tieba.baidu.com/index.htmlb. 加載數(shù)據(jù)后,生成rawRDD,接著嘗試將RDD轉(zhuǎn)換成以下格式:
NoneSome({"log_type": "pageview","log_server_time": "2017-09-04 12:00:00","cookie": "cookie1","ip": "127.0.0.3","url": "https:// ** .baidu.com"})Some({"log_type": "click","log_server_time": "2017-09-04 12:00:02","cookie": "cookie1","ip": "127.0.0.3","url": "https:// ** .baidu.com"})Some({"log_type": "pageview","log_server_time": "2017-09-04 12:00:01","cookie": "cookie2","ip": "127.0.0.4","url": "https:// ** .baidu.com"})Some({"log_type": "click","log_server_time": "2017-09-04 12:00:04","cookie": "cookie1","ip": "127.0.0.3","url": "https:// ** .baidu.com"})Some({"log_type": "pageview","log_server_time": "2017-09-04 12:00:02","cookie": "cookie2","ip": "127.0.0.4","url": "http://news.baidu.com"})Some({"log_type": "click","log_server_time": "2017-09-04 12:00:03","cookie": "cookie2","ip": "127.0.0.4","url": "http://news.baidu.com"})Some({"log_type": "pageview","log_server_time": "2017-09-04 12:00:04","cookie": "cookie2","ip": "127.0.0.4","url": "http://music.baidu.com/?fr=tieba"})Some({"log_type": "pageview","log_server_time": "2017-09-04 12:45:01","cookie": "cookie1","ip": "127.0.0.3","url": "https://tieba.baidu.com/index.html"})Some({"log_type": "click","log_server_time": "2017-09-04 12:45:02","cookie": "cookie1","ip": "127.0.0.3","url": "https://tieba.baidu.com/index.html"})Some({"log_type": "click","log_server_time": "2017-09-04 12:45:03","cookie": "cookie1","ip": "127.0.0.3","url": "https://tieba.baidu.com/index.html"})Some({"log_type": "hhhh","log_server_time": "2017-09-04 12:45:03","cookie": "cookie1","ip": "127.0.0.3","url": "https://tieba.baidu.com/index.html"})Some({"log_type": "3333ss","log_server_time": "2017-09-04 12:45:03","cookie": "cookie1","ip": "127.0.0.3","url": "https://tieba.baidu.com/index.html"})c. 但我們認(rèn)為這不是我們想要的格式,所以我們把它變成了parsedLogRDD:
{"log_type": "pageview","log_server_time": "2017-09-04 12:00:00","cookie": "cookie1","ip": "127.0.0.3","url": "https:// ** .baidu.com"}{"log_type": "click","log_server_time": "2017-09-04 12:00:02","cookie": "cookie1","ip": "127.0.0.3","url": "https:// ** .baidu.com"}{"log_type": "pageview","log_server_time": "2017-09-04 12:00:01","cookie": "cookie2","ip": "127.0.0.4","url": "https:// ** .baidu.com"}{"log_type": "click","log_server_time": "2017-09-04 12:00:04","cookie": "cookie1","ip": "127.0.0.3","url": "https:// ** .baidu.com"}{"log_type": "pageview","log_server_time": "2017-09-04 12:00:02", "cookie": "cookie2", "ip": "127.0.0.4", "url": "http://news.baidu.com"}{"log_type": "click", "log_server_time": "2017-09-04 12:00:03", "cookie": "cookie2", "ip": "127.0.0.4", "url": "http://news.baidu.com"}{"log_type": "pageview", "log_server_time": "2017-09-04 12:00:04", "cookie": "cookie2", "ip": "127.0.0.4", "url": "http://music.baidu.com/?fr=tieba"}{"log_type": "pageview", "log_server_time": "2017-09-04 12:45:01", "cookie": "cookie1", "ip": "127.0.0.3", "url": "https://tieba.baidu.com/index.html"}{"log_type": "click", "log_server_time": "2017-09-04 12:45:02", "cookie": "cookie1", "ip": "127.0.0.3", "url": "https://tieba.baidu.com/index.html"}{"log_type": "click", "log_server_time": "2017-09-04 12:45:03", "cookie": "cookie1", "ip": "127.0.0.3", "url": "https://tieba.baidu.com/index.html"}d. 而此處的parsedLogRDD里面的value部分其實(shí)是與trackerLog對(duì)象的屬性一一相對(duì)應(yīng)的,trackerLog對(duì)象格式類似如下:
TrackerLog(pageview,2017-09-04 12:00:00,cookie1,127.0.0.3,https:// ** .baidu.com)............2. 項(xiàng)目目標(biāo)a. 會(huì)話切割類型
我們是想要進(jìn)行會(huì)話切割,會(huì)話切割必定是cookie級(jí)別或者user級(jí)別的,即我們按cookie、按user進(jìn)程切成切割,一個(gè)cookie或者user可以有多個(gè)會(huì)話。
如果無(wú)法理解,可以先往后面看,再由結(jié)果回過(guò)頭來(lái)看。
0x02 編程實(shí)現(xiàn)1. 按cookie進(jìn)行分組現(xiàn)在,我們這里采取先用cookie分組,然后再按user切割的方式。即看一下有多少個(gè)cookie,類似于有多少個(gè)用戶,然后再?gòu)挠脩糁星谐啥嗌賯€(gè)會(huì)話,會(huì)話默認(rèn)是每30分鐘切一個(gè)。
a. 按照cookie進(jìn)行分組
val cookieGroupRDD: RDD[(String, Iterable[TrackerLog])] = parsedLogRDD.groupBy(trackerLog => trackerLog.getCookie.toString)分組之后,我們的數(shù)據(jù)形式類似于如下格式,即按cookie進(jìn)行了分組:
cookie1 -> Iterator(trackerLog1,trackerLog2.....)cookie2 -> Iterator(trackerLog4,trackerLog5.....)此時(shí)的每個(gè)cookie分成一個(gè)key-value,value為裝有trackerLog對(duì)象的迭代器,cookie均相同。
b. 實(shí)際得到的效果如下:
(cookie2,CompactBuffer({"log_type": "pageview", "log_server_time": "2017-09-04 12:00:01", "cookie": "cookie2", "ip": "127.0.0.4", "url": "https:// ** .baidu.com"}, {"log_type": "pageview", "log_server_time": "2017-09-04 12:00:02", "cookie": "cookie2", "ip": "127.0.0.4", "url": "http://news.baidu.com"}, {"log_type": "click", "log_server_time": "2017-09-04 12:00:03", "cookie": "cookie2", "ip": "127.0.0.4", "url": "http://news.baidu.com"}, {"log_type": "pageview", "log_server_time": "2017-09-04 12:00:04", "cookie": "cookie2", "ip": "127.0.0.4", "url": "http://music.baidu.com/?fr=tieba"}))(cookie1,CompactBuffer({"log_type": "pageview", "log_server_time": "2017-09-04 12:00:00", "cookie": "cookie1", "ip": "127.0.0.3", "url": "https:// ** .baidu.com"}, {"log_type": "click", "log_server_time": "2017-09-04 12:00:02", "cookie": "cookie1", "ip": "127.0.0.3", "url": "https:// ** .baidu.com"}, {"log_type": "click", "log_server_time": "2017-09-04 12:00:04", "cookie": "cookie1", "ip": "127.0.0.3", "url": "https:// ** .baidu.com"}, {"log_type": "pageview", "log_server_time": "2017-09-04 12:45:01", "cookie": "cookie1", "ip": "127.0.0.3", "url": "https://tieba.baidu.com/index.html"}, {"log_type": "click", "log_server_time": "2017-09-04 12:45:02", "cookie": "cookie1", "ip": "127.0.0.3", "url": "https://tieba.baidu.com/index.html"}, {"log_type": "click", "log_server_time": "2017-09-04 12:45:03", "cookie": "cookie1", "ip": "127.0.0.3", "url": "https://tieba.baidu.com/index.html"}))cookie2有4條數(shù)據(jù),cookie1有6條數(shù)據(jù)。
2. 按user進(jìn)行分組按cookie進(jìn)行切割之后,下面還需要進(jìn)行按user進(jìn)行切割,即每30分鐘切割成一份。
a. 對(duì)每個(gè)cookie再按user進(jìn)行會(huì)話切割
//4、按user進(jìn)行分組 val sessionRDD: RDD[(String, TrackerSession)] = cookieGroupRDD.flatMapValues { case iter => //處理每個(gè)user的日志 val processor = new OneUserTrackerLogsProcessor(iter.toArray) processor.buildSessions() } sessionRDD.collect().foreach(println)b. 這里先抽離出一部分代碼,新建OneUserTrackerLogsProcessor,后面再將邏輯結(jié)構(gòu)補(bǔ)上
package com.shaonaiyi.spark.session/** * @Auther: shaonaiyi@163.com * @Date: 2019/12/14 20:38 * @Description: 轉(zhuǎn)化每個(gè)user的trackerLogs為trackerSession */class OneUserTrackerLogsProcessor(trackerLogs: Array[TrackerLog]) { def buildSessions() : Array[TrackerSession] = { //1、會(huì)話切割 //2、生成會(huì)話 Array() }}到目前為止,應(yīng)該清楚sessionRDD的結(jié)構(gòu),key是cookie,value是TrackerSession,此過(guò)程只不過(guò)是將value轉(zhuǎn)化了下。
3. 將日志按時(shí)間進(jìn)行排序a. 用戶的日志session間隔超過(guò)30分鐘,則標(biāo)記為一個(gè)新的session,那就需要對(duì)日志進(jìn)行時(shí)間的比較,所以要先將日志進(jìn)行排序,在OneUserTrackerLogsProcessor添加語(yǔ)句:
private val sortedTrackerLogs = trackerLogs.sortBy(trackerLog => trackerLog.getLogServerTime.toString)4. 切割會(huì)話a. 此時(shí)OneUserTrackerLogsProcessor的完整代碼如下
package com.shaonaiyi.sessionimport com.shaonaiyi.spark.session.{TrackerLog, TrackerSession}import org.apache.commons.lang3.time.FastDateFor ** timport scala.collection.mutable.ArrayBuffer/** * @Auther: shaonaiyi@163.com * @Date: 2019/12/14 20:38 * @Description: 轉(zhuǎn)化每個(gè)user的trackerLogs為trackerSession */class OneUserTrackerLogsProcessor(trackerLogs: Array[TrackerLog]) { private val sortedTrackerLogs = trackerLogs.sortBy(trackerLog => trackerLog.getLogServerTime.toString) private val dateFor ** t = FastDateFor ** t.getInstance("yyyy-MM-dd HH:mm:ss") //1、會(huì)話切割 val oneCuttingSessionLogs = new ArrayBuffer[TrackerLog]() //存放正在切割會(huì)話的所有日志 val initBuilder = ArrayBuffer.newBuilder[ArrayBuffer[TrackerLog]] //存放切割完的會(huì)話的所有日志 def buildSessions() : Array[TrackerSession] = { sortedTrackerLogs.foldLeft((initBuilder, Option.empty[TrackerLog])) { case ((builder, preLog), currLog) => val currLogTime = dateFor ** t.parse(currLog.getLogServerTime.toString).getTime if (preLog.nonEmpty && currLogTime - dateFor ** t.parse(preLog.get.getLogServerTime.toString).getTime >= 30 * 60 * 1000) { //切割成新的會(huì)話 builder += oneCuttingSessionLogs.clone() oneCuttingSessionLogs.clear() } oneCuttingSessionLogs += currLog (builder, Some(currLog)) } //2、生成會(huì)話 Array() }}b. 根據(jù)時(shí)間排好序后,因?yàn)橐M(jìn)行時(shí)間比較,而日志的格式是無(wú)法進(jìn)行比較的,所以需要將時(shí)間轉(zhuǎn)化為時(shí)間戳。此處是使用FastDateFor ** t類,注意引入的類應(yīng)該是下面這句
import org.apache.commons.lang3.time.FastDateFor ** tc. 判斷的兩條日志,此處定義為preLog,currLog,只需要進(jìn)行時(shí)間的比較即可,如果比較的第一條日志,因?yàn)闆](méi)有得比較,所以就略過(guò),不需要進(jìn)行操作。如果后一條日志的時(shí)間減去前一條時(shí)間的相差30分鐘,則將當(dāng)前遍歷到的日志(oneCuttingSessionLogs)往builder里寫一份,也就是往initBuilder里寫一份,initBuilder存放的是切割完的會(huì)話的所有日志。
d. 寫一份到initBuilder后,刪除oneCuttingSessionLogs的內(nèi)容,將currLog寫入到oneCuttingSessionLogs。
e. 最后返回的已經(jīng)切分好的會(huì)話的所有日志,以及當(dāng)前的日志。
f. 此時(shí)可以再新建一個(gè)變量cuttedSessionLogsResult來(lái)獲得想要的結(jié)果
val cuttedSessionLogsResult = sortedTrackerLogs.foldLeft((init.......}._1.result()g. 最后一個(gè)會(huì)話也要放進(jìn)去,如果有的話
if (oneCuttingSessionLogs.nonEmpty) { cuttedSessionLogsResult += oneCuttingSessionLogs }最后,一組日志里面,又重新切成了一個(gè)又一個(gè)的會(huì)話,一個(gè)會(huì)話里面,可以有多條日志。
此時(shí)cuttedSessionLogsResult返回的類型為:ArrayBuffer[ArrayBuffer[TrackerLog]]
5. 生成會(huì)話目前我們的會(huì)話已經(jīng)切割完成了,現(xiàn)在要將切割后的會(huì)話再進(jìn)行完善,以達(dá)到我們想要的TrackerSession,所以我們需要對(duì)數(shù)據(jù)進(jìn)行整合。回顧上一篇教程:網(wǎng)站用戶行為分析項(xiàng)目之會(huì)話切割(一) ,我們的目的是得到下面兩張表:TrackerLog表,字段為:
log_typelog_server_timecookieipurl
TrackerSession表,字段為:
session_idsession_server_timecookiecookie_labeliplanding_urlpageview_countclick_countdo ** indo ** in_label
所以現(xiàn)在需要一個(gè)一個(gè)拼湊出來(lái)。a. 代碼如下:
//2、生成會(huì)話 cuttedSessionLogsResult. ** p { case sessionLogs => val session = new TrackerSession() session.setSessionId(UUID.randomUUID().toString) session.setSessionServerTime(sessionLogs.head.getLogServerTime) session.setCookie(sessionLogs.head.getCookie) session.setIp(sessionLogs.head.getIp) val pageviewLogs = sessionLogs.filter(_.getLogType.toString.equals("pageview")) if(pageviewLogs.length == 0) { session.setLandingUrl("-") } else { session.setLandingUrl(pageviewLogs.head.getUrl) } session.setPageviewCount(pageviewLogs.length) val clickLogs = sessionLogs.filter(_.getLogType.toString.equals("click")) session.setClickCount(clickLogs.length) if (pageviewLogs.length == 0) { session.setDo ** in("-") } else { val url = new URL(pageviewLogs.head.getUrl.toString) session.setDo ** in(url.getHost) } session }b. 刪除原來(lái)的Array(),將buildSessions返回的類型修改為ArrayBuffer
def buildSessions() : ArrayBuffer[TrackerSession] = {6. 當(dāng)前結(jié)果查看至此,還有cookie_label、do ** in_label兩個(gè)字段沒(méi)有加進(jìn)去。
a. 在TrackerSession加上實(shí)現(xiàn)序列化接口Serializable。然后執(zhí)行,得到一下結(jié)果。
(cookie2,{"session_id": "38059172-e0aa-4d37-97da-12778a5a ** 55", "session_server_time": "2017-09-04 12:00:01", "cookie": "cookie2", "cookie_label": null, "ip": "127.0.0.4", "landing_url": "https:// ** .baidu.com", "pageview_count": 3, "click_count": 1, "do ** in": " ** .baidu.com", "do ** in_label": null})(cookie1,{"session_id": "218fdf54-8b34-484d-b53b-0769ea5d1421", "session_server_time": "2017-09-04 12:00:00", "cookie": "cookie1", "cookie_label": null, "ip": "127.0.0.3", "landing_url": "https:// ** .baidu.com", "pageview_count": 1, "click_count": 2, "do ** in": " ** .baidu.com", "do ** in_label": null})(cookie1,{"session_id": "ec2f3a38-3335-45f5-99f8-c27947bca687", "session_server_time": "2017-09-04 12:45:01", "cookie": "cookie1", "cookie_label": null, "ip": "127.0.0.3", "landing_url": "https://tieba.baidu.com/index.html", "pageview_count": 1, "click_count": 2, "do ** in": "tieba.baidu.com", "do ** in_label": null})b. 結(jié)果講解parsedLogRDD一共有十條數(shù)據(jù),現(xiàn)在是得到3個(gè)會(huì)話。觀察每個(gè)會(huì)話的pageview_count、click_count兩個(gè)字段,(3+1)+(1+2)+(1+2)=10條。也就是cookie2有一個(gè)會(huì)話,此會(huì)話里面有3個(gè)pageview事件,1個(gè)click事件。而cookie1因?yàn)樗娜罩纠锩妫瑫r(shí)間間隔有超過(guò)30分鐘的,所以進(jìn)行了切分,切分成了兩個(gè)會(huì)話。
7. 實(shí)現(xiàn)do ** in_label字段a. 觀察前面的會(huì)話結(jié)果,其實(shí)cookie_label、do ** in_label兩個(gè)字段都還是NULL的,現(xiàn)在我們需要統(tǒng)計(jì)一下,先完成do ** in_label,我們的do ** in_label數(shù)據(jù)量比較小,所以我們可以存放在傳統(tǒng)數(shù)據(jù)庫(kù)里面。因?yàn)檫@里只是演示,所以我就直接在代碼中寫死了。在SessionCutETL中添加代碼
//網(wǎng)站域名標(biāo)簽數(shù)據(jù),此處只是演示,其實(shí)可以存放在數(shù)據(jù)庫(kù)里 val do ** inLabelMap = Map( " ** .baidu.com" -> "level1", " ** .taobao.com" -> "level2", "jd.com" -> "level3", "youku.com" -> "level4" )b. 因?yàn)閿?shù)據(jù)量比較小,所以,還可以將此數(shù)據(jù)廣播出去
//廣播 val do ** inLabelMapB = sc.broadcast(do ** inLabelMap)c. 將do ** inLabelMapB傳進(jìn)buildSessions函數(shù),以參數(shù)的形式傳,修改兩行代碼為:
processor.buildSessions(do ** inLabelMapB.value)def buildSessions(do ** inLabelMap:Map[String, String]) : ArrayBuffer[TrackerSession] = {d. 設(shè)置do ** inLabel,根據(jù)do ** in獲得相對(duì)應(yīng)的do ** inLabel,沒(méi)有就用“-”
val do ** inLabel = do ** inLabelMap.getOrElse(session.getDo ** in.toString, "-") session.setDo ** inLabel(do ** inLabel)e. 執(zhí)行,查看結(jié)果,發(fā)現(xiàn)標(biāo)簽已經(jīng)有了
8. 實(shí)現(xiàn)cookie_label字段a. 獲取cookie_label數(shù)據(jù)
//5、給會(huì)話的cookie打標(biāo)簽 val cookieLabelRDD: RDD[(String, String)] = sc.textFile("data/cookie_label.txt"). ** p { case line => val temp = line.split("|") (temp(0), temp(1)) // (cookie, cookie_label) }b. sessionRDD、cookieLabelRDD的key都是cookie,所以可以進(jìn)行關(guān)聯(lián),sessionRDD的數(shù)據(jù)肯定是要的,只不過(guò)是加入cookieLabelRDD的數(shù)據(jù)而已
val joinRDD: RDD[(String,(TrackerSession, Option[String]))] = sessionRDD.leftOuterJoin(cookieLabelRDD) val cookieLabeledSessionRDD: RDD[TrackerSession] = joinRDD. ** p { case (cookie, (session, cookieLabelOpt)) => if (cookieLabelOpt.nonEmpty) { session.setCookieLabel(cookieLabelOpt.get) } else { session.setCookieLabel("-") } session } cookieLabeledSessionRDD.collect().foreach(println)因?yàn)樽箨P(guān)聯(lián)后,cookieLabelRDD所對(duì)應(yīng)的value可能是空的,所以對(duì)應(yīng)的應(yīng)該是Option[String]。
c. 執(zhí)行,查看結(jié)果,發(fā)現(xiàn)cookie標(biāo)簽已經(jīng)有了
9. 保存統(tǒng)計(jì)結(jié)果a. 因?yàn)槭且詐arquet方式保存,所以需要引入一個(gè)jar包,勿忘!
<dependency> <groupId>org.apache.parquet</groupId> <artifactId>parquet-avro</artifactId> <version>1.8.1</version> </dependency>b. 保存parsedLogRDD
//6、保存數(shù)據(jù) //6.1、保存TrackerLog,對(duì)應(yīng)的是parsedLogRDD val trackerLogOutputPath = "data/output/trackerLog" AvroWriteSupport.setSche ** (sc.hadoopConfiguration, TrackerLog.SCHEMA$) parsedLogRDD. ** p((null, _)).saveAsNewAPIHadoopFile(trackerLogOutputPath, classOf[Void], classOf[TrackerLog], classOf[AvroParquetOutputFor ** t[TrackerLog]] )c. 保存cookieLabeledSessionRDD
//6.2、保存TrackerSession,對(duì)應(yīng)的是cookieLabeledSessionRDD val trackerSessionOutputPath = "data/output/trackerSession" AvroWriteSupport.setSche ** (sc.hadoopConfiguration, TrackerSession.SCHEMA$) cookieLabeledSessionRDD. ** p((null, _)).saveAsNewAPIHadoopFile(trackerSessionOutputPath, classOf[Void], classOf[TrackerSession], classOf[AvroParquetOutputFor ** t[TrackerSession]] )d. 然后執(zhí)行,發(fā)現(xiàn)報(bào)錯(cuò),第一個(gè)錯(cuò)是一直都有的,第二個(gè)錯(cuò)是新的。
10. 解決報(bào)錯(cuò)a. 請(qǐng)查看本博主另一篇文章:Windows本地安裝Hadoop
0x03 結(jié)果展示a. 刪除報(bào)錯(cuò)時(shí)所生成的文件夾,不然會(huì)報(bào)錯(cuò)
b. 刪除data/output/trackerLog文件夾,然后重新執(zhí)行,即可得到想要的答案
0xFF 總結(jié)數(shù)據(jù)轉(zhuǎn)化的過(guò)程比較繁瑣,想要自己多動(dòng)手嘗試,了解其來(lái)龍去脈,反復(fù)看多幾遍。請(qǐng)點(diǎn)贊關(guān)注,獲取網(wǎng)站用戶行為分析項(xiàng)目系列全教程!作者簡(jiǎn)介:邵奈一 全棧工程師、市場(chǎng)洞察者、專欄編輯 | 公眾號(hào) | 微信 | 微博 | CSDN | 簡(jiǎn)書 |
福利:邵奈一的技術(shù)博客導(dǎo)航邵奈一 原創(chuàng)不易,如轉(zhuǎn)載請(qǐng)標(biāo)明出處。
Copyright 2021 快鯨
掃碼咨詢與免費(fèi)使用
申請(qǐng)免費(fèi)使用