各網站站長和營運人員經常使用網站數據分析工具,谷歌分析、百度統計、騰訊分析等被廣泛使用,要想統計數據先要收集數據,下面和大家分析數據收集的原理,並建構一個資料收集系統。
資料收集原則分析
簡單來說,網站統計分析工具需要收集到使用者瀏覽目標網站的行為(如開啟某網頁、點選某按鈕、將商品加入購物車等)及行為附加資料(如某下單行為所產生的訂單金額等)。早期的網站統計往往只收集一種使用者行為:頁面的開啟。而後用戶在頁面中的行為均無法收集。這種收集策略能滿足基本的流量分析、來源分析、內容分析及訪客屬性等常用分析視角,但是,隨著ajax技術的廣泛使用及電子商務網站對於電子商務目標的統計分析的需求越來越強烈,這種傳統的收集策略已經顯得力不能及。
後來,Google在其產品谷歌分析中創新性的引入了可定制的數據收集腳本,用戶通過谷歌分析定義好的可擴展接口,只需編寫少量的javascript代碼就可以實現自定義事件和自定義指標的追蹤和分析。目前百度統計、搜狗分析等產品都複製了Google分析的模式。
其實說起來兩種資料收集模式的基本原理和流程是一致的,只是後者透過javascript收集到了更多的資訊。下面來看看現在各種網站統計工具的資料收集基本原理。
流程概覽
首先,使用者的行為會觸發瀏覽器對被統計頁面的一個http請求,這裡姑且先認為行為就是開啟網頁。當網頁被打開,頁面中的埋點javascript片段會被執行,用過相關工具的朋友應該知道,一般網站統計工具都會要求使用者在網頁中加入一小段javascript程式碼,這個程式碼片段一般會動態建立一個script標籤,並將src指向一個單獨的js文件,此時這個單獨的js文件(圖1中綠色節點)會被瀏覽器請求到並執行,這個js往往就是真正的資料收集腳本。資料收集完成後,js會請求一個後端的資料收集腳本(圖1中的backend),這個腳本一般是一個偽裝成圖片的動態腳本程序,可能由php、python或其它服務端語言編寫,js會將收集到的資料透過http參數的方式傳遞給後端腳本,後端腳本解析參數並以固定格式記錄到存取日誌,同時可能會在http回應中給客戶端種植一些用於追蹤的cookie。
上面是一個資料收集的大概流程,以下以Google分析為例,每個階段進行一個相對詳細的分析。
埋點腳本執行階段
若要使用Google分析(以下簡稱GA),需要在頁面中插入一段它提供的javascript片段,這個片段往往被稱為埋點代碼。
其中_gaq是GA的的全域數組,用於放置各種配置,其中每一條配置的格式為:
_gaq.push([‘Action’, ‘param1&rsquo ;, ‘param2’, …]);
Action指定配置動作,後面是相關的參數列表。 GA給的預設埋點代碼會給予兩個預置配置,_setAccount用來設定網站識別ID,這個識別ID是在註冊GA時分配的。 _trackPageview告訴GA追蹤一次頁面訪問。更多配置請參考:https://developers.google.com/analytics/devguides/collection/gajs/。實際上,這個_gaq是被當作一個FIFO佇列來用的,設定程式碼不必出現在埋點程式碼之前,請具體請參考上述連結的說明。
就本文來說,_gaq的機制不是重點,重點是後面匿名函數的程式碼,這才是埋點程式碼真正要做的事。這段程式碼的主要目的就是引入一個外部的js檔案(ga.js),方式是透過document.createElement方法建立一個script並根據協議(http或https)將src指向對應的ga.js,最後將這個element插入頁面的dom樹上。
注意ga.async = true的意思是非同步呼叫外部js文件,也就是不阻塞瀏覽器的解析,待外部js下載完成後非同步執行。這個屬性是HTML5新引進的。
資料收集腳本執行階段
資料收集腳本(ga.js)被請求後會被執行,這個腳本一般要做如下幾件事:
1、透過瀏覽器內建javascript物件收集信息,如頁面title(透過document.title )、referrer(上一跳url,透過document.referrer)、使用者顯示器解析度(透過windows.screen)、cookie資訊(透過document.cookie)等等一些資訊。
2、解析_gaq收集配置資訊。這裡面可能會包括用戶自訂的事件追蹤、業務資料(如電子商務網站的商品編號等)等。
3、將上面兩步驟收集的資料以預先定義格式解析並拼接。
4、請求一個後端腳本,將訊息放在http request參數中攜帶給後端腳本。
這裡唯一的問題是步驟4,javascript請求後端腳本常用的方法是ajax,但是ajax是不能跨域請求的。這裡ga.js在被統計網站的網域內執行,而後端腳本在另外的網域(GA的後端統計腳本是http://www.google-analytics.com/__utm.gif),ajax行不通。一種通用的方法是js腳本建立一個Image對象,將Image對象的src屬性指向後端腳本並攜帶參數,此時即實作了跨域請求後端。這也是後端腳本為什麼通常偽裝成gif檔案的原因。透過http抓包可以看到ga.js對__utm.gif的請求。
可以看到ga.js在請求__utm.gif時帶了很多信息,例如utmsr=1280×1024是屏幕分辨率,utmac=UA-35712773-1是_gaq中解析出的我的GA標識ID等等。
值得注意的是,__utm.gif未必只會在埋點代碼執行時被請求,如果用_trackEvent配置了事件跟踪,則在事件發生時也會請求這個腳本。
由於ga.js經過了壓縮和混淆,可讀性很差,我們就不分析了,具體後面實現階段我會實現一個功能類似的腳本。
後端腳本執行階段
GA的__utm.gif是一個偽裝成gif的腳本。這種後端腳本一般要完成以下幾件事:
1、解析http請求參數的到資訊。
2、從伺服器(WebServer)中取得一些客戶端無法取得的訊息,如訪客ip等。
3、將訊息依格式寫入log。
4、產生一副1×1的空gif圖片作為回應內容並將回應頭的Content-type設為image/gif。
5、在回應頭中透過Set-cookie設定一些需要的cookie資訊。
之所以要設定cookie是因為如果要追蹤唯一訪客,通常做法是如果在請求時發現客戶端沒有指定的追蹤cookie,則根據規則產生一個全域唯一的cookie並種植給用戶,否則Set-cookie中放置獲取到的追蹤cookie以保持相同使用者cookie不變(見圖4)。
這種做法雖然不是完美的(例如使用者清除cookie或更換瀏覽器會被認為是兩個使用者),但是是目前被廣泛使用的手段。注意,如果沒有跨站追蹤同一用戶的需求,可以透過js將cookie種植在被統計站點的域下(GA是這麼做的),如果要全網統一定位,則透過後端腳本種植在服務端域下(我們待會的實現會這麼做)。
系統的設計實作
根據上述原理,我自己建立了一個訪問日誌收集系統。
我將這個系統叫做MyAnalytics。
確定收集的資訊
為了簡單起見,我不打算實作GA的完整資料收集模型,而是收集資訊。
埋點程式碼
埋點程式碼我會借鏡GA的模式,但目前不會將組態物件當作FIFO佇列用。
我現在正在使用名為ma.js的統計腳本,並啟用了二級域名analytics.codinglabs.org。當然這裡有一點小問題,因為我並沒有https的伺服器,所以如果一個https網站部署了程式碼會有問題,不過這裡我們先忽略吧。
前端統計腳本
我寫了一個不是很完善但能完成基本工作的統計腳本ma.js:
(function () { var params = {}; //Document对象数据 if(document) { params.domain = document.domain || ''; params.url = document.URL || ''; params.title = document.title || ''; params.referrer = document.referrer || ''; } //Window对象数据 if(window && window.screen) { params.sh = window.screen.height || 0; params.sw = window.screen.width || 0; params.cd = window.screen.colorDepth || 0; } //navigator对象数据 if(navigator) { params.lang = navigator.language || ''; } //解析_maq配置 if(_maq) { for(var i in _maq) { switch(_maq[i][0]) { case '_setAccount': params.account = _maq[i][1]; break; default: break; } } } //拼接参数串 var args = ''; for(var i in params) { if(args != '') { args += '&'; } args += i + '=' + encodeURIComponent(params[i]); } //通过Image对象请求后端脚本 var img = new Image(1, 1); img.src = 'http://analytics.codinglabs.org/1.gif?' + args; })();
整個腳本放在匿名函數裡,確保不會污染全域環境。功能在原理一節已經說明,不再贅述。其中1.gif是後端腳本。
日誌格式
日誌採用每行一筆記錄的方式,採用不可見字元^A(ascii碼0x01,Linux下可透過ctrl v ctrl a輸入,下文皆以「^A」表示不可見字元0x01),具體格式如下:
時間^AIP^A網域^AURL^A頁標題^AReferrer^A解析度高^A解析度寬^A顏色深度^ A語言^A客戶端資訊^A使用者識別^A網站標識
後腳本
#为了简单和效率考虑,我打算直接使用nginx的access_log做日志收集,不过有个问题就是nginx配置本身的逻辑表达能力有限,所以我选用了OpenResty做这个事情。OpenResty是一个基于Nginx扩展出的高性能应用开发平台,内部集成了诸多有用的模块,其中的核心是通过ngx_lua模块集成了Lua,从而在nginx配置文件中可以通过Lua来表述业务。关于这个平台我这里不做过多介绍,感兴趣的同学可以参考其官方网站http://openresty.org/,或者这里有其作者章亦春(agentzh)做的一个非常有爱的介绍OpenResty的slide:http://agentzh.org/misc/slides/ngx-openresty-ecosystem/,关于ngx_lua可以参考:https://github.com/chaoslawful/lua-nginx-module。
首先,需要在nginx的配置文件中定义日志格式:
log_format tick “$msec^A$remote_addr^A$u_domain^A$u_url^A$u_title^A$u_referrer^A$u_sh^A$u_sw^A$u_cd^A$u_lang^A$http_user_agent^A$u_utrace^A$u_account”;
注意这里以u_开头的是我们待会会自己定义的变量,其它的是nginx内置变量。
然后是核心的两个location:
location /1.gif { #伪装成gif文件 default_type image/gif; #本身关闭access_log,通过subrequest记录log access_log off; access_by_lua " -- 用户跟踪cookie名为__utrace local uid = ngx.var.cookie___utrace if not uid then -- 如果没有则生成一个跟踪cookie,算法为md5(时间戳+IP+客户端信息) uid = ngx.md5(ngx.now() .. ngx.var.remote_addr .. ngx.var.http_user_agent) end ngx.header['Set-Cookie'] = {'__utrace=' .. uid .. '; path=/'} if ngx.var.arg_domain then -- 通过subrequest到/i-log记录日志,将参数和用户跟踪cookie带过去 ngx.location.capture('/i-log?' .. ngx.var.args .. '&utrace=' .. uid) end "; #此请求不缓存 add_header Expires "Fri, 01 Jan 1980 00:00:00 GMT"; add_header Pragma "no-cache"; add_header Cache-Control "no-cache, max-age=0, must-revalidate"; #返回一个1×1的空gif图片 empty_gif; } location /i-log { #内部location,不允许外部直接访问 internal; #设置变量,注意需要unescape set_unescape_uri $u_domain $arg_domain; set_unescape_uri $u_url $arg_url; set_unescape_uri $u_title $arg_title; set_unescape_uri $u_referrer $arg_referrer; set_unescape_uri $u_sh $arg_sh; set_unescape_uri $u_sw $arg_sw; set_unescape_uri $u_cd $arg_cd; set_unescape_uri $u_lang $arg_lang; set_unescape_uri $u_utrace $arg_utrace; set_unescape_uri $u_account $arg_account; #打开日志 log_subrequest on; #记录日志到ma.log,实际应用中最好加buffer,格式为tick access_log /path/to/logs/directory/ma.log tick; #输出空字符串 echo ''; }
要完全解释这段脚本的每一个细节有点超出本文的范围,而且用到了诸多第三方ngxin模块(全都包含在OpenResty中了),重点的地方我都用注释标出来了,可以不用完全理解每一行的意义,只要大约知道这个配置完成了我们在原理一节提到的后端逻辑就可以了。
日志轮转
日志收集系统需要处理大量的访问日志,在时间的累积下文件规模急剧膨胀,放在同一文件中管理不便。所以通常要按时间段将日志切分,例如每天或每小时切分一个日志。我这里为了效果明显,每一小时切分一个日志。通过 crontab 定时调用一个 shell 脚本,以下是该脚本的内容:
_prefix="/path/to/nginx" time=`date +%Y%m%d%H` mv ${_prefix}/logs/ma.log ${_prefix}/logs/ma/ma-${time}.log kill -USR1 `cat ${_prefix}/logs/nginx.pid`
这个脚本将ma.log移动到指定文件夹并重命名为ma-{yyyymmddhh}.log,然后向nginx发送USR1信号令其重新打开日志文件。
然后再/etc/crontab里加入一行:
59 * * * * root /path/to/directory/rotatelog.sh
在每个小时的59分启动这个脚本进行日志轮转操作。
测试
下面可以测试这个系统是否能正常运行了。我昨天就在我的博客中埋了相关的点,通过http抓包可以看到ma.js和1.gif已经被正确请求。
同时可以看一下1.gif的请求参数。
相关信息确实也放在了请求参数中。
然后我tail打开日志文件,然后刷新一下页面,因为没有设access log buffer, 我立即得到了一条新日志:
1351060731.360^A0.0.0.0^Awww.codinglabs.org^Ahttp://www.codinglabs.org/^ACodingLabs^A^A1024^A1280^A24^Azh-CN^AMozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4^A4d612be64366768d32e623d594e82678^AU-1-1
注意实际上原日志中的^A是不可见的,这里我用可见的^A替换为方便阅读,另外IP由于涉及隐私我替换为了0.0.0.0。
关于分析
通过上面的分析和开发可以大致理解一个网站统计的日志收集系统是如何工作的。有了这些日志,就可以进行后续的分析了。本文只注重日志收集,所以不会写太多关于分析的东西。
注意,原始日志最好尽量多的保留信息而不要做过多过滤和处理。例如上面的MyAnalytics保留了毫秒级时间戳而不是格式化后的时间,时间的格式化是后面的系统做的事而不是日志收集系统的责任。后面的系统根据原始日志可以分析出很多东西,例如通过IP库可以定位访问者的地域、user agent中可以得到访问者的操作系统、浏览器等信息,再结合复杂的分析模型,就可以做流量、来源、访客、地域、路径等分析了。当然,一般不会直接对原始日志分析,而是会将其清洗格式化后转存到其它地方,如MySQL或HBase中再做分析。
分析部分的工作有很多开源的基础设施可以使用,例如实时分析可以使用Storm,而离线分析可以使用Hadoop。当然,在日志比较小的情况下,也可以通过shell命令做一些简单的分析,例如,下面三条命令可以分别得出我的博客在今天上午8点到9点的访问量(PV),访客数(UV)和独立IP数(IP):
awk -F^A '{print $1}' ma-2012102409.log | wc -l awk -F^A '{print $12}' ma-2012102409.log | uniq | wc -l awk -F^A '{print $2}' ma-2012102409.log | uniq | wc -l
以上是如何使用nginx lua實現網站統計中的資料收集的詳細內容。更多資訊請關注PHP中文網其他相關文章!