轉載自:http://blog.codinglabs.org/articles/how-web-analytics-data-collection-system-work.htmljavascript
網站數據統計分析工具是網站站長和運營人員常用的一種工具,比較經常使用的有谷歌分析、百度統計和騰訊分析等等。全部這些統計分析工具的第一步都是網站訪問數據的收集。目前主流的數據收集方式基本都是基於javascript的。本文將簡要分析這種數據收集的原理,並一步一步實際搭建一個實際的數據收集系統。php
簡單來講,網站統計分析工具須要收集到用戶瀏覽目標網站的行爲(如打開某網頁、點擊某按鈕、將商品加入購物車等)及行爲附加數據(如某下單行爲產生的訂單金額等)。早期的網站統計每每只收集一種用戶行爲:頁面的打開。然後用戶在頁面中的行爲均沒法收集。這種收集策略能知足基本的流量分析、來源分析、內容分析及訪客屬性等經常使用分析視角,可是,隨着ajax技術的普遍使用及電子商務網站對於電子商務目標的統計分析的需求愈來愈強烈,這種傳統的收集策略已經顯得力不能及。html
後來,Google在其產品谷歌分析中創新性的引入了可定製的數據收集腳本,用戶經過谷歌分析定義好的可擴展接口,只需編寫少許的javascript代碼就能夠實現自定義事件和自定義指標的跟蹤和分析。目前百度統計、搜狗分析等產品均照搬了谷歌分析的模式。前端
其實提及來兩種數據收集模式的基本原理和流程是一致的,只是後一種經過javascript收集到了更多的信息。下面看一下如今各類網站統計工具的數據收集基本原理。java
首先經過一幅圖整體看一下數據收集的基本流程。python
圖1. 網站統計數據收集基本流程nginx
首先,用戶的行爲會觸發瀏覽器對被統計頁面的一個http請求,這裏姑且先認爲行爲就是打開網頁。當網頁被打開,頁面中的埋點javascript片斷會被執行,用過相關工具的朋友應該知道,通常網站統計工具都會要求用戶在網頁中加入一小段javascript代碼,這個代碼片斷通常會動態建立一個script標籤,並將src指向一個單獨的js文件,此時這個單獨的js文件(圖1中綠色節點)會被瀏覽器請求到並執行,這個js每每就是真正的數據收集腳本。數據收集完成後,js會請求一個後端的數據收集腳本(圖1中的backend),這個腳本通常是一個假裝成圖片的動態腳本程序,可能由php、python或其它服務端語言編寫,js會將收集到的數據經過http參數的方式傳遞給後端腳本,後端腳本解析參數並按固定格式記錄到訪問日誌,同時可能會在http響應中給客戶端種植一些用於追蹤的cookie。git
上面是一個數據收集的大概流程,下面以谷歌分析爲例,對每個階段進行一個相對詳細的分析。github
若要使用谷歌分析(如下簡稱GA),須要在頁面中插入一段它提供的javascript片斷,這個片斷每每被稱爲埋點代碼。下面是個人博客中所放置的谷歌分析埋點代碼截圖:web
圖2. 谷歌分析埋點代碼
其中_gaq是GA的的全局數組,用於放置各類配置,其中每一條配置的格式爲:
1
|
_gaq.push([
'Action'
,
'param1'
,
'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)被請求後會被執行,這個腳本通常要作以下幾件事:
一、經過瀏覽器內置javascript對象收集信息,如頁面title(經過document.title)、referrer(上一跳url,經過document.referrer)、用戶顯示器分辨率(經過windows.screen)、cookie信息(經過document.cookie)等等一些信息。
二、解析_gaq收集配置信息。這裏面可能會包括用戶自定義的事件跟蹤、業務數據(如電子商務網站的商品編號等)等。
三、將上面兩步收集的數據按預約義格式解析並拼接。
四、請求一個後端腳本,將信息放在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的請求:
圖3. 後端腳本請求的http包
能夠看到ga.js在請求__utm.gif時帶了不少信息,例如utmsr=1280×1024是屏幕分辨率,utmac=UA-35712773-1是_gaq中解析出的個人GA標識ID等等。
值得注意的是,__utm.gif未必只會在埋點代碼執行時被請求,若是用_trackEvent配置了事件跟蹤,則在事件發生時也會請求這個腳本。
因爲ga.js通過了壓縮和混淆,可讀性不好,咱們就不分析了,具體後面實現階段我會實現一個功能相似的腳本。
GA的__utm.gif是一個假裝成gif的腳本。這種後端腳本通常要完成如下幾件事情:
一、解析http請求參數的到信息。
二、從服務器(WebServer)中獲取一些客戶端沒法獲取的信息,如訪客ip等。
三、將信息按格式寫入log。
四、生成一副1×1的空gif圖片做爲響應內容並將響應頭的Content-type設爲image/gif。
五、在響應頭中經過Set-cookie設置一些須要的cookie信息。
之因此要設置cookie是由於若是要跟蹤惟一訪客,一般作法是若是在請求時發現客戶端沒有指定的跟蹤cookie,則根據規則生成一個全局惟一的cookie並種植給用戶,不然Set-cookie中放置獲取到的跟蹤cookie以保持同一用戶cookie不變(見圖4)。
圖4. 經過cookie跟蹤惟一用戶的原理
這種作法雖然不是完美的(例如用戶清掉cookie或更換瀏覽器會被認爲是兩個用戶),可是是目前被普遍使用的手段。注意,若是沒有跨站跟蹤同一用戶的需求,能夠經過js將cookie種植在被統計站點的域下(GA是這麼作的),若是要全網統必定位,則經過後端腳本種植在服務端域下(咱們待會的實現會這麼作)。
根據上述原理,我本身搭建了一個訪問日誌收集系統。整體來講,搭建這個系統要作以下的事:
圖5. 訪問數據收集系統工做分解
下面詳述每一步的實現。我將這個系統叫作MyAnalytics。
爲了簡單起見,我不打算實現GA的完整數據收集模型,而是收集如下信息。
名稱 | 途徑 | 備註 |
訪問時間 | web server | Nginx $msec |
IP | web server | Nginx $remote_addr |
域名 | javascript | document.domain |
URL | javascript | document.URL |
頁面標題 | javascript | document.title |
分辨率 | javascript | window.screen.height & width |
顏色深度 | javascript | window.screen.colorDepth |
Referrer | javascript | document.referrer |
瀏覽客戶端 | web server | Nginx $http_user_agent |
客戶端語言 | javascript | navigator.language |
訪客標識 | cookie | |
網站標識 | javascript | 自定義對象 |
埋點代碼我將借鑑GA的模式,可是目前不會將配置對象做爲一個FIFO隊列用。一個埋點代碼的模板以下:
1
2
3
4
5
6
7
8
9
10
|
<script type=
"text/javascript"
>
var
_maq = _maq || [];
_maq.push([
'_setAccount'
,
'網站標識'
]);
(
function
() {
var
ma = document.createElement(
'script'
); ma.type =
'text/javascript'
; ma.async =
true
;
ma.src = (
'https:'
== document.location.protocol ?
'https://analytics'
:
'http://analytics'
) +
'.codinglabs.org/ma.js'
;
var
s = document.getElementsByTagName(
'script'
)[0]; s.parentNode.insertBefore(ma, s);
})();
</script>
|
這裏我啓用了二級域名analytics.codinglabs.org,統計腳本的名稱爲ma.js。固然這裏有一點小問題,由於我並無https的服務器,因此若是一個https站點部署了代碼會有問題,不過這裏咱們先忽略吧。
我寫了一個不是很完善但能完成基本工做的統計腳本ma.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
(
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);
})();
|
整個腳本放在匿名函數裏,確保不會污染全局環境。功能在原理一節已經說明,再也不贅述。其中1.gif是後端腳本。
日誌採用每行一條記錄的方式,採用不可見字符^A(ascii碼0×01,Linux下可經過ctrl + v ctrl + a輸入,下文均用「^A」表示不可見字符0×01),具體格式以下:
時間^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的配置文件中定義日誌格式:
1
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
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腳本實現的,shell腳本以下:
1
2
3
4
5
|
_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里加入一行:
1
|
59 * * * * root
/path/to/directory/rotatelog
.sh
|
在每一個小時的59分啓動這個腳本進行日誌輪轉操做。
下面能夠測試這個系統是否能正常運行了。我昨天就在個人博客中埋了相關的點,經過http抓包能夠看到ma.js和1.gif已經被正確請求:
圖6. http包分析ma.js和1.gif的請求
同時能夠看一下1.gif的請求參數:
圖7. 1.gif的請求參數
相關信息確實也放在了請求參數中。
而後我tail打開日誌文件,而後刷新一下頁面,由於沒有設access log buffer, 我當即獲得了一條新日誌:
1
|
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。
看一眼日誌輪轉目錄,因爲我以前已經埋了點,因此已經生成了不少輪轉文件:
圖8. 輪轉日誌
經過上面的分析和開發能夠大體理解一個網站統計的日誌收集系統是如何工做的。有了這些日誌,就能夠進行後續的分析了。本文只注重日誌收集,因此不會寫太多關於分析的東西。
注意,原始日誌最好儘可能多的保留信息而不要作過多過濾和處理。例如上面的MyAnalytics保留了毫秒級時間戳而不是格式化後的時間,時間的格式化是後面的系統作的事而不是日誌收集系統的責任。後面的系統根據原始日誌能夠分析出不少東西,例如經過IP庫能夠定位訪問者的地域、user agent中能夠獲得訪問者的操做系統、瀏覽器等信息,再結合複雜的分析模型,就能夠作流量、來源、訪客、地域、路徑等分析了。固然,通常不會直接對原始日誌分析,而是會將其清洗格式化後轉存到其它地方,如MySQL或HBase中再作分析。
分析部分的工做有不少開源的基礎設施可使用,例如實時分析可使用Storm,而離線分析可使用Hadoop。固然,在日誌比較小的狀況下,也能夠經過shell命令作一些簡單的分析,例如,下面三條命令能夠分別得出個人博客在今天上午8點到9點的訪問量(PV),訪客數(UV)和獨立IP數(IP):
1
2
3
|
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
|
其它好玩的東西朋友們能夠慢慢挖掘。
GA的開發者文檔:https://developers.google.com/analytics/devguides/collection/gajs/
一篇關於實現nginx收日誌的文章:http://blog.linezing.com/2011/11/%E4%BD%BF%E7%94%A8nginx%E8%AE%B0%E6%97%A5%E5%BF%97
關於Nginx能夠參考:http://wiki.nginx.org/Main
OpenResty的官方網站爲:http://openresty.org
ngx_lua模塊可參考:https://github.com/chaoslawful/lua-nginx-module