1. 程式人生 > >網站統計中的數據收集原理及實現

網站統計中的數據收集原理及實現

fun 美的 置配 客戶 etc 分析 獲取 固定 open

網站統計中的數據收集原理及實現 網站數據統計分析工具是網站站長和運營人員經常使用的一種工具,比較常用的有谷歌分析、百度統計和騰訊分析等等。所有這些統計分析工具的第一步都是網站訪問數據的收集。目前主流的數據收集方式基本都是基於javascript的。本文將簡要分析這種數據收集的原理,並一步一步實際搭建一個實際的數據收集系統。

  數據收集原理分析

  簡單來說,網站統計分析工具需要收集到用戶瀏覽目標網站的行為(如打開某網頁、點擊某按鈕、將商品加入購物車等)及行為附加數據(如某下單行為產生的訂單金額等)。早期的網站統計往往只收集一種用戶行為:頁面的打開。而後用戶在頁面中的行為均無法收集。這種收集策略能滿足基本的流量分析、來源分析、內容分析及訪客屬性等常用分析視角,但是,隨著ajax技術的廣泛使用及電子商務網站對於電子商務目標的統計分析的需求越來越強烈,這種傳統的收集策略已經顯得力不能及。

  後來,Google在其產品谷歌分析中創新性的引入了可定制的數據收集腳本,用戶通過谷歌分析定義好的可擴展接口,只需編寫少量的javascript代碼就可以實現自定義事件和自定義指標的跟蹤和分析。目前百度統計、搜狗分析等產品均照搬了谷歌分析的模式。

  其實說起來兩種數據收集模式的基本原理和流程是一致的,只是後一種通過javascript收集到了更多的信息。下面看一下現在各種網站統計工具的數據收集基本原理。

  流程概覽

  首先通過一幅圖總體看一下數據收集的基本流程。


技術分享圖片圖1. 網站統計數據收集基本流程

  首先,用戶的行為會觸發瀏覽器對被統計頁面的一個http請求,這裏姑且先認為行為就是打開網頁。當網頁被打開,頁面中的埋點javascript片段會被執行,用過相關工具的朋友應該知道,一般網站統計工具都會要求用戶在網頁中加入一小段javascript代碼,這個代碼片段一般會動態創建一個script標簽,並將src指向一個單獨的js文件,此時這個單獨的js文件(圖1中綠色節點)會被瀏覽器請求到並執行,這個js往往就是真正的數據收集腳本。數據收集完成後,js會請求一個後端的數據收集腳本(圖1中的backend),這個腳本一般是一個偽裝成圖片的動態腳本程序,可能由php、python或其它服務端語言編寫,js會將收集到的數據通過http參數的方式傳遞給後端腳本,後端腳本解析參數並按固定格式記錄到訪問日誌,同時可能會在http響應中給客戶端種植一些用於追蹤的cookie。

  上面是一個數據收集的大概流程,下面以谷歌分析為例,對每一個階段進行一個相對詳細的分析。

  埋點腳本執行階段

  若要使用谷歌分析(以下簡稱GA),需要在頁面中插入一段它提供的javascript片段,這個片段往往被稱為埋點代碼。下面是我的博客中所放置的谷歌分析埋點代碼截圖:
技術分享圖片

圖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)被請求後會被執行,這個腳本一般要做如下幾件事:

  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的請求:

技術分享圖片

圖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的腳本。這種後端腳本一般要完成以下幾件事情:

  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)。

技術分享圖片
圖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); img.src = ‘http://analytics.codinglabs.org/1.gif?‘ + args; })();

  整個腳本放在匿名函數裏,確保不會汙染全局環境。功能在原理一節已經說明,不再贅述。其中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

網站統計中的數據收集原理及實現