http://www.jianshu.com/p/84d4f9c6e83f

虽然网络上有比较多的SEO日志分析工具,比如爱站,光年,但那都是固定维度的,不如自己写的灵活,想怎么拆分就怎么拆分,加上最近在学习《利用python进行数据分析》这本书,正好可以用来练习练习,顺便熟悉一下pandas库。不得不说,pandas这个库真的强大的不要不要的,对数据的加载、存储、清理、转换、合并样样兼顾,虽然目前只看到第9章,但是感觉对数据的处理能力有了很大的进步!
对于SEO日志分析,比较常用拆分维度是:

  • 统计总抓取量
  • 统计不重复抓取量
  • 统计状态码数量
  • 统计目录的抓取量
  • Top N抓取量

此外,我参考了光年日志分析工具,也加入了停留时间和访问时间,尽量把维度拆分的细一点多一点(练手嘛),但是,如果按我想的那样,拆分的很细的话,那肯定要写很多行代码(事实上,没用pandas写之前,就用了400多行代码实现过一个demo),所以取巧了一下,拆分成需要的数据之后,用 pivot_table 的方法建立透视表。
日志的格式如下:
220.181.108.81 - - [05/Mar/2016:20:41:23 +0800] "GET /images/im_qq.gif.pagespeed.ce.8GQAmIPgsj.gif HTTP/1.1" 200 4581 "http://\w+.\w+.com/\d+.html" "Mozilla/5.0 (Windows NT 6.2; Trident/7.0; MANM; ED; rv:11.0) like Gecko"
日志分析脚本demo如下:

# -*- coding: utf-8 -*-
'''爬虫日志分析demo1.0'''

from time import strptime,mktime,time
from pandas import Series,DataFrame
import pandas as pd
import sys,socket
reload(sys) 
sys.setdefaultencoding('utf-8')
start = time()
format = "%d/%b/%Y:%H:%M:%S"  #写出日志的日期格式

def file_open(file):
    col_names=['ip','datetime','resource','status','bytes','refer','ua','path','level1','data']
    reader = pd.read_csv(file,sep='\s+',header=None,chunksize=1000000,names=range(10))
    data = DataFrame(columns=col_names)
    for piece in reader:
        data[['ip','status','bytes','refer','ua']] = piece[[0,6,7,8,9]]
        data['refer'] = data['refer'].fillna('abc')
        data['datetime'] = piece[3].str.replace('[','')
        data['resource'] = piece[5].str.split(' ').str[1]
        data['data'] = piece[3].str.match('\[(\S+):\d+:\d+:\d+').str[0]
        data['path'] = piece[5].str.match('\w+\ ((?:/[A-Za-z0-9-_/\.]*/)|(?:/)\W)').str[0].str.strip()
        data['level1'] = piece[5].str.match('\w+\ ((?:/[\w\d_-]*/)|(?:/) )').str[0].str.strip()
    return data

'''判断爬虫真假'''
def check_spider(df):
    start = time()
    ip = list(df['ip'].unique())
    print u'正在检查spider真伪,共%s个待查询iP,耗时较长,请稍等' % len(ip)
    true_spider = []
    fake_spider = []

    for x,y in enumerate(ip):
        try:
            result = socket.gethostbyaddr(y)[2][0]
            true_spider.append(result)
            print 'NO.%s %s is spider and now appending in true spider list' % (x,y)
        except socket.herror, e:
            print 'NO.%s %s is facke spider' % (x,y)
            fake_spider.append(y)

    log = df[df['ip'].isin(true_spider)]
    print u'执行完成,共耗时%s秒' % (time()-start)
    print 'True Spider IP %s,False Spider IP %s' % (len(true_spider),len(fake_spider))
    return log

'''提取某个爬虫访问记录'''
def get_spider_log(df,spider):
    spiders = df['ua'].str.contains(spider)
    spider_log = df[spiders]
    spider_log['datetime'] = spider_log['datetime'].apply(lambda x:int(mktime(strptime(x,format))))
    return spider_log

'''统计停留时间和访问次数'''
def get_x(x):
    a = []
    a.append(x)
    return a

def get_list(list_name):
    '''后一个减去前一个,计算时间差'''
    t_list = list(list_name) #传入的是一个Serice对象,所以要转化成list
    t_list.sort()
    values = []
    weight = len(t_list)

    for value in xrange(weight):
        if weight == 1:  #只有一个时间的情况,停留时间算成1秒
            values.append(1)
        elif value > 0:  #前面是升序,所以要 >0 才能用后一个减去前一个
            values.append((t_list[value]-t_list[value-1]))

    ''' size 是大于1799的个数,因为间隔1800秒以上访问次数+1,大于1799是包含了1800'''
    size = len(filter(lambda x:x >1799,values))

    '''如果有一个大于1800秒,表示共访问了2次,有 N 个,表示访问了 N+1 次'''
    visits = size + 1  

    '''过滤大于1800的值,因为过滤前可能是[1811,2],有两个停留时间,过滤后变成[2]只剩一个时间,因此加上size修正'''
    _values = filter(lambda x:x <1800,values)  #访问时间差的列表
    '''存在过滤后为空列表的情况,但只要访问过,就有停留时间'''
    if _values == []:
        _values.append(1)

    stay_time = reduce(lambda x,y:x+y,_values) + size

    result = [visits,stay_time]

    return result

if __name__ == '__main__':

    script,file,output = sys.argv

    log = file_open(file)

    spider_log = get_spider_log(log,'Baiduspider')  #默认百度爬虫

    spider_log = check_spider(spider_log)  #检查爬虫真伪    

    drop_dup = spider_log.drop_duplicates(['data','resource']) #去除'resource'列中的重复项,算不重复抓取量用

    '''不重复抓取量'''
    No_rep = drop_dup.pivot_table(values=['resource'],index=['data','level1','path'],aggfunc='count')      
    No_rep.columns = ['No_rep']

    '''次级目录总抓取数,数数的时候会按层次化索引来数,所以数'ua'列也可以'''
    All_crawl = spider_log.pivot_table(values=['ua'],index=['data','level1','path'],aggfunc='count') 
    All_crawl.columns = ['All_crawl']

    '''次级目录总字节数'''
    All_bytes = spider_log.pivot_table(values=['bytes'],index=['data','level1','path'],aggfunc='sum') 
    All_bytes.columns=['All_bytes']

    '''平均抓取字节(总抓取字节/抓取量)'''
    Avg_bytes = pd.Series(All_bytes['All_bytes']/All_crawl['All_crawl'],index=All_crawl.index,name='avg_bytes')

    '''次级状态码数量'''
    All_status = spider_log.pivot_table('resource',index=['data','level1','path'],columns=['status'],fill_value=0,aggfunc='count')  

     '''算出停留时间和访问次数'''
    t = spider_log['datetime'].groupby([spider_log['data'],spider_log['ip'],spider_log['level1'],spider_log['path']]).apply(lambda x:get_x(x))

    t = t.apply(lambda x:get_list(x[0])).reset_index()

    t['visits'] = t[0].str.get(0)

    t['saty_time'] = t[0].str.get(1)

    '''停留时间和访问次数'''
    visits_stay = t.pivot_table(index=['data','level1','path'],aggfunc='sum') 

    ''' 平均停留时间(总停留时间/总抓取量)''
    Avg_saty = pd.Series(visits_stay['saty_time']/All_crawl['All_crawl'],index=All_crawl.index,name='Avg_saty')

    '''次级目录IP访问数量'''
    Ip_count = t.pivot_table('ip',index=['data','level1','path'],aggfunc='count')

    '''精确到次级目录'''
    last = No_rep.join([Ip_count,All_crawl,All_bytes,Avg_bytes,Avg_saty,visits_stay,All_status]).reset_index()

    '''精确到一级目录'''
    no_path = last.pivot_table(index=['data','level1'],aggfunc='sum')
    no_path['avg_bytes'] = pd.Series(no_path['All_bytes']/no_path['All_crawl'],index=no_path.index)
    no_path['Avg_saty'] = pd.Series(no_path['saty_time']/no_path['All_crawl'],index=no_path.index)

#    last.to_csv('ulast.csv',index=False)
    no_path.to_csv(output)     #默认输出

    over = time()

    print '***Analysis OK!Take %s seconds***' % (over-start)

导出的结果如下:


SEO日志分析结果


这里说一下页面停留时间计算原理,因为日志的记录里只记录了进入页面的时间,并没有记录停留时间,所以只能间接地算出来。比如:A IP 访问了1和2两个页面,那么1页面的停留时间=2页面的进入时间-1页面的进入时间。由于A IP 没有访问第3个页面,所以算不到第2个页面的停留时间,我这里人为的当成停留了1秒。

总停留时间的计算思路是按 IP 和目录分组,得到每个目录的时间列表,然后列表里的后一个数减去前一个数得出时间差,过滤时间差>1799的数,然后累加,算出每个目录的总停留时间。而访问数量就是>1799的数量+1。

另外,脚本算出来的总停留时间和光年日志分析工具的会不一样,不过那并不碍事,本来总停留时间就是一个参考,同一个标准算出来的数据对最后的结论影响不大。

最后,如果是用 shell 进行SEO日志分析的话,可与阅读这篇文章:
使用shell分析日志——老狼



作者:担得一身好酱油
链接:http://www.jianshu.com/p/84d4f9c6e83f
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。