虽然网络上有比较多的SEO日志分析工具,比如爱站,光年,但那都是固定维度的,不如自己写的灵活,想怎么拆分就怎么拆分,加上最近在学习[《利用python进行数据分析》][pandas-book]这本书,正好可以用来练习练习,顺便熟悉一下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日志分析结果][result]
这里说一下页面停留时间计算原理,因为日志的记录里只记录了进入页面的时间,并没有记录停留时间,所以只能间接地算出来。比如:A IP 访问了1和2两个页面,那么1页面的停留时间=2页面的进入时间-1页面的进入时间。由于A IP 没有访问第3个页面,所以算不到第2个页面的停留时间,我这里人为的当成停留了1秒。
总停留时间的计算思路是按 IP 和目录分组,得到每个目录的时间列表,然后列表里的后一个数减去前一个数得出时间差,过滤时间差>1799的数,然后累加,算出每个目录的总停留时间。而访问数量就是>1799的数量+1。
另外,脚本算出来的总停留时间和光年日志分析工具的会不一样,不过那并不碍事,本来总停留时间就是一个参考,同一个标准算出来的数据对最后的结论影响不大。
最后,如果是用 shell 进行SEO日志分析的话,可与阅读这篇文章:
[使用shell分析日志][shell]——老狼
[shell]:http://www.itseo.net/direction/show-145.html
[result]:http://upload-images.jianshu.io/upload_images/1464422-375da4f3a91693d7.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240
[pandas-book]:https://book.douban.com/subject/25779298/ "《利用python进行数据分析》"
[pandas]:http://pandas.pydata.org/pandas-docs/version/0.18.0/ "Pandas-0.18.0-documentation"