精华内容
下载资源
问答
  • 网站数据统计分析工具是网站站长和运营人员经常使用的一种工具,比较常用的有谷歌分析、百度统计 和 腾讯分析等等。所有这些统计分析工具的第一...简单来说,网站统计分析工具需要收集到用户浏览目标网站的行为(如打

    网站数据统计分析工具是网站站长和运营人员经常使用的一种工具,比较常用的有谷歌分析百度统计  腾讯分析等等。所有这些统计分析工具的第一步都是网站访问数据的收集。目前主流的数据收集方式基本都是基于javascript的。本文将简要分析这种数据收集的原理,并一步一步实际搭建一个实际的数据收集系统。

    1、数据收集原理分析

    简单来说,网站统计分析工具需要收集到用户浏览目标网站的行为(如打开某网页、点击某按钮、将商品加入购物车等)及行为附加数据(如某下单行为产生的订单金额等)。早期的网站统计往往只收集一种用户行为:页面的打开。而后用户在页面中的行为均无法收集。这种收集策略能满足基本的流量分析、来源分析、内容分析及访客属性等常用分析视角,但是,随着ajax技术的广泛使用及电子商务网站对于电子商务目标的统计分析的需求越来越强烈,这种传统的收集策略已经显得力不能及。
    后来,Google在其产品谷歌分析中创新性的引入了可定制的数据收集脚本,用户通过谷歌分析定义好的可扩展接口,只需编写少量的javascript代码就可以实现自定义事件和自定义指标的跟踪和分析。目前百度统计、搜狗分析等产品均照搬了谷歌分析的模式。
    其实说起来两种数据收集模式的基本原理和流程是一致的,只是后一种通过javascript收集到了更多的信息。下面看一下现在各种网站统计工具的数据收集基本原理。

    1.1 流程概览

    首先通过一幅图总体看一下数据收集的基本流程。


    图1. 网站统计数据收集基本流程

    首先,用户的行为会触发浏览器对被统计页面的一个http请求,这里姑且先认为行为就是打开网页。当网页被打开,页面中的埋点javascript片段会被执行,用过相关工具的朋友应该知道,一般网站统计工具都会要求用户在网页中加入一小段javascript代码,这个代码片段一般会动态创建一个script标签,并将src指向一个单独的js文件,此时这个单独的js文件(图1中绿色节点)会被浏览器请求到并执行,这个js往往就是真正的数据收集脚本。数据收集完成后,js会请求一个后端的数据收集脚本(图1中的backend),这个脚本一般是一个伪装成图片的动态脚本程序,可能由php、python或其它服务端语言编写,js会将收集到的数据通过http参数的方式传递给后端脚本,后端脚本解析参数并按固定格式记录到访问日志,同时可能会在http响应中给客户端种植一些用于追踪的cookie。
    上面是一个数据收集的大概流程,下面以谷歌分析为例,对每一个阶段进行一个相对详细的分析。

    1.2 埋点脚本执行阶段

    若要使用谷歌分析(以下简称GA),需要在页面中插入一段它提供的javascript片段,这个片段往往被称为埋点代码。下面是我的博客中所放置的谷歌分析埋点代码截图:

     

    图2. 谷歌分析埋点代码

    其中_gaq是GA的的全局数组,用于放置各种配置,其中每一条配置的格式为:

    _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新引入的。

    1.3 数据收集脚本执行阶段

    数据收集脚本(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经过了压缩和混淆,可读性很差,我们就不分析了,具体后面实现阶段我会实现一个功能类似的脚本。

    1.4 后端脚本执行阶段

    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是这么做的),如果要全网统一定位,则通过后端脚本种植在服务端域下(我们待会的实现会这么做)。

    2、系统的设计实现

    根据上述原理,我自己搭建了一个访问日志收集系统。总体来说,搭建这个系统要做如下的事:

    图5. 访问数据收集系统工作分解

    下面详述每一步的实现。我将这个系统叫做MyAnalytics。

    2.1 确定收集的信息

    为了简单起见,我不打算实现GA的完整数据收集模型,而是收集以下信息。

    名称途径备注
    访问时间web serverNginx $msec
    IPweb serverNginx $remote_addr
    域名javascriptdocument.domain
    URLjavascriptdocument.URL
    页面标题javascriptdocument.title
    分辨率javascriptwindow.screen.height & width
    颜色深度javascriptwindow.screen.colorDepth
    Referrerjavascriptdocument.referrer
    浏览客户端web serverNginx $http_user_agent
    客户端语言javascriptnavigator.language
    访客标识cookie 
    网站标识javascript自定义对象

    2.2 埋点代码

    埋点代码我将借鉴GA的模式,但是目前不会将配置对象作为一个FIFO队列用。一个埋点代码的模板如下:

    <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站点部署了代码会有问题,不过这里我们先忽略吧。

    2.3 前端统计脚本

    我写了一个不是很完善但能完成基本工作的统计脚本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是后端脚本。

    2.4 日志格式

    日志采用每行一条记录的方式,采用不可见字符^A(ascii码0x01,Linux下可通过ctrl + v ctrl + a输入,下文均用“^A”表示不可见字符0x01),具体格式如下:
    时间^AIP^A域名^AURL^A页面标题^AReferrer^A分辨率高^A分辨率宽^A颜色深度^A语言^A客户端信息^A用户标识^A网站标识

    2.5 后端脚本

    为了简单和效率考虑,我打算直接使用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中了),重点的地方我都用注释标出来了,可以不用完全理解每一行的意义,只要大约知道这个配置完成了我们在原理一节提到的后端逻辑就可以了。

    2.6 日志轮转

    真正的日志收集系统访问日志会非常多,时间一长文件变得很大,而且日志放在一个文件不便于管理。所以通常要按时间段将日志切分,例如每天或每小时切分一个日志。我这里为了效果明显,每一小时切分一个日志。我是通过crontab定时调用一个shell脚本实现的,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分启动这个脚本进行日志轮转操作。

    2.7 测试

    下面可以测试这个系统是否能正常运行了。我昨天就在我的博客中埋了相关的点,通过http抓包可以看到ma.js和1.gif已经被正确请求:

    图6. http包分析ma.js和1.gif的请求

    同时可以看一下1.gif的请求参数:

    图7. 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。
    看一眼日志轮转目录,由于我之前已经埋了点,所以已经生成了很多轮转文件:

    图8. 轮转日志

    3、日志统计分析

    通过上面的分析和开发可以大致理解一个网站统计的日志收集系统是如何工作的。有了这些日志,就可以进行后续的分析了。本文只注重日志收集,所以不会写太多关于分析的东西。
    注意,原始日志最好尽量多的保留信息而不要做过多过滤和处理。例如上面的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

    其它好玩的东西朋友们可以慢慢挖掘。

    4、Refer:

    [1] GA的开发者文档:

    https://developers.google.com/analytics/devguides/collection/gajs/

    [2] 一篇关于实现nginx收日志的文章:

    http://blog.linezing.com/2011/11/%E4%BD%BF%E7%94%A8nginx%E8%AE%B0%E6%97%A5%E5%BF%97

    [3] ngx_lua模块可参考:https://github.com/chaoslawful/lua-nginx-module

    [4] Google Analytics 网址的构造:http://www.biaodianfu.com/google-analytics-url-builder.html

    [5] 网站分析度量、意义以及不为人所知的(2)

    http://www.chinawebanalytics.cn/metrics-and-its-back-story-2/

    [6]Google Analytics(分析)数据收集 官方文档

    https://developers.google.com/analytics/devguides/collection/

    [7] Google Analytics 使用教程

    http://wenku.baidu.com/view/390c59200722192e4536f626.html

    [8] 使用nginx记日志

    http://blogread.cn/it/article/4861?f=wb#original

    [9] 记录一下互联网日志实时收集和实时计算的简单方案

    http://dwz.cn/2gq4dp

    [10] 南游记:WOT 2015“互联网+”时代大数据技术峰会

    http://jxy.me/2015/11/29/south-trip/

    [11] JSTracker 之前端异常数据采集

    http://taobaofed.org/blog/2015/10/28/jstracker-how-to-collect-data/

    [12] JSTracker 之异常数据处理

    http://taobaofed.org/blog/2015/11/06/jstracker-data-processing/

    [13] try catch 对代码运行的性能影响

    http://taobaofed.org/blog/2015/10/28/try-catch-runing-problem/

    [14] 初创公司构建数据分析平台

    http://www.infoq.com/cn/presentations/start-up-companies-build-data-analysis-platform

    [15] 如何用数据驱动产品和运营

    http://geek.csdn.net/news/detail/60279

    [16] TalkingData肖文峰:移动统计分析,难点何在?

    http://www.afenxi.com/post/10422

    [17] 把用户行为分析做到极致

    https://zhuanlan.zhihu.com/p/20788978?refer=sangwf

    注:本文http抓包使用Chrome浏览器开发者工具,绘制思维导图使用Xmind,流程和结构图使用Tikz PGF


    原文地址:http://blog.codinglabs.org/articles/how-web-analytics-data-collection-system-work.html

    展开全文
  • 2001-使用Hive+MR统计分析网站指标

    千次阅读 2015-11-25 11:02:54
    使用Hive+MR统计分析网站指标
    1. 网站用户行为分析背景
    
    数据源来自网站渠道用户行为日志,每天产生10G用户日志。产生的日志的特点:
    (1)每小时生成一个文件,每个文件约50M,每天每台日志采集服务器产生24个文件
    (2)生产环境共有8台日志采集服务器,故每天产生日志:8 * (50*24) 约为10G
    (3)通过shell脚本,对每天采集服务器上的日志文件进行合并形成一个大约1G的文件,命名格式:日期.log。例如: 2015-07-05.log

    1.1 数据收集
    在”统计电商网站的PV“案例中,我们收集的原始日志文件部分内容如图所示。
    "05/Jul/2015:00:01:04 +0800" "GET" "http%3A//jf.10086.cn/m/" "HTTP/1.1" "200" "http://jf.10086.cn/m/subject/100000000000009_0.html" "Mozilla/5.0 (Linux; U; Android 4.4.2; zh-cn; Lenovo A3800-d Build/LenovoA3800-d) AppleWebKit/533.1 (KHTML, like Gecko)Version/4.0 MQQBrowser/5.4 TBS/025438 Mobile Safari/533.1 MicroMessenger/6.2.0.70_r1180778.561 NetType/cmnet Language/zh_CN" "10.139.198.176" "480x854" "24" "%u5927%u7C7B%u5217%u8868%u9875_%u4E2D%u56FD%u79FB%u52A8%u79EF%u5206%u5546%u57CE" "0" "3037487029517069460000" "3037487029517069460000" "1" "75"
    "05/Jul/2015:00:01:04 +0800"  "GET" "http%3A//jf.10086.cn/portal/ware/web/SearchWareAction%3Faction%3DsearchWareInfo%26pager.offset%3D144" "HTTP/1.1" "200" "http://jf.10086.cn/portal/ware/web/SearchWareAction?action=searchWareInfo&pager.offset=156" "Mozilla/5.0 (Linux; U; Android 4.4.2; zh-CN; HUAWEI MT2-L01 Build/HuaweiMT2-L01) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 UCBrowser/10.5.2.598 U3/0.8.0 Mobile Safari/534.30" "223.73.104.224" "720x1208" "32" "%u641C%u7D22_%u4E2D%u56FD%u79FB%u52A8%u79EF%u5206%u5546%u57CE" "0" "3046252153674140570000" "3046252153674140570000" "1" "2699"
    "05/Jul/2015:00:01:04 +0800"  "GET" "" "HTTP/1.1" "200" "http://jf.10086.cn/" "Mozilla/5.0 (Linux; Android 4.4.4; vivo Y13L Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/33.0.0.0 Mobile Safari/537.36 baiduboxapp/5.1 (Baidu; P1 4.4.4)" "10.154.210.240" "480x855" "32" "%u9996%u9875_%u4E2D%u56FD%u79FB%u52A8%u79EF%u5206%u5546%u57CE" "0" "3098781670304015290000" "3098781670304015290000" "0" "831"
      

    日志文件中列与列之间用空格进行分割,每个列用双引号,每列字段具体含义如表所示。
    序号
    字段名称
    字段类型
    列含义
    举例
    0
    time_local
    string
    访问日期
    05/Jul/2015:05:01:05 +0800
    1
    request_method
    string
    请求方法
    GET
    2
    arg_referrerPage
    string
    当前页面前一个页面
    http%3A//wap.jf.10086.cn/
    3
    server_protocol
    string
    协议
    HTTP/1.1
    4
    status
    string
    响应状态
    200
    5
    http_referer
    string
    请求页面
    http://jf.10086.cn/
    6
    http_user_agent
    string
    请求代理
    Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Mobile/11D257
    7
    http_x_forwarded_for
    string
    用户IP地址
    117.95.112.54
    8
    screenSize
    string
    屏幕分辨率
    320x568
    9
    screenColor
    string
    颜色
    32
    10
    pageTitle
    string
    页面标题
    %u9996%u9875_%u4E2D%u56FD%u79FB%u52A8%u79EF%u5206%u5546%u57CE
    11
    siteType
    string
    渠道
    (0-网站端,1-移动端)
    0
    12
    uid
    string
    用户访问唯一标示(uid)
    3011949129193080000000
    13
    sid
    string
    用户会话标示(sid)
    3011949129193080000000
    14
    sflag
    string
    会话
    (1-新增,0-更新)
    1
    15
    onloadTotalTime
    string
    页面访问时长
    452
    16
    access_day
    string
    分区表
     
      

    创建日志原始Hive表data_collect代码如下:
    create external table data_collect(
    time_local string,
    request_method string ,
    referrerPage string,
    server_protocol string,
    status string  ,
    http_referer string,
    http_user_agent string,
    http_x_forwarded_for string,
    screenSize string,
    screenColor string,
    pageTitle string ,
    siteType string,
    uid string,
    sid string ,
    sflag string  ,
    onloadTotalTime string          
    )partitioned by(access_day string)
    row format SERDE 'org.apache.hadoop.hive.contrib.serde2.RegexSerDe' 
    WITH SERDEPROPERTIES  (
    'input.regex'='"(.*?)[\\s][+0-9]*"[\\s]*"(.*?)"[\\s]*"(.*?)"[\\s]*"(.*?)"[\\s]*"(.*?)"[\\s]*"(.*?)"[\\s]*"(.*?)"[\\s]*"(.*?)"[\\s]*"(.*?)"[\\s]*"(.*?)"[\\s]*"(.*?)"[\\s]*"(.*?)"[\\s]*"(.*?)"[\\s]*"(.*?)"[\\s]*"(.*?)"[\\s]*"(.*?)"[\\s]*', 
    'output.format.string' = '%1$s   %2$s %3$s %4$s %5$s %6$s %7$s %8$s %9$s %10$s    %11$s     %12$s    %13$s    %14$s    %15$s    %16$s')
    STORED AS TEXTFILE;
      

    加载数据
    load data local inpath '/home/hadoop/20150705' overwrite into table data_collect partition(access_day=20150705)
      
    验证Hive表
    hive (jfyun)> add jar /home/hadoop/app/hive/lib/hive-contrib-0.13.0.jar;
    hive (jfyun)> select time_local ,request_method,onloadTotalTime ,access_day from data_collect;

    time_local      request_method  onloadtotaltime access_day
    06/Jul/2015:00:01:04 +0800      GET     75      20150706
    06/Jul/2015:01:01:04 +0800      GET     2699      20150706
    06/Jul/2015:02:01:04 +0800      GET     831     20150706
    06/Jul/2015:03:01:07 +0800      GET     135     20150706
      

    备注:其中access_day一列是分区信息,表示当前日期。
    2. 网站总体概况
    2.1 PV统计(页面访问量)
    (1) 基本概念
    通常是衡量一个网络新闻频道或网站甚至一条网络新闻的主要指标。网页浏览数是评价网站流量最常用的指标之一,简称为PV。监测网站PV的变化趋势和分析其变化原因是很多站长定期要做的工作。 Page Views中的Page一般是指普通的html网页,也包含php、jsp等动态产生的html内容。来自浏览器的一次html内容请求会被看作一个PV,逐渐累计成为PV总数。
    (2) 计算方法
    用户每1次对网站中的每个网页访问均被记录1次。用户对同一页面的多次访问,访问量累计。
    2.2 UV统计(网站独立访客)
    (1) 基本概念
    独立IP:是指独立用户/独立访客。指访问某个站点或点击某条新闻的不同IP地址的人数
    (2) 计算方法
    在同一天的00:00-24:00内,独立IP只记录第一次进入网站的具有独立IP的访问者,可以通过设置cookie,记录第一次访问设置新用户,后续为老用户
    2.3 每天的PV|UV|人均访问页面数(PV/UV)
    统计PV|UV|人均访问页面数(PV/UV)的HQL
    select access_day,count(1) pv,count(distinct uid) uv,count(1)/count(distinct uid) avg_visit_page from data_collect group by access_day;
    统计2015-07-05 两台日期采集服务器的PV|UV|人均访问页面数(PV/UV)
    access_day       pv       uv      avg_visit_page
    20150705        4253807 428268   9.93
    Time taken: 321.797 seconds, Fetched: 1 row(s)

    通过1台4G内存/2CPU 虚拟机上测试2G日志数据花费5分钟, 整个过程使用Map: 8  Reduce: 3 。
    2.4 平均网站停留时间
         停留时间是指用户访问网站的时间长短,即用户打开商城的最后一个页面的时间点减去打开商城第一个页面的时间点.计算公式: 每个访客每天的网站停留时间=最后一次时间-首次访问时间。
    统计每个用户停留时间
    create table avg_stay_time_tmp
    as
    select access_day,uid ,max(unix_timestamp(time_local,'dd/MMM/yyyy:HH:mm:ss'))-min(unix_timestamp(time_local,'dd/MMM/yyyy:HH:mm:ss')) stay_time from data_collect  group by access_day ,uid;
    统计网站平均停留时间
    select access_day ,ceil(sum(stay_time)/count(1)) avg_stay_time from avg_stay_time_tmp group by access_day;


    统计结果为
    access_day      avg_stay_time
    20150705        315

    2.5 统计每个IP的访问量
    可以分析那个IP对网站的访问最高,采用Hive平台的HQL进行统计。

    select http_x_forwarded_for,count(sid) count
    from data_collect
    group by http_x_forwarded_for
    order by count desc
    limit 10;


    2.5 统计每个省份页面访问量
    采用Hive平台的HQL进行统计,由于用户访问日志仅有对应的IP地址,没有对应的IP,那么需要通过IP获取对应的省份,然后进行分类统计。

    自定义UDF函数,将IP专为省份
    public class IpToProv extends UDF {
    
         public static Map<String, String> map = new HashMap<String, String>();
    
         static {
              map.put("100", "北京");
              map.put("200", "广东");
              map.put("210", "上海");
              map.put("220", "天津");
              map.put("230", "重庆");
              map.put("240", "辽宁");
              map.put("250", "江苏");
              map.put("270", "湖北");
              map.put("280", "四川");
              map.put("290", "陕西");
              map.put("311", "河北");
              map.put("351", "山西");
              map.put("371", "河南");
              map.put("431", "吉林");
              map.put("451", "黑龙江");
              map.put("471", "内蒙古");
              map.put("531", "山东");
              map.put("551", "安徽");
              map.put("571", "浙江");
              map.put("591", "福建");
              map.put("731", "湖南");
              map.put("771", "广西");
              map.put("791", "江西");
              map.put("851", "贵州");
              map.put("871", "云南");
              map.put("891", "西藏");
              map.put("898", "海南");
              map.put("931", "甘肃");
              map.put("951", "宁夏");
              map.put("971", "青海");
              map.put("991", "新疆");
         }
    
         /**
         * IP转为省份
         *
         * @param ip
         * @return
         */
         public String evaluate(String ip) {
              IPSeeker ipSeeker = new IPSeeker();
              String lookup = ipSeeker.lookup(ip, "999");
              return map.get(lookup) == null ? "北京" : map.get(lookup);
         }
    
         public static void main(String[] args) {
              IpToProv area = new IpToProv();
              String evaluate = area.evaluate("180.155.87.248");
              System.out.println(evaluate);
         }
    }

    自定义的验证
    add jar /home/hadoop/IpToProv.jar
    create temporary function ipToProv as 'cn.hive.IpToProv';
    select ipToProv(http_x_forwarded_for) from data_collect limit 1 ;

    编写HQL语句,直接统计每个省份页面访问量
    select ipToProv(http_x_forwarded_for) province_name ,count(sid) count
    from data_collect
    group by ipToProv(http_x_forwarded_for)
    order by count desc;
    limit 10;
    备注:省份编码列表
    序号
    编码
    名称
    1
    100
    北京
    2
    200
    广东
    3
    210
    上海
    4
    220
    天津
    5
    230
    重庆
    6
    240
    辽宁
    7
    250
    江苏
    8
    270
    湖北
    9
    280
    四川
    10
    290
    陕西
    11
    311
    河北
    12
    351
    山西
    13
    371
    河南
    14
    431
    吉林
    15
    451
    黑龙江
    16
    471
    内蒙古
    17
    531
    山东
    18
    551
    安徽
    19
    571
    浙江
    20
    591
    福建
    21
    731
    湖南
    22
    771
    广西
    23
    791
    江西
    24
    851
    贵州
    25
    871
    云南
    26
    891
    西藏
    27
    898
    海南
    28
    931
    甘肃
    29
    951
    宁夏
    30
    971
    青海
    31
    991
    新疆
      

    3.统计访问的top20
    3.1 Hive统计网站中url访问top20
    select t.access_day,t.http_referer,t.count
    from
    (select access_day,http_referer,count(1) count from data_collect  where access_day='20150705' group by access_day, http_referer) t
    order by t.count desc
    limit 20;

    统计结果

    3.2 MR统计网站中url访问top20
    因为统计top20的url,原始日志表中存在大量的冗余字段,通过Hive平台进行过滤。把有效的信息放入到一张基础表。
    create external table top_n(
    access_day string,
    time_local string,
    http_referer string,
    uid string,
    sid string,
    loadtotaltime int
    )
    row format delimited
    fields terminated by '\t';
    
    insert overwrite  table top_n
    select  access_day ,time_local,http_referer,uid,sid,onloadtotaltime from data_collect where access_day='20150705';
    编写MR程序
    /**
    * 统计每天每隔URL的TOPN
    *
    * 集群运行命令: hadoop jar urlTopN.jar hdfs://mycluster:9000/user/hive/warehouse/jfyun.db/top_n  hdfs://mycluster:9000/topN
    * @author shenfl
    *
    */
    public class URLTopN extends Configured implements Tool {
    
         @Override
         public int run(String[] args) throws Exception {
    
              Configuration conf = new Configuration();
              conf.set("mapreduce.output.basename", "topN");// 修改Reduce生产文件的名称
              conf.set("mapreduce.output.textoutputformat.separator", "$$");// reduce输出结果分隔符修改
              conf.set("N", "20");// 设置topN
              Job job = Job.getInstance(conf, URLTopN.class.getSimpleName());
              job.setJarByClass(URLTopN.class);
    
              FileInputFormat.setInputDirRecursive(job, true);
              FileInputFormat.setInputPaths(job, new Path(args[0]));
              job.setMapperClass(URLTopNMapper.class);
              job.setMapOutputKeyClass(Text.class);
              job.setMapOutputValueClass(LongWritable.class);
             
              job.setReducerClass(URLTopNReducer.class);
              job.setOutputKeyClass(Text.class);
              job.setOutputValueClass(LongWritable.class);
    
              Path outputDir = new Path(args[1]);
              deleteOutDir(conf, outputDir);
              FileOutputFormat.setOutputPath(job, outputDir);
    
              boolean waitForCompletion = job.waitForCompletion(true);
              return waitForCompletion ? 0 : 1;
         }
    
         /**
         * @param conf
         * @param outputDir
         * @throws IOException
         */
         public void deleteOutDir(Configuration conf, Path outputDir) throws IOException {
              FileSystem fs = FileSystem.get(conf);
              if (fs.exists(outputDir)) {
                   fs.delete(outputDir, true);
              }
         }
    
         public static void main(String[] args) {
    
              try {
                   int run = ToolRunner.run(new Configuration(), new URLTopN(), args);
                   System.exit(run);
              } catch (Exception e) {
                   e.printStackTrace();
              }
         }
    
         public static class URLTopNMapper extends Mapper<LongWritable, Text, Text, LongWritable> {
    
              Text k2 = new Text();
              LongWritable v2 = new LongWritable();
    
              @Override
              protected void map(LongWritable k1, Text v1, Context context) throws IOException, InterruptedException {
    
                   String line = v1.toString();
                   String[] splited = line.split("\t");
                   k2.set(splited[2]);
                   v2.set(1);
                   context.write(k2, v2);
              }
         }
    
         public static class URLTopNReducer extends Reducer<Text, LongWritable, Text, LongWritable> {
    
              LongWritable v3 = new LongWritable();
              LogInfoWritable logInfo;
              public static Integer topN = Integer.MIN_VALUE;
    
              /**
              * 存储每个 url访问的次数,并且必须使用TreeMap,默认k升序,可以自己订单k的对象,使用降序排序
              */
    
              TreeMap<LogInfoWritable, NullWritable> logMap = new TreeMap<LogInfoWritable, NullWritable>();
    
              /**
              * ReduceTask任务启动时候调用一次
              */
              @Override
              protected void setup(Reducer<Text, LongWritable, Text, LongWritable>.Context context) throws IOException,
                        InterruptedException {
                   Configuration conf = context.getConfiguration();
                   topN = Integer.parseInt(conf.get("N"));
              }
    
              @Override
              protected void reduce(Text k2, Iterable<LongWritable> v2s, Context context) throws IOException,
                        InterruptedException {
    
                   long sum = 0;
                   for (LongWritable v : v2s) {
                        sum += v.get();
                   }
                   logInfo = new LogInfoWritable();
                   logInfo.set(k2.toString(), sum);
                   logMap.put(logInfo, NullWritable.get());
              }
    
              /**
              * 执行完所有的reduce后,调用cleanup方法.输出前访问量top10的 url 要求:
              * logMap中的value按照从从到到小输出,这样才能获取访问量top10url
              */
              @Override
              protected void cleanup(Context context) throws IOException, InterruptedException {
    
                   int count = 0;
                   for (Map.Entry<LogInfoWritable, NullWritable> entry : logMap.entrySet()) {
                        if (++count <= topN) {
                             context.write(new Text(entry.getKey().getUrl()), new LongWritable(entry.getKey().getSum()));
                        }
                   }
              }
         }
         public static class LogInfoWritable implements WritableComparable<LogInfoWritable> {
    
              private String url;
              private long sum;
             
    
              public String getUrl() {
                   return url;
              }
    
              public void setUrl(String url) {
                   this.url = url;
              }
    
              public long getSum() {
                   return sum;
              }
    
              public void setSum(long sum) {
                   this.sum = sum;
              }
    
              public void set(String url, long sum) {
                   this.url  = url;
                   this.sum = sum;
              }
             
              @Override
              public void write(DataOutput out) throws IOException {
                  
                   out.writeUTF(url);
                   out.writeLong(sum);
              }
              @Override
              public void readFields(DataInput in) throws IOException {
                   this.url = in.readUTF();
                   this.sum = in.readLong();
              }
              //从大到小排序
              @Override
              public int compareTo(LogInfoWritable o) {
                   return this.sum-o.sum>0?-1:1;
              }
             
              @Override
              public String toString() {
                   return this.url + "\t" +  this.sum;
              }
         }
    }

    执行MR程序
    hadoop jar urlTopN.jar hdfs://mycluster:9000/user/hive/warehouse/jfyun.db/top_n    hdfs://mycluster:9000/topN



    4.用户环境
    用户环境是对访问用户的操作系统及浏览器等信息进行统计展示。
    具体用户环境统计信息包括:“浏览器比例分析”、“操作系统比例分析”、“屏幕颜色”、“屏幕尺寸”及“网络属性”。

    4.1 屏幕颜色访问量
    通过屏幕分辨率的访问情况,采用Hive平台表进行统计,统计Top3的数据
    select screencolor,count(distinct uid) count
    from data_collect
    where access_day='20150705'
    group by screencolor
    order by count desc
    limit 3;
    

    4.2 屏幕大小使用情况
    通过屏幕大小的访问情况,采用Hive平台表进行统计,统计Top10的数据。
    select screensize,count(distinct uid) count
    from data_collect
    where access_day='20150705'
    group by screensize
    order by count desc;


    展开全文
  • 网站流量统计分析系统-01

    千次阅读 2018-04-20 20:26:41
    一、需求分析网站流量统计系统需求分析二、项目架构三、具体实现1.js埋点0准备工作(准备两个页面a.jsp和b.jsp):a.jsp代码如下:&lt;%@ page language="java" contentType="text/html; charset...

    一、需求分析

    网站流量统计系统需求分析

    二、项目架构


    三、具体实现

    1.js埋点

    0准备工作(准备两个页面a.jsp和b.jsp):

    a.jsp


    代码如下:

    <%@ page language="java" contentType="text/html; charset=UTF-8"
        pageEncoding="UTF-8"%>
    <!DOCTYPE html>
    <html>
    <head>
    	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    	<title>页面A</title>
    	<script type="text/javascript" src="<%=request.getContextPath()%>/js/tongji.js"></script>
    	
    </head>
    
    <body>
     <span>页面A</span>
      AAAAAAAAAAAAA
      <a href="${pageContext.request.contextPath }/b.jsp">BBBB</a>
    </body>
    
    </html>

    b.jsp

    代码如下:

    <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
    <!DOCTYPE html>
    <html>
    <head>
    	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    	<title>页面B</title>
    	<script type="text/javascript" src="<%=request.getContextPath()%>/js/tongji.js"></script>
    </head>
    <body>
    	BBBBBBBBBBBBB
    </body>
    </html>

    1编写js埋点代码

    1.代码如下

     
    /**函数可对字符串进行编码,这样就可以在所有的计算机上读取该字符串。*/
    function ar_encode(str)
    {
    	//进行URL编码
    	return encodeURI(str);
    }
    
    
    /**屏幕分辨率*/
    function ar_get_screen()
    {
    	var c = "";
    
    	if (self.screen) {
    		c = screen.width+"x"+screen.height;
    	}
    
    	return c;
    }
    
    /**颜色质量*/
    function ar_get_color()
    {
    	var c = ""; 
    
    	if (self.screen) {
    		c = screen.colorDepth+"-bit";
    	}
    
    	return c;
    }
    
    /**返回当前的浏览器语言*/
    function ar_get_language()
    {
    	var l = "";
    	var n = navigator;
    
    	if (n.language) {
    		l = n.language.toLowerCase();
    	}
    	else
    	if (n.browserLanguage) {
    		l = n.browserLanguage.toLowerCase();
    	}
    
    	return l;
    }
    
    /**返回浏览器类型IE,Firefox*/
    function ar_get_agent()
    {
    	var a = "";
    	var n = navigator;
    
    	if (n.userAgent) {
    		a = n.userAgent;
    	}
    
    	return a;
    }
    
    /**方法可返回一个布尔值,该值指示浏览器是否支持并启用了Java*/
    function ar_get_jvm_enabled()
    {
    	var j = "";
    	var n = navigator;
    
    	j = n.javaEnabled() ? 1 : 0;
    
    	return j;
    }
    
    /**返回浏览器是否支持(启用)cookie */
    function ar_get_cookie_enabled()
    {
    	var c = "";
    	var n = navigator;
    	c = n.cookieEnabled ? 1 : 0;
    
    	return c;
    }
    
    /**检测浏览器是否支持Flash或有Flash插件*/
    function ar_get_flash_ver()
    {
    	var f="",n=navigator;
    
    	if (n.plugins && n.plugins.length) {
    		for (var ii=0;ii<n.plugins.length;ii++) {
    			if (n.plugins[ii].name.indexOf('Shockwave Flash')!=-1) {
    				f=n.plugins[ii].description.split('Shockwave Flash ')[1];
    				break;
    			}
    		}
    	}
    	else
    	if (window.ActiveXObject) {
    		for (var ii=10;ii>=2;ii--) {
    			try {
    				var fl=eval("new ActiveXObject('ShockwaveFlash.ShockwaveFlash."+ii+"');");
    				if (fl) {
    					f=ii + '.0';
    					break;
    				}
    			}
    			 catch(e) {}
    		}
    	}
    	return f;
    } 
    
     
    /**匹配顶级域名*/
    function ar_c_ctry_top_domain(str)
    {
    	var pattern = "/^aero$|^cat$|^coop$|^int$|^museum$|^pro$|^travel$|^xxx$|^com$|^net$|^gov$|^org$|^mil$|^edu$|^biz$|^info$|^name$|^ac$|^mil$|^co$|^ed$|^gv$|^nt$|^bj$|^hz$|^sh$|^tj$|^cq$|^he$|^nm$|^ln$|^jl$|^hl$|^js$|^zj$|^ah$|^hb$|^hn$|^gd$|^gx$|^hi$|^sc$|^gz$|^yn$|^xz$|^sn$|^gs$|^qh$|^nx$|^xj$|^tw$|^hk$|^mo$|^fj$|^ha$|^jx$|^sd$|^sx$/i";
    
    	if(str.match(pattern)){ return 1; }
    
    	return 0;
    }
    
    /**处理域名地址*/
    function ar_get_domain(host)
    {
    	//如果存在则截去域名开头的 "www."
    	var d=host.replace(/^www\./, "");
    
    	//剩余部分按照"."进行split操作,获取长度
    	var ss=d.split(".");
    	var l=ss.length;
    
    	//如果长度为3,则为xxx.yyy.zz格式
    	if(l == 3){
    		//如果yyy为顶级域名,zz为次级域名,保留所有
    		if(ar_c_ctry_top_domain(ss[1]) && ar_c_ctry_domain(ss[2])){
    		}
    		//否则只保留后两节
    		else{
    			d = ss[1]+"."+ss[2];
    		}
    	}
    	//如果长度大于3
    	else if(l >= 3){
    
    		//如果host本身是个ip地址,则直接返回该ip地址为完整域名
    		var ip_pat = "^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$";
    		if(host.match(ip_pat)){
    			return d;
    		}
    		//如果host后两节为顶级域名及次级域名,则保留后三节
    		if(ar_c_ctry_top_domain(ss[l-2]) && ar_c_ctry_domain(ss[l-1])) {
    			d = ss[l-3]+"."+ss[l-2]+"."+ss[l-1];
    		}
    		//否则保留后两节
    		else{
    			d = ss[l-2]+"."+ss[l-1];
    		}
    	}
    		
    	return d;
    }
    
    
    /**返回cookie信息*/
    function ar_get_cookie(name)
    {
    	var mn=name+"=";//0.将传来的名字拼接上“=”
    	var b,e;
    	var co=document.cookie;//1.获取当前页面当前浏览器的所有cookie信息
    
    	if (mn=="=") {//2.当传进来的name为空就返回所有“当前页面当前浏览器的所有cookie信息”
    		return co;
    	}
    
    	b=co.indexOf(mn);//3.在“当前页面当前浏览器的所有cookie信息”的数组中查找传递来的cookie
    
    	if (b < 0) {//4.如果没有当前的cookie就返回空
    		return "";
    	}
    
    	e=co.indexOf(";", b+name.length);//5.如果找到了拿出cookie
    
    	if (e < 0) {//6.返回cookie中的值(此处涉及到cookie的结构
    		return co.substring(b+name.length + 1);
    	}
    	else {
    		return co.substring(b+name.length + 1, e);
    	}
    }
    
    /**设置cookie信息*/
    function ar_set_cookie(name, val, cotp) 
    { 
    	var date=new Date; 
    	var year=date.getFullYear(); 
    	var hour=date.getHours(); 
    
    	var cookie="";
    
    	if (cotp == 0) { 
    		cookie=name+"="+val+";"; 
    	} 
    	else if (cotp == 1) { 
    		year=year+10; 
    		date.setYear(year); 
    		cookie=name+"="+val+";expires="+date.toGMTString()+";"; 
    	} 
    	else if (cotp == 2) { 
    		hour=hour+1; 
    		date.setHours(hour); 
    		cookie=name+"="+val+";expires="+date.toGMTString()+";"; 
    	} 
    
    	var d=ar_get_domain(document.domain);
    	if(d != ""){
    		cookie +="domain="+d+";";
    	}
    	cookie +="path="+"/;";
    
    	document.cookie=cookie;
    }
    
    
    
    /**返回客户端时间*/
    function ar_get_stm() 
    { 
    	return new Date().getTime();
    } 
    
    
    /**返回指定个数的随机数字串*/
    function ar_get_random(n) {
    	var str = "";
    	for (var i = 0; i < n; i ++) {
    		str += String(parseInt(Math.random() * 10));
    	}
    	return str;
    }
    
    /* main function */
    function ar_main() {
    	
    	var dest_path   = "http://localhost/LogDemox/servlet/LogServlet?"; 
    	var expire_time = 30 * 60 * 1000;//会话超时时长
    
    	//处理uv
    	//--获取cookie ar_stat_uv的值
    	var uv_str = ar_get_cookie("ar_stat_uv");
    	var uv_id = "";
    	//--如果cookie ar_stat_uv的值为空
    	if (uv_str == ""){
    		//--为这个新uv配置id,为一个长度20的随机数字
    		uv_id = ar_get_random(20);
    		//--设置cookie ar_stat_uv 保存时间为10年
    		ar_set_cookie("ar_stat_uv", uv_id, 1);
    	}
    	//--如果cookie ar_stat_uv的值不为空
    	else{
    		//--获取uv_id
    		uv_id  = uv_str;
    	}
    
    	//处理ss
    	//--获取cookie ar_stat_ss
    	var ss_str = ar_get_cookie("ar_stat_ss"); 
    	var ss_id = "";  //sessin id
    	var ss_no = 0;   //session有效期内访问页面的次数
    
    	//--如果cookie中不存在ar_stat_ss 说明是一次新的会话
    	if (ss_str == ""){
    		//--随机生成长度为10的session id
    		ss_id = ar_get_random(10);
    		//--session有效期内页面访问次数为0
    		ss_no = 0;
    		//--拼接cookie ar_stat_ss 值 格式为 会话编号_会话期内访问次数_客户端时间_网站id
    		value = ss_id+"_"+ss_no+"_"+ar_get_stm();
    		//--设置cookie ar_stat_ss
    		ar_set_cookie("ar_stat_ss", value, 0); 
    	} 
    	//--如果cookie中存在ar_stat_ss
    	else { 
    		//获取ss相关信息
    		var items = ss_str.split("_");
    		//--ss_id
    		var cookie_ss_id  = items[0];
    		//--ss_no
    		var cookie_ss_no  = parseInt(items[1]);
    		//--ss_stm
    		var cookie_ss_stm = items[2];
    
    		//如果当前时间-当前会话上一次访问页面的时间>30分钟,虽然cookie还存在,但是其实已经超时了!仍然需要重新生成cookie
    		if (ar_get_stm() - cookie_ss_stm > expire_time) { 
    			//--重新生成会话id
    			ss_id = ar_get_random(10);
    			//--设置会话中的页面访问次数为0
    			ss_no = 0;
    		} 
    		//--如果会话没有超时
    		else{
    			//--会话id不变
    			ss_id = cookie_ss_id;
    			//--设置会话中的页面方位次数+1
    			ss_no = cookie_ss_no + 1;
    		}
    
    		//--重新拼接cookie ar_stat_ss的值 
    		value = ss_id+"_"+ss_no+"_"+ar_get_stm();
    		ar_set_cookie("ar_stat_ss", value, 0); 
    	}
    
      
        //返回导航到当前网页的超链接所在网页的URL
    	var ref = document.referrer; 
    	ref = ar_encode(String(ref)); 
    
    	//当前地址
    	var url = document.URL; 
    	url = ar_encode(String(url)); 
    
    	//当前资源名
    	var urlname = document.URL.substring(document.URL.lastIndexOf("/")+1);
    	urlname = ar_encode(String(urlname)); 
    
    	//网页标题
    	var title = document.title;
    	title = ar_encode(String(title)); 
    
    	//网页字符集
    	var charset = document.charset;
    	charset = ar_encode(String(charset)); 
    
    	//屏幕信息
    	var screen = ar_get_screen(); 
    	screen = ar_encode(String(screen)); 
    
    	//颜色信息
    	var color =ar_get_color(); 
    	color =ar_encode(String(color)); 
    
    	//语言信息
    	var language = ar_get_language(); 
    	language = ar_encode(String(language));
    
     	//浏览器类型
    	var agent =ar_get_agent(); 
    	agent =ar_encode(String(agent));
    
    	//浏览器是否支持并启用了java
    	var jvm_enabled =ar_get_jvm_enabled(); 
    	jvm_enabled =ar_encode(String(jvm_enabled)); 
    
    	//浏览器是否支持并启用了cookie
    	var cookie_enabled =ar_get_cookie_enabled(); 
    	cookie_enabled =ar_encode(String(cookie_enabled)); 
    
    	//浏览器flash版本
    	var flash_ver = ar_get_flash_ver();
    	flash_ver = ar_encode(String(flash_ver)); 
    
    	//当前uv状态 格式为"会话id_会话次数_当前时间"
    	var stat_ss = ss_id+"_"+ss_no+"_"+ar_get_stm();
    
    	//拼接访问地址 增加如上信息
    	dest=dest_path+"url="+url+"&urlname="+urlname+"&title="+title+"&chset="+charset+"&scr="+screen+"&col="+color+"&lg="+language+"&je="+jvm_enabled+"&ce="+cookie_enabled+"&fv="+flash_ver+"&cnv="+String(Math.random())+"&ref="+ref+"&uagent="+agent+"&stat_uv="+uv_id+"&stat_ss="+stat_ss;
    
    
        //通过插入图片访问该地址
        document.getElementsByTagName("body")[0].innerHTML += "<img src=\""+dest+"\" border=\"0\" width=\"1\" height=\"1\" />";
        
    }
    
    window.onload = function(){
    	//触发main方法
    	ar_main();
    }
    
    

    2.js阅读顺序







    3.验证js埋点

    访问方式


    访问结果:


    连续访问3次的结果


    关闭浏览器,再代开后访问的结果


    重新换一个浏览器访问的结果


    证明js埋点收集到的数据正常

    2编写收集日志的服务端

    1.需要的字段


    2.发现通过js收集到的日志信息有多余的信息并且缺少数据(公网ip)所以收集到的日志需要预处理如下

    ip需要在收集日志的服务端收集这样收集的ip是公网ip,如果在js中收集的ip是内网ip不是我们需要的ip

    因此服务端收集到数据后需要做的操作有:

    (1)进行url转码

    (2)将需要的信息用|分割

    (3)收集公网ip

    代码如下:

    package cn.tedu.flux;
    
    import java.io.IOException;
    import java.net.URLDecoder;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.apache.log4j.Logger;
    
    public class LogServlet extends HttpServlet {
    	private static Logger logger = Logger.getLogger(LogServlet.class);
    	
    	public void doGet(HttpServletRequest request, HttpServletResponse response)
    			throws ServletException, IOException {
    		//获取所有参数
    		String str = request.getQueryString();
    		//进行url解码
    		str = URLDecoder.decode(str,"utf-8");
    		//将str进行处理变为将值用|分隔的形式
    		StringBuffer buffer = new StringBuffer();
    		String [] kvs = str.split("&");
    		for(String kv : kvs){
    			String value = kv.split("=").length >=2 ? kv.split("=")[1] : "";
    			buffer.append(value+"|");
    		}
    		//拼接ip
    		String ip = request.getRemoteAddr();
    		buffer.append(ip);
    		
    		str = buffer.toString();
    		System.out.println(str);
    		logger.info(str);
    	}
    
    	public void doPost(HttpServletRequest request, HttpServletResponse response)
    			throws ServletException, IOException {
    		doGet(request, response);
    	}
    
    }
    

    3.服务端进行预处理后的数据,发现符合需求

    log4j:ERROR Cannot Append to Appender! Appender either closed or not setup correctly!
    http://localhost/demo/a.jsp|a.jsp|页面A|UTF-8|1366x768|24-bit|zh-cn|0|1||0.6847100276188913||Mozilla/5.0 (Windows NT 6.1; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0|58290320824172982441|5764225803_0_1524211141502|0:0:0:0:0:0:0:1
    http://localhost/demo/a.jsp|a.jsp|页面A|UTF-8|1366x768|24-bit|zh-cn|0|1||0.6847100276188913||Mozilla/5.0 (Windows NT 6.1; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0|58290320824172982441|5764225803_0_1524211141502|0:0:0:0:0:0:0:1
    http://localhost/demo/a.jsp|a.jsp|页面A|UTF-8|1366x768|24-bit|zh-cn|0|1||0.6818952847160782||Mozilla/5.0 (Windows NT 6.1; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0|58290320824172982441|5764225803_1_1524211151191|0:0:0:0:0:0:0:1
    http://localhost/demo/a.jsp|a.jsp|页面A|UTF-8|1366x768|24-bit|zh-cn|0|1||0.6818952847160782||Mozilla/5.0 (Windows NT 6.1; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0|58290320824172982441|5764225803_1_1524211151191|0:0:0:0:0:0:0:1
    log4j:ERROR Cannot Append to Appender! Appender either closed or not setup correctly!
    http://localhost/demo/a.jsp|a.jsp|页面A|UTF-8|1366x768|24-bit|zh-cn|0|1|26.9 r9|0.07667920747010104||Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.104 Safari/537.36 Core/1.53.4882.400 QQBrowser/9.7.13059.400|38627225991274809163|1282704282_0_1524211501931|0:0:0:0:0:0:0:1
    http://localhost/demo/a.jsp|a.jsp|页面A|UTF-8|1366x768|24-bit|zh-cn|0|1|26.9 r9|0.07667920747010104||Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.104 Safari/537.36 Core/1.53.4882.400 QQBrowser/9.7.13059.400|38627225991274809163|1282704282_0_1524211501931|0:0:0:0:0:0:0:1
    log4j:ERROR Cannot Append to Appender! Appender either closed or not setup correctly!

    4.进行预处理过的数据需要让flume收集日志

    log4j可以和flume直接对接:官网资料

    这种方式虽然快但有一个缺点就是数据可能会丢,所以需要布置两条线来收集日志:一条直接对接到flume中,一条落地到日志中

    (1)导包:资源

    (2)配置log4j


    高可用配置log4j对接flume


    文件中的日志:


    资源:demologDemox

    3.配置flume,将log4j传来的数据存放到hdfs中

    flume配置信息


    #命名Agent a1的组件  
    a1.sources  =  r1
    a1.sinks  =  k1
    a1.channels  =  c1
    
    #描述/配置Source  
    a1.sources.r1.type  =  avro
    a1.sources.r1.bind  =  0.0.0.0
    a1.sources.r1.port  =  44444
    
    a1.sources.r1.interceptors = i1
    a1.sources.r1.interceptors.i1.type = regex_extractor
    a1.sources.r1.interceptors.i1.regex = ^(?:[^\\|]*\\|){14}\\d+_\\d+_(\\d+)\\|[^\\|]*$
    a1.sources.r1.interceptors.i1.serializers = s1
    a1.sources.r1.interceptors.i1.serializers.s1.name = timestamp
    #描述Sink  
    a1.sinks.k1.type  = hdfs
    a1.sinks.k1.hdfs.path = hdfs://hadoop01:9000/flux/reportTime=%Y-%m-%d
    a1.sinks.k1.hdfs.fileType = DataStream
    a1.sinks.k1.hdfs.rollInterval=30
    a1.sinks.k1.hdfs.rollSize=0
    a1.sinks.k1.hdfs.rollCount=0
    #描述内存Channel  
    a1.channels.c1.type  =  memory
    a1.channels.c1.capacity  =  1000
    a1.channels.c1.transactionCapacity  =  100
    
    #为Channle绑定Source和Sink  
    a1.sources.r1.channels  =  c1
    a1.sinks.k1.channel  =  c1

    访问a.jsp后查看hdfs中收集到了数据


    4.通过hive进行处理

    1.准备测试数据

    ie浏览器:连续访问3次a.jsp,访问2次b.jsp,再访问1次a.jsp

    关闭ie,再打开:访问2次a.jsp

    换谷歌浏览器:访问2次a.jsp,访问1次b.jsp

    人为可以计算出:pv-----11

                               uv-----2

                               vv-----3

    2.检查埋点收集的数据是否正确

    uv验证正确为2,pv验证正确为11


    验证vv正确为3,并且每个会话的计数都是递增的


    3.将hdfs中的数据存入到hive的表中

    #建立数据库
    create database flux;

    #建立外部表
    create EXTERNAL table flux (url String,urlname String,title String,chset String,scr String,col String,lg String,je String,ec String,fv String,cn String,ref String,uagent String,stat_uv String,stat_ss String,cip String) partitioned by (reportTime string) row format delimited fields terminated by '|' stored as textfile ;

    #增加分区

    ALTER TABLE flux add  PARTITION (reportTime='2018-04-20') location '/flux/reportTime=2018-04-20';



    4.数据已经到位,接下来进行业务计算

    ====================================================================================
    time
    	'2018-04-20'
    	
    pv  访问的次数  日志的条数就是访问的次数 由他这个数据单方面的来的体现一个网站的访问受欢迎程度显然是不合理的
    	select count(*) as pv from  flux where reportTime='2018-04-20'
    	
    uv  stat_uv(里面的值是uv_id的值,每一个用户都有一个自己独立的uv_id是20位的随即数   如果一个客户在30分钟内没有点击或者这个网页认为和关闭浏览器的效果是一样的,认为是1次会话的结束)
    	select count(distinct stat_uv) as uv from flux where reportTime='2017-01-10'
    	
    vv	会话的次数,一个ss――id就是一个会话的次数  ss_id_浏览次数_会话当前的时间
    根据session利用会话级别的session,这个session――id信息是一个10位的随机数,保存在浏览器的内存中,当浏览器关闭的时候,session――id消失。一个用户可以贡献多个session――id
    	select count(distinct split(stat_ss,'_')[0]) as vv from flux where reportTime='2017-01-10';
    	
    br  跳出率   反应一个网站是否受欢迎的程度,如果一个用户进入该网站了,但是没有点击任何其他的页面说明该用户对该网站的内容不感兴趣。关闭了该网页称为一次跳出。跳出的次数/会话的次数
    按照会话分组,分组后的urlname去重后只有一个的作为a/会话ss_id去重
    	select round(a / b,2) from (select count(*) as a from (select count(distinct urlname) as pcount from flux where reportTime='2017-01-10' group by split(stat_ss,'_')[0]) as tbr11  where pcount == 1) as tbr1 left join (select count(distinct split(stat_ss,'_')[0]) as b from flux) as tbr2;
    我的hql语句
    select round(a/b,2) from
    (select count(*) as a from
    (select count(distinct urlname) as p from flux where reportTime='2017-01-10' group by split(stat_ss,'_')[0])as tab11 where p=1)(如果这个表不给一个别名的话,p就不知道从哪里来的) as tab1 
    left join
    (select count(distinct split(stat_ss,'_')[0]) as b from flux where reportTime='2017-01-10' )as tab2;
    
    newip 新增ip,就是在以前的cip记录中不存在的ip
    	select count(distinct cip) as newip from flux where cip not in (select distinct cip as history from (select * from flux) as tnewip where reportTime<>'2017-01-10') and reportTime='2017-01-10';
    
    我的hql:新的cip就是在此之前的去重cip中没有的新的cip的数量
    select count(distinct cip) as newip from flux where cip not in( select distinct cip as history from (select * from flux) as tab1 where reportTime<>'2017-01-10') and reportTime='2017-01-10';(绿色部分必须这样写,不能写成flux,因为这样很多的flux表都不知道是哪个了)
    
    
    avgtime:平均访问时长,同一个会话中找到会话时间最大的值,减去会话时长最小的值,然后求平均
    	select round(avg(time),2) as avgtime from(select round((max(split(stat_ss,'_')[2]) - min(split(stat_ss,'_')[2]))/1000,0) time from flux group by split(stat_ss,'_')[0]) as tavgtime
    
    	select round(avg(time),2) as avgtime from (select round(max(split(stat_ss,'_')[2])-min(split(stat_ss,'_')[2])/1000,0)  as time from flux group by split(stat_ss,'_')[0] ) as tab1;
    
    newcust:新独立访客
    	select count(distinct stat_uv) as newcust from flux where stat_uv not in (select distinct stat_uv as history from (select * from flux) as tnewcust where reportTime<>'2017-01-10') and reportTime='2017-01-10';
    
    select count(distinct stat_uv) as newcust from flux where stat_uv not in(
    select distinct stat_uv as history from(select * from flux) as tab1 where reportTime<>'2017-01-10')and reportTime='2017-01-10';
    
    
    viewdeep :平均访问深度 客户去重以后的平均访问页面的数量
    	select round(avg(deep),2) as viewdeep from (select count(distinct urlname) as deep from flux where reportTime='2017-01-10' group by split(stat_uv,'_')[0]) as tviewdeep;
    
    
    select round(avg(deep),2) as viewdeep from (select count(distinct urlname) as deep from flux where reportTime='2017-01-10' group by split(stat_ss,'_')[0])as tab1;
    ====================================================================================

    pv计算:


    展开全文
  • 在上一篇《网站数据统计分析之一:日志收集原理及其实现》中,咱们详细的介绍了整个日志采集的原理与流程。但是不是这样在真实的业务环境中就万事大吉了呢?事实往往并非如此。比如针对前端采集日志,业务的同学经常...

    在上一篇《网站数据统计分析之一:日志收集原理及其实现》中,咱们详细的介绍了整个日志采集的原理与流程。但是不是这样在真实的业务环境中就万事大吉了呢?事实往往并非如此。比如针对前端采集日志,业务的同学经常会有疑问:你们的数据怎么和后端日志对不上呢?后端比你们多了 N%!技术的同学也会问:你们怎么不打后端记日志呢?后端比你们效率和准确性更高。带着这些疑问今天咱们就来聊聊前端日志采集中的这些是是非非。

    1、前端 VS 后端到底哪个准?该用谁?

    这应该算是统计分析同学最为关注的问题之一了,到底哪个准我们应该从技术和业务两个角度来看待这个问题。

    1.1 从技术架构层面日志分类

    日志采集从技术架构层面而言就两种,前端与后端。前端日志采集说白了也就是页面部署统计代码,通过

     <img src='/log_xxx.gif?k=v'> 或者 javascript 发送 ajax 请求的方式来发送日志请求。后端一般在 webCGI 中通过日志 API 接口输出日志(比如 java 中 log4j),或者直接 webServer 中打印日志(比如 Tomcat)。那这两种技术方案各有何优劣呢?

    1.1.1 前端 JS 采集

    优势:轻量,调试友好,可扩展性维护性好

    劣势:数据不安全,易丢失,客户端环境复杂兼容成本高

    1.1.2 后端服务采集

    优势:数据完整性有保证,业务数据安全

    劣势:对后端业务代码有一定侵入性,容易受爬虫影响,非后台交互行为日志采集不到

    通过上面比较我们可以看到前后端采集方案各有优劣,仅从数据量角度而言,后端日志采集方案能保证日志更为完整准确。

    1.2 从业务架构层面日志分类

    从业务架构出发,日志主要分为三大类:

    • 行为日志:浏览、点击、各种交互行为等

    行为日志一般侧重于用户各种行为交互、用户属性采集,用来评估用户体验、运营效果或者最后数据挖掘。比如漏斗、留存分析。

    • 业务日志:用户、帖子、订单、库存等

    业务日志往往和后端数据库、应用服务强关联,并且往往对日志有特别高的安全、性能、稳定、准确性要求,比如计费、支付等。

    • 系统日志

    这类日志一般用来衡量监控系统健康状况,比如磁盘、带宽是否满了,机器负载是否很高,或者RD自己通过程序输出的应用日志,用来监控应用服务是否异常,比如接口是否有超时,是否有恶意访问等。

    1.3 前后端差异的原因

    1.3.1 记录日志时机不同

    对于行为日志而言,前端 js 采集脚本为了不影响主体业务逻辑以及取得相应业务参数,一般放在页面底部。假设咱们某个页面200个请求,后端日志会在某个请求返回给客户端之前就记录日志,而前端日志此时就比较吃亏了,需要等到浏览器执行完200个请求到页面底部 js 时,才能发出请求,这当中的时间差是日志差距的主要原因之一。那么问题就来了,如果一个页面用户打开后没加载执行完(因为前置js错误、性能延时、主动关闭等),应该算一个 pv/uv 吗?这种场景下,一般是认为不应该算的,很显然后端“抢跑”了,而且会比前端多不少。这个差异和你的页面复杂程度,用户网络质量密切相关,就实测数据来看,页面顶部到页面底部会有 10%左右差异,前端与后端会有20%以上差异。

    1.3.2 爬虫影响

    这个和公司的业务密切相关,一般都会有竞品或者其它商业、科研目的的爬虫抓取网站信息,低级的爬虫不会触发 js 请求,但会记录服务器日志,高级的爬虫封装了浏览器内核的才会执行 js 代码,这也是前后端日志差异的重要原因之一。

    1.3.3 网络质量的原因

    在移动端前端 js 请求丢失率更高,因为网络状况非常复杂,2G、3G、4G、WiFi 等等,请求从客户端发出来,由于不稳定的网络条件,不一定能到前端JS日志服务器。

    1.3.4 平台差异

    M端部分浏览器默认是单标签的浏览方式,任何一个点击、上一页、下一页按钮都会导致下一个页面会覆盖上一个页面,进而导致后端有日志但是前端无法记录到用户日志的情况。

    另一种情况是可能部分老的移动端浏览器甚至都不支持 js,这就完全丢失了这部分日志。

    1.3.5 其它差异

    缓存、以及其它的用户行为也可能导致请求执行到了,但是没有发送成功,比如用户在页面加载完成后,请求还未发送完成时关掉页面,可能导致请求被 cancel 掉,这对一些用户黏性不是很强,跳出率很高的网站而言是另一个差异来源。

    1.4 究竟该用谁? 

    综上所述,到底哪个准其实是相对而言的,得分业务场景来看,不可能有一个绝对值,至于用哪个就看你具体的业务诉求了。

    • 前端 JS 日志只适合用来做全流量分析与统计,更多的是用来反应整体的流量趋势与用户行为,并不能精确到单个的用户行为与单次的访问轨迹。它的优势在于与后端解耦,调试、扩展、采集方便,额外的开发成本很低,适合做成 SAAS 模式。这也是为什么百度统计、GA它们采用这套方案能做成一款行业通用,甚至全球化的数据产品。

    • 如果对日志有特别高要求的业务场景比如计费、支付等等,要求日志一条不丢同时日志安全稳定,那就必须依赖数据库或者后端日志,但相应的开发维护成本会大些。

    2、GA、百度统计、自己的日志,到底哪个准?

    记得几年前流行过一句经典语录:幸福是个比较级,要有东西垫底才感觉的到。言外之意也就是说凡事经过比较后,才能区分出优劣。回到咱们的话题,早期创业公司一般会选择第三方统计系统,一来成本低,二来投资人往往需要看第三方数据对你公司的业务运营状态作出评估或者估值。但第三方统计只能做通用统计,对个性化的统计与深度数据挖掘无能为力,而且企业的商业机密堪忧。因此业务做大后公司往往会选择自己搭建数据平台和日志采集系统。那么问题马上接踵而至:自己的日志怎么和 GA、百度统计对不上呢?甚至这几者中任意两个都存在一定差距。

    其实原因大抵都是1.3中提到的原因,除此之外还有比较细节的技术实现差异,比如请求大小,域名是否被屏蔽(比如去年 5月开始 GA 就被墙了)、第三方 Cookie、埋码是否完全、统计口径与规则等等。

    当你发现其中存在差异时,需要验证各种可能原因去校验数据,如无特殊原因,最终应该以自己采集的为准。

    3、数据丢了吗?丢多少?

    在 1.3.1 中提到了一些内部线上测试数据,外界也有一些同学做过类似的测试,结论都差不多,部分业务场景下丢失率高到不可思议。比如点击前发送日志然后立即跳转,如果不做任何优化处理,这种场景的丢失率巨高,往往超过 50%。

    4、前端日志采集丢失问题能解决吗?

    4.1 传统解决方案

    从技术角度可以归纳为两点:

    • 用户关闭页面过早,统计脚本还未加载/初始化完成

    • 用户关闭或者跳出页面的时候,请求未发出

    针对第一点,概率较小,一般的处理方式就是,不要把统计脚本参合到其他脚本中,单独加载,并且放在前头,让它优先加载。很多公司的做法是,不让开发者关心统计脚本的加载,用户请求页面的时候,Nginx 会在 Body 开始标签位置注入一段脚本。或者把 js 动态请求换成 <img src> 硬编码的方式发送请求。

    对于问题二,处理方案就有很多了。

    4.1.1 阻塞式的 Ajax 请求

    还记得 XMLHttpRequest::open 方法的第三个参数吧,如果设置为 false 就是同步加载,

    window.addEventListener('unload', function(event) {
      var xhr = new XMLHttpRequest(),
      xhr.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");
      xhr.open('post', '/log', false); // 同步请求
      xhr.send(data);
    });

    阻塞页面关闭,当然可以在 readState 为 2 的时候就 abort 请求,因为我们不关心响应的内容,只要请求发出去就行了。

    4.1.2 暴力的死循环

    原理跟上面类似,只不过是使用一个空的死循环阻塞页面关闭,

    window.addEventListener('unload', function(event) {
      send(data);
      var now = +new Date;
      while(new Date - now >= 10) {} // 阻塞 10ms
    });

    4.1.3 发一个图片请求阻塞

    大部分浏览器都会等待图片的加载,趁这个机会把统计数据发送出去

    window.addEventListener('unload', function(event) {
      send(data);
      (new Image).src = 'http://example.com/s.gif';
    });

    以上提到的几个方案都是一个原理,让浏览器继续保持阻塞状态,等数据发送出去后再跳转,这里存在的问题是:

    • 少量浏览器下可能不奏效

    • 等待一会儿再跳转,用户体验上打了折扣,尤其是移动端上

    是否有更好的方案解决这个问题呢,前端同学秉着「小强精神」也提出了两个可实践的方案。

    4.2 优化方案

    不就是埋点统计数据嘛,非得在当前页面发送出去?优化方案的思路具有一定的跳跃性,我们考虑将数据在下跳页中发送,那么问题就转换为,如何将数据传递给下跳页?

    对于链接点击量的统计,我们可以将链接信息通过 url 传递给下跳页,传递思路如下:

    4.2.1 url 传参

    通过数组标识一个链接的位置信息,如 [站点id,页面id,模块id,链接index],通过四个参数可以惟一标识链接位置属性,使用 URL param 参数将数组数据传递给下跳页,等待由下跳页将数据发送出去。

    这里存在的问题是,下跳页中必须部署同样的统计脚本,但对一个系统来说,这是很容易做到的。我们也不会在自己的网页上放其他网站的链接吧,所以整个数据的统计都在一个闭环内。还有就是如果统计业务复杂的情况下,这种方案的维护成本与用户体验很差。

    4.2.2 通过 window.name 传递数据

    window.name 是浏览器给我们开放的一个接口,设置该属性的值后,即便页面发生了跳转,这个值依然不会变化,并且可以跨域使用。

    这里存在的问题是,该属性可能被开发者用于其他途径。我们可以限制开发者直接使用 window.name,封装接口,通过接口调用,如 aralejs 提供的nameStorage

    nameStorage.setItem(key, value);
    nameStorage.getItem(key);
    nameStorage.removeItem(key);

    储存形式为:

        scheme                  nameStorage datas
          |                            |
    ------------           ------------------------
    nameStorage:origin-name?key1=value1&key2=value2
                -----------
                     |
             window origin name

    以上虽然基本解决了数据丢失和体验差的问题,但是这也很大程度依赖于开发者的编程习惯,如不能随便玩耍 window.name ,业务复杂的场景下容易出问题,比如容易被覆盖;也对系统有一定的要求,必须在所有页面上部署同样的埋点脚本。

    4.2.3 localstorage 存储重发

    localstorage 是 HTML5 提供的两种在客户端存储数据的新方法之一,对于丢失率高的场景,咱们可以先把请求日志存储在 localstorage 中,失败后在下个页面重发,并且可以添加重试机制,这样日志的完整性能很大程度上提高。从性能角度讲还可以统一发送,减少连接。

    但是针对跳出率高的场景,这种方式实测效果并不明显。

    4.3 这件事情应该交给浏览器来解决

    上面提到的各种方案,不乏黑科技,然而存在的问题还是一大堆,如果团队的开发者执行力不够,中途容易出现各种麻烦。所以真正能够解决这个问题的,必然还是浏览器本身!

    为什么不能给用户提供这样一个 API,即使页面跳转了,也能够将上个页面的请求发出去呢?庆幸的是,W3C 工作组也想到了这个问题,提出了 Beacon API 的草案

    Beacon API 允许开发者发送少量错误分析和上报的信息,它的特点很明显:

    • 在空闲的时候异步发送统计,不影响页面诸如 JS、CSS Animation 等执行

    • 即使页面在 unload 状态下,也会异步发送统计,不影响页面过渡/跳转到下跳页

    • 能够被客户端优化发送,尤其在 Mobile 环境下,可以将 Beacon 请求合并到其他请求上,一同处理

    sendBeacon 函数挂在在 navigator 上,在 unload 之前,这个函数一定是被初始化了的。其使用方式为:

    window.addEventListener('unload', function(event) {
      navigator.sendBeacon('/collector', data);
    });

    navigator.sendBeacon(url, data);,第一个参数为数据上报的地址,第二个参数为要发送的数据,支持的数据格式有:ArrayBufferView, Blob, DOMString, 和 FormData。

    Beacon 的还有一个非常实用的移动端使用场景,当用户从浏览器切换到其他 app 界面或者 Home 屏的时候,部分浏览器默认会停止页面脚本的执行,如果在这个时候使用了 unload 事件,可能会让你失望,因为 unload 事件并不会触发,此时,Beacon 就派上用途了,它是不会受影响的。

    本节是对页面打点丢失问题的简单探讨,枚举了我们通常会用到的一些解决方案,可能不是很完善,如果你有更好的建议,可以提出来。

    很多问题,我们绞尽脑汁,可能很少会考虑,这个问题是不是应该由我们来解决,或者说这个问题交给谁处理是最恰当的。本文的探讨可以看到,浏览器本身才是最好的问题解决方,当网站流量变大之后,上面提到的丢失问题就更加明显,这也迫使浏览器本身做了改善,自然也在情理之中。

    5、总结

    至此,本文探讨了前端日志采集过程中的一些常见问题,解释了数据分析以及RD同学一些常见困惑,并提供了一些相应的优化思路与方案。文中提到的各种问题还是以 PV 为主,其实还存在另一个常见的指标差异:UV,这个指标的差异原因更为复杂,改天有空再详细分享下。总之日志采集与统计分析没有部分同学想象的那么简单,这里面的坑其实很多,需要大家不断的去探索,从技术和业务角度去不断优化改进,前路漫漫。

    说明:本文第四节结合了 Refer [1] 以及本文作者的实际经验整理而成,在此致谢,感谢分享。

    6、Refer:

    [1] 页面跳转时,统计数据丢失问题探讨

    http://www.barretlee.com/blog/2016/02/20/navigator-beacon-api/

    [2] 网站数据统计分析之一:日志收集原理及其实现

    http://my.oschina.net/leejun2005/blog/292709

    [3] 站长统计、百度统计、腾讯统计、Google Analytics 哪一统计的数据相对准确些?

    https://www.zhihu.com/question/19955915

    [4] 网站分析——我们的数据准确吗?

    http://bit.ly/1RZnvWi

    [5] 为什么两个监测工具报告中的数据不同

    http://bit.ly/1QebUBe

    [6] JavaScript API 调用说明

    http://help.dplus.cnzz.com/?p=62#dplus.track

    [7] 数据采集与埋点

    http://www.sensorsdata.cn/blog/shu-ju-jie-ru-yu-mai-dian/

    [8] Beforeunload打点丢失原因分析及解决方案:

    http://blogread.cn/it/article/6804?f=wb

    [9] beforeunload丢失率统计:

    http://ued.taobao.org/blog/2012/11/beforeunload%E4%B8%A2%E5%A4%B1%E7%8E%87%E7%BB%9F%E8%AE%A1/

    原文链接:http://my.oschina.net/leejun2005/blog/620713

    展开全文
  • 网站访问统计分析常用术语

    千次阅读 2011-12-08 08:55:38
    流量分析软件都可以按时间,比如每天或每星期,显示出访问数。很多软件还可以以图形方式显示,就更加直观。 在进行了某项特定营销活动后,检验效果如何的第一个指标当然就是看所带来的访问数。比如网站的文章被社会...
  • 统计分析工具

    千次阅读 2015-05-26 16:34:58
    网站统计分析: http://piwik.org/  知名网站统计分析工具,开源免费。 移动统计分析: https://count.ly/ 开源,移动统计分析首选。社区版免费开源;企业版收费、开源,可私有部署(部署在自己的服务器上)...
  • 网站访问统计分析工具之罗列比较

    千次阅读 2010-03-12 15:28:00
    本人最近对统计网站访问系统考察了一番,便寻访国内外的软件,也对其进行了比较,想来也是对大家有用的,写下来以供参考。 第一、不管做什么,我们首先需要搞清楚概念,是不是?下来呢,我先说下名词解释 网站流量...
  • 常用统计分析软件介绍

    万次阅读 2018-03-08 08:45:12
    SAS 是英文Statistical Analysis System的缩写,翻译成汉语是统计分析系统,最初由美国北卡罗来纳州立大学两名研究生开始研制,1976 年创立SAS公司, 2003年全球员工总数近万人,统计软件采用按年租用制,年租金收入...
  • Linux下安装awstats日志统计分析

    千次阅读 2014-02-27 00:52:53
    实际测试62M日志(30万条记录)如开启dnslookup分析时间在2小时以上,而关闭dnslookup的话分析时间在1分钟,极大缩短了分析时间。关掉dnslookup的损失就是无法获得参观者的国家信息,awstats官方建议如需要国家信息...
  • 16种常用统计分析软件介绍

    万次阅读 2019-05-13 18:41:19
    SAS 是英文Statistical Analysis System的缩写,翻译成汉语是统计分析系统,最初由美国北卡罗来纳州立大学两名研究生开始研制,1976 年创立SAS公司, 2003年全球员工总数近万人,统计软件采用按年租用制,年租金收入...
  • nginx日志统计分析自动报表工具goaccess(推荐) 官网: https://goaccess.io/download 源码 https://github.com/opensourceteams/linux 功能描述 nginx日志统计分析自动报表工具goaccess(推荐) 网站总访问量统计,...
  • Web服务器日志统计分析完全解决方案from: http://kunming.cyberpolice.cn/aqjs/20004.html 摘要对于所有的ICP来说,除了保证网站稳定正常运行以外,一个重要的问题就是网站访问量的统计和分
  • Page View,页面访问量,指页面浏览的次数,用以衡量网站用户访问的网页数量。也就是曝光量。一般来说,PV与来访者的数量成正比,但是PV并不直接决定页面的真实来访者数量,如同一个来访者通过不断的刷新页面,也...
  • 什么是网站统计代码

    千次阅读 2019-08-05 22:04:40
    网站统计:是指通过专业的网站统计分析系统(或软件),对网站访问信息的记录并归类,以及在此基础上的统计分析,如网站访问量的增长趋势图、用户访问最高的时段、访问最多的网页、停留时间、用户使用的搜索引擎,...
  • 网站数据统计分析工具是网站站长和运营人员经常使用的一种工具,比较常用的有谷歌分析、百度统计和腾讯分析等等。...简单来说,网站统计分析工具需要收集到用户浏览目标网站的行为(如打开某网页、点
  • Piwik现已改名为Matomo,这是一套国外著名的开源网站统计系统,类似于百度统计、Google Analytics等系统。最大的区别就是可以看到其中的源码,这正合我意。因为我一直对统计的系统很好奇,很想知道里面的运行原理是...
  • 国外数据平台统计分析sdk

    千次阅读 2020-02-04 22:01:04
    1.Appsee 2.Mixpanel 3.Google Mobile Analytics 4.Countly 5.Flurry 6.Localytics 7.Yandex.Metrica 8.Distimo 9.AppFigures ...国外的统计有: Flurry(https://developer.yahoo.com) ...
  • 统计分析软件介绍

    千次阅读 2010-04-05 22:59:00
    PSPP is a program for statistical analysis of sampled data. It is a Free replacement for the proprietary program SPSS, and appears very similar to it with a few ... - 同样建基于S语言的统计分析软件
  • 数据仓库、OLAP和 数据挖掘、统计分析的关系和区别分析 一、什么是数据挖掘  数据挖掘(Data Mining),又称为数据库中的知识发现(Knowledge Discovery in Database, KDD),就是从大量数据中获取有效的、新颖的、...
  • 世界三大统计分析软件sas splus spss

    千次阅读 2009-06-11 21:23:00
    尤其是它的创业产品—统计分析系统部分,由于具有强大的数据分析能力,一直是业界中比较著名的应用软件,在数据处理方法和统计分析领域,被誉为国际上的标准软件和最具权威的优秀统计软件包,SAS系统中提供的主要...
  • 如何正确的使用百度统计分析数据

    千次阅读 2013-12-23 14:43:02
    如何更好的使用百度统计的功能?小蚂蚁站长吧​结合网络上的信息和自己的经验总接了一下观点分享给大家。百度统计提供几十种图形... 目前百度统计提供的功能包括:趋势分析、来源分析、页面分析、访客分析和一些搜索引
  • 而与此同时,在构建网站建设中各个单位都会遇到各种各样的问题,那么对web服务器的运行和访问情况进行详细和周全的分析对于了解网站运行情况,发现网站存在的不足,促进网站的更好发展重要性是不言而喻的。...
  • 刚发现一日志统计工具,能分析Apache/IIS等,使用图标输出,统计得非常强,下面是例子:http://ns3744.ovh.net/awstats/awstats.pl?config=destailleur.fr 介绍:AWStats: Advanced Web StatisticsAWStats是在...
  • 网站统计中的数据收集原理及实现网站统计 埋点 Web Openresty网站数据统计分析工具是网站站长和运营人员经常使用的一种工具,比较常用的有谷歌分析、百度统计和腾讯分析等等。所有这些统计分析工具的第一步都是...
  • 网站统计中的数据收集原理及实现

    千次阅读 2015-08-07 17:25:06
    网站数据统计分析工具是网站站长和运营人员经常使用的一种工具,比较常用的有谷歌分析、百度统计和腾讯分析等等。所有这些统计分析工具的第... 简单来说,网站统计分析工具需要收集到用户浏览目标网站的行为(如打开某

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 76,175
精华内容 30,470
关键字:

网站统计分析html