為什么要進行網站流量數據統計分析?
隨着大數據時代的到來,各行各業產生的數據呈爆發式增長,大數據的技術從之前的“虛無”變成可能,數據產生的各種潛在價值慢慢的被人們挖掘出來利用在各行各業上。比如網站流量數據統計分析,可以幫助網站管理員、運營人員、推廣人員等實時獲取網站流量信息,並從流量來源、網站內容、網站訪客特性等多方面提供網站分析的數據依據。從而幫助提高網站流量,提升網站用戶體驗,讓訪客更多的沉淀下來變成會員或客戶,通過更少的投入獲取最大化的收入。
網站流量日志數據采集原理分析
首先,用戶的行為會觸發瀏覽器對被統計頁面的一個 http 請求,比如打開某網頁。當網頁被打開,頁面中的埋點 javascript 代碼會被執行。
埋點是指:在網頁中預先加入小段 javascript 代碼,這個代碼片段一般會動態創建一個 script 標簽,並將 src 屬性指向一個單獨的 js 文件,此時這個單獨的 js 文件(圖中綠色節點)會被瀏覽器請求到並執行,這個 js 往往就是真正的數據收集腳本。
數據收集完成后,js 會請求一個后端的數據收集腳本(圖中的 backend),這個腳本一般是一個偽裝成圖片的動態腳本程序,js 會將收集到的數據通過http 參數的方式傳遞給后端腳本,后端腳本解析參數並按固定格式記錄到訪問日志,同時可能會在 http 響應中給客戶端種植一些用於追蹤的 cookie。
設計實現
根據原理分析並結合 Google Analytics,想搭建一個自定義日志數據采集系統,要做以下幾件事:
確定收集信息
名稱 | 途徑 | 備注 |
訪問時間 | 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 | Nginx $http_cookie |
網站標識 | javascript | 自定義對象 |
狀態碼 | web server | Nginx $status |
發送內容量 | web server | Nginx $body_bytes_sent |
確定埋點代碼
埋點,是網站分析的一種常用的數據采集方法。核心就是在需要進行數據采集的關鍵點植入統計代碼,進行數據的采集。比如以谷歌分析原型來說,需要在頁面中插入一段它提供的 javascript 片段,這個片段往往被稱為埋點代碼。(以Google的埋點代碼為例)
<script type="text/javascript"> var _maq = _maq || []; _maq.push(['_setAccount', 'UA-XXXXX-X']); (function() { var ma = document.createElement('script'); ma.type = 'text/javascript'; ma.async = true; ma.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ma.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore( m a, s); })(); </script>
其中_maq 是全局數組,用於放置各種配置,其中每一條配置的格式為:
_maq.push(['Action', 'param1', 'param2', ...]);
_maq 的機制不是重點,重點是后面匿名函數的代碼,這段代碼的主要目的就是引入一個外部的 js 文件(ma.js),方式是通過 document.createElement 方法創建一個 script 並根據協議(http 或 https)將 src 指向對應的 ma.js,最后將這個元素插入頁面的 dom 樹上。
注意 ma.async = true 的意思是異步調用外部 js 文件,即不阻塞瀏覽器的解析,待外部 js 下載完成后異步執行。這個屬性是 HTML5 新引入的。
前端數據收集腳本
數據收集腳本(ma.js)被請求后會被執行,一般要做如下幾件事:
-
通過瀏覽器內置javascript對象收集信息,如頁面title (通過document.title)、referrer(上一跳 url,通過 document.referrer)、用戶顯示器分辨率(通過windows.screen)、cookie 信息(通過 document.cookie)等等一些信息。
- 解析_maq 數組,收集配置信息。這里面可能會包括用戶自定義的事件跟蹤、業務數據(如電子商務網站的商品編號等)等。
- 將上面兩步收集的數據按預定義格式解析並拼接(get 請求參數)。
- 請求一個后端腳本,將信息放在 http request 參數中攜帶給后端腳本。
這里唯一的問題是步驟 4,javascript 請求后端腳本常用的方法是 ajax,但是ajax 是不能跨域請求的。一種通用的方法是 js 腳本創建一個 Image 對象,將 Image對象的 src 屬性指向后端腳本並攜帶參數,此時即實現了跨域請求后端。這也是后端腳本為什么通常偽裝成 gif 文件的原因。
示例代碼
(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://xxx.xxxxx.xxxxx/log.gif? ' + args; })();
整個腳本放在匿名函數里,確保不會污染全局環境。其中 log.gif 是后端腳本。
后端腳本
log.gif 是后端腳本,是一個偽裝成 gif 圖片的腳本。后端腳本一般需要完成以下幾件事情:
- 解析 http 請求參數得到信息。
- 從 Web 服務器中獲取一些客戶端無法獲取的信息,如訪客 ip 等。
- 將信息按格式寫入 log。
- 生成一副 1×1 的空 gif 圖片作為響應內容並將響應頭的 Content-type設為 image/gif。
- 在響應頭中通過 Set-cookie 設置一些需要的 cookie 信息。
之所以要設置 cookie 是因為如果要跟蹤唯一訪客,通常做法是如果在請求時發現客戶端沒有指定的跟蹤 cookie,則根據規則生成一個全局唯一的 cookie 並種植給用戶,否則 Set-cookie 中放置獲取到的跟蹤 cookie 以保持同一用戶 cookie不變。這種做法雖然不是完美的(例如用戶清掉 cookie 或更換瀏覽器會被認為是兩個用戶),但是目前被廣泛使用的手段。
我們使用 nginx 的 的 access_log 做日志收集,不過有個問題就是 nginx 配置本身的邏輯表達能力有限,所以選用 OpenResty 做這個事情。
OpenResty 是一個基於 Nginx 擴展出的高性能應用開發平台,內部集成了諸多有用的模塊,其中的核心是通過 ngx_lua 模塊集成了 Lua,從而在 nginx 配置文件中可以通過 Lua 來表述業務。
Lua 是一種輕量小巧的腳本語言,用標准 C 語言編寫並以源代碼形式開放,其設計目的是為了嵌入應用程序中,從而為應用程序提供靈活的擴展和定制功能。
首先,需要在 nginx 的配置文件中定義日志格式:
log_format tick "$msec||$remote_addr||$status||$body_bytes_sent||$u_domain||$u_url| |$u_title||$u_referrer||$u_sh||$u_sw||$u_cd||$u_lang||$http_user_ag ent||$u_account";
注意這里以 u_開頭的是我們待會會自己定義的變量,其它的是 nginx 內置變量。然后是核心的兩個 location:
location / log.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,來自 ngx_set_misc 模塊 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_account $arg_account; #打開日志 log_subrequest on; #記錄日志到 ma.log 格式為 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 信號令其重新打開日志文件。
USR1 通常被用來告知應用程序重載配置文件, 向服務器發送一個 USR1 信號將導致以下步驟的發生:停止接受新的連接,等待當前連接停止,重新載入配置文件,重新打開日志文件,重啟服務器,從而實現相對平滑的不關機的更改。
cat ${_prefix}/logs/nginx.pid 取 nginx 的進程號
然后再/etc/crontab 里加入一行:
59 * * * * root /path/to/directory/rotatelog.sh
在每個小時的 59 分啟動這個腳本進行日志輪轉操作。