去年一年,我写了将近100篇网络日志。
现在这一年结束了,我要统计"访问量排名",看看哪些文章最受欢迎。(隆重预告:本文结尾处将揭晓前5名。)
以往,我用的是AWStats日志分析软件。它可以生成很详细的报表,但是不太容易定制,得不到某些想要的信息。所以,我就决定自己写一个Bash脚本,统计服务器的日志,顺便温习一下脚本知识。
事实证明,这件事比我预想的难。虽然最终脚本只有20多行,但花了我整整一天,反复查看手册,确认用法和合适的参数。下面就是我的日志分析脚本,虽然它还不是通用的,但是我相信里面用到的命令,足以满足一般的日志分析需求,同时也是很好的学习Bash的实例。如果下面的每一个命令你都知道,我觉得可以堪称熟练使用Bash了。
一、操作环境
在介绍脚本之前,先讲一下我的服务器环境。
我的网络服务器软件是Apache,它会对每一个http请求留下记录,就像下面这一条:
203.218.148.99 - - [01/Feb/2011:00:02:09 +0800] "GET /blog/2009/11/an_autobiography_of_yang_xianyi.html HTTP/1.1" 200 84058 "http://www.ruanyifeng.com/blog/2009/11/freenomics.html" "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13"
它的意思是2011年2月1日,IP地址为203.218.148.99的访问者,向服务器请求访问网址/blog/2009/11/an_autobiography_of_yang_xianyi.html。
当天所有的访问记录,组成一个日志。过去一年,一共生成了365个日志文件。它们存放在12个目录中,每一个目录表示一个月(2011-01、2011-02、......2011-12),里面的日志文件依次为www-01.log、www-02.log、......www-31.log(假定该月有31天)。
在不压缩的情况下,365个日志文件加起来,要占掉10GB空间。我的目标就是分析这10GB日志,最后得到一个如下形式的访问量排名:
访问量 网址1
访问量 网址2
访问量 网址3
...... ......
二、为什么要用Bash
很多计算机语言,都可以用来完成这个任务。但是,如果只是简单的日志分析,我觉得Bash脚本是最合适的工具。
主要原因有两个:一是"开发快",Bash脚本是各种Linux命令的组合,只要知道这些命令怎么用,就可以写脚本,基本上不用学习新的语法,而且它不用编译,直接运行,可以边写边试,对开发非常友好。二是"功能强",Bash脚本的设计目的,就是为了处理输入和输出,尤其是单行的文本,所以非常合适处理日志文件,各种现成的参数加上管道机制,威力无穷。
前面已经说过,最终的脚本我只用了20多行,处理10GB的日志,20秒左右就得到了结果。考虑到排序的巨大计算量,这样的结果非常令人满意,充分证明了Bash的威力。
三、总体思路
我的总体处理思路是这样的:
第一步,处理单个日志。统计每一天各篇文章的访问量。
第二步,生成月度排名。将每一天的统计结果汇总,得到月度访问量。
第三步,生成年度排名。将12个月的统计结果汇总,进行年度访问量的排序。
四、处理单个日志
以2011年1月1日的日志为例,它在目录2011-01之中,文件名是www-01.log,里面有10万条如下格式的记录:
203.218.148.99 - - [01/Feb/2011:00:02:09 +0800] "GET /blog/2009/11/an_autobiography_of_yang_xianyi.html HTTP/1.1" 200 84058 "http://www.ruanyifeng.com/blog/2009/11/freenomics.html" "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13"
处理这个日志,我只用了一行代码:
awk '$9 == 200 {print $7}' www-01.log | grep -i '^/blog/2011/.*\.html$' | sort | uniq -c | sed 's/^ *//g' > www-01.log.result
它用管道连接了5个命令,每一个都很简单,我们依次来看:
(1) awk '$9 == 200 {print $7}' www-01.log
awk命令默认用空格,将每一行文本分割成若干个字段。仔细数一下,我们需要的只是第7个字段,即http请求的网址,{print $7}表示将第7个字段输出,结果就是:
/blog/2009/11/an_autobiography_of_yang_xianyi.html
考虑到我们只统计成功的请求,因此再加一个限制条件,服务器的状态代码必须是200(表示成功),写成"$9 == 200",即第9个字段必须是200,否则不输出第7个字段。
更精细的统计,还应该区分网络蜘蛛和真实访问者,由于我想不出简单的分辨方法,这里只好忽略了。
(2)grep -i '^/blog/2011/.*\.html$'
在输出的所有记录的第7个字段之中,并不是每一条记录都需要统计的。根据我的文章的命名特点,它们的网址应该都以"/blog/2011/"开头,以".html"结尾。所以,我用一个正则表达式"^/blog/2011/.*\.html$",找出这些记录。参数i表示不区分大小写。
(3)sort
这时,所有需要统计的记录应该都列出来了,但是它们的次序是杂乱的。接着,使用sort命令,不过目的不是为了排序,而是把相同的网址排列在一起,为后面使用uniq命令创造条件。
(4)uniq -c
uniq的作用是过滤重复的记录,只保留一行。c参数的作用,是在每行的开头添加该记录的出现次数。处理之后的输出应该是这样的:
32 /blog/2011/01/guidelines_for_english_translations_in_public_places.html
32 /blog/2011/01/api_for_google_s_url_shortener.html
30 /blog/2011/01/brief_history_of_arm.html
它表示以上三篇文章,在1月1日的日志中,分别有32条、32条、30条的访问记录(即访问次数)。
(5)sed 's/^ *//g' > www-01.log.result
上一步uniq命令添加的访问次数,是有前导空格的。也就是说,在上例的32、32、30之前有一连串空格,为了后续操作的方便,这里把前导空格删去。sed命令是一个处理行文本的编辑器,'s/^ *//g'是一个正则表达式(^和*之间有一个空格),表示将行首的连续空格替换为空(即删除)。接着,将排序结果重定向到文件www-01.result。单个日志分析就完成了。
五、月度汇总排名
经过上一步之后,1月份的31个日志文件,生成了31个对应的分析结果文件。为了汇总整个月的情况,必须把这31个结果文件合并。
(6)合并分析结果
for i in www-*.log.result
do
cat $i >> log.result
done
这是一个循环结构,把所有www-01.log.result形式的文件,都写进log.result文件。
然后,我用一行语句,计算月度排名。
sort -k2 log.result | uniq -f1 --all-repeated=separate |./log.awk |sort -rn > final.log.result
这行语句由3个命令和1个awk脚本组成:
(7)sort -k2 log.result
由于是31个文件汇总,log.result文件里面的记录是无序的,必须用sort命令,将相同网址的记录归类在一起。但是此时,访问次数是第一个字段,网址是第二个字段,因此参数k2表示根据第二个字段进行排序。
(8)uniq -f1 --all-repeated=separate
uniq的作用是过滤重复的记录,参数f1表示忽略第一个字段(访问次数),只考虑后面的字段(网址);参数表示all-repeated=separate,表示过滤掉所有只出现一次的记录,保留所有重复的记录,并且每一组之间用一个空行分隔。这一步完成以后,输出结果变成如下的形式:
617 /blog/2011/01/guidelines_for_english_translations_in_public_places.html
455 /blog/2011/01/guidelines_for_english_translations_in_public_places.html223 /blog/2011/01/2010_my_blogging_summary.html
253 /blog/2011/01/2010_my_blogging_summary.html
相同网址都归在一组,组间用空行分割。为了简洁,上面的例子每一组只包含两条记录,实际上每一组都包含31条记录(分别代表当月每天的访问次数)。
(9)log.awk脚本
为了将31天的访问次数加总,我动了很多脑筋。最后发现,唯一的方法就是用awk命令,而且必须另写一个awk脚本。
#!/usr/bin/awk -f
BEGIN {
RS="" #将多行记录的分隔符定为一个空行
}{
sum=0 #定义一个表示总和的变量,初值为0
for(i=1;i<=NF;i++){ #遍历所有字段
if((i%2)!=0){ #判断是否为奇数字段
sum += $i #如果是的话,累加这些字段的值
}
}
print sum,$2 #输出总和,后面跟上对应的网址
}
我已经对上面这个log.awk脚本加了详细注释。这里再说明几点:首先,默认情况下,awk将"\n"作为记录的分隔符,设置RS=""表示改为将空行作为分隔符,因此形成了一个多行记录;其次,NF是一个awk的内置变量,表示当前行的字段总数。由于输入文件之中,每一行都包含两个字段,第一个是访问数,第二个是网址,所以这里做一个条件判断,只要是奇数字段就累加,偶数字段则一律跳过。最后,每个记录输出一个累加值和网址,它们之间用空格分割。
(10)sort -rn > final.log.result
对awk脚本的处理结果进行排序,sort默认使用第一个字段,参数r表示逆序,从大往小排;参数n表示以数值形式排序,不以默认的字典形式排序,否则会出现10小于2的结果。排序结果重定向到final.log.result。至此,月度排名完成。
六、脚本文件
用一个脚本,包含上面两节所有的内容。
#!/bin/bash
if ls ./*.result &> /dev/null #判断当前目录中是否有后缀名为result的文件存在
then
rm *.result #如果有的话,删除这些文件
fitouch log.result #创建一个空文件
for i in www-*.log #遍历当前目录中所有log文件
do
echo $i ... #输出一行字,表示开始处理当前文件
awk '$9 == 200 {print $7}' $i|grep -i '^/blog/2011/.*\.html$'|sort|uniq -c|sed 's/^ *//g' > $i.result #生成当前日志的处理结果
cat $i.result >> log.result #将处理结果追加到log.result文件
echo $i.result finished #输出一行字,表示结束处理当前文件
doneecho final.log.result ... #输出一行字,表示最终统计开始
sort -k2 log.result | uniq -f1 --all-repeated=separate |./log.awk |sort -rn > final.log.result #生成最终的结果文件final.log.result
echo final.log.result finished #输出一行字,表示最终统计结束
这就是月度排名的最终脚本。编写的时候,我假定这个脚本和log.awk脚本与日志文件在同一个目录中,而且这两个脚本都具有执行权限。
年度排名的处理与此类似,就不再赘述了。
=================================================================
关于脚本介绍,就到此为止。
接下来,揭晓2011年度访问量最大的我的5篇文章。
第五名、《我的Google Adsense帐户被关》
我真想问问Google Adsense中国小组的成员:"难道你们都是机器人吗?难道你们看不出来哪些是流氓网站,哪些是正派网站吗?你们是否真的尽职工作了,还是在不负责任地草菅人命?"
第四名、《乔布斯的告别》
斯蒂夫・乔布斯活着的时候,对病情讳莫如深,外界对他的身体状态毫不知情。现在他去世了,根据各方面透露的信息,我们终于可以还原他的病历,了解像他这样伟人怎样对待生与死。
第三名、《Dan计划:重新定义人生的10000个小时》
在此之前,他几乎没有打过高尔夫球,甚至对这项运动都没有太大兴趣。他的计划是,辞职以后,每天练习6个小时,一周练习6天,坚持6年,总计超过10000个小时,然后成为职业选手。他把这称为"Dan计划"。
第二名、《保持简单----纪念丹尼斯•里奇(Dennis Ritchie)》
13岁的丹尼斯•里奇(Dennis Ritchie),就这样随着父亲一起来到新泽西。那时,谁也没有想到,这个文静的少年将在这里待上一辈子,并且创造出改变世界的发明。
第一名、《人生只有900个月》
你可以画一个30x30的表格,一张A4纸就够了。每过一个月,就在一个格子里打钩。你全部的人生就在这张纸上。你会因此有一个清晰的概念:你的人生是如何蹉跎的。
(完)
jlake 说:
分析日志,最需要掌握的是的是正则表达式。
配合一点脚本程序,基本上够了。
如果要把结果做得清楚明了,那还需要把分析结果做成图表,还需要一门功夫。
2012年1月 6日 13:59 | 档案 | 引用
张刚 说:
Bash提供了无限想象的空间,组合拳就是这么强大,好像我还是第一个留言的。 ;)
2012年1月 6日 14:06 | 档案 | 引用
betwinyou 说:
博主有点意思,偶尔来逛逛。
2012年1月 6日 14:17 | 档案 | 引用
James 说:
一是"运行快",Bash直接与内核交互,没有中间环节,外生的编程语言很难比它更快
不知道说什么好了。。。。。
2012年1月 6日 14:43 | 档案 | 引用
blindsniper 说:
看来还是非技术性的文章访问量高
2012年1月 6日 14:47 | 档案 | 引用
我也第一次留个言, 说:
我想不到博主样样精通,所以今天搜索了下。果然高材生
2012年1月 6日 15:20 | 档案 | 引用
黄立 说:
图表可以用xcelsius,我觉得可以先将所有文本合并然后用awk统计只要几行代码:
{
if ($9 == 200 & $7 ~ /^\/blog\/2011/){
sort[$7] += 1
}
for (i in sort)
print i,sort[i]
}
之后再用sort -k 1 输出即可
2012年1月 6日 15:25 | 档案 | 引用
明城 说:
这个提法的确在技术人员眼里看来很谬误,不过博主是非技术人员所以还是情有可原
有关 bash 以及 shell 的更多信息可以参看 http://en.wikipedia.org/wiki/Bash_(Unix_shell)
同时也希望博主能及早改成这个语误
2012年1月 6日 15:51 | 档案 | 引用
kei 说:
同感啊 前5的那几篇真不错
2012年1月 6日 15:53 | 档案 | 引用
阮一峰 说:
我的原意是,与其他解释性语言(perl、php、python、rugy)相比,Bash无论是运行速度还是开发速度,都算快的。
这样说,应该就对了吧。
我马上修改原文,谢谢各位指正。
2012年1月 6日 16:28 | 档案 | 引用
grants 说:
perl泪流满面
2012年1月 6日 17:03 | 档案 | 引用
lyman 说:
bash 的问题在于调试麻烦。这需求上 perl 是最合理的选择。另外用好 awk 的话其实可以简化一些。类似这样(未测试)的写法可以省掉 uniq,也使排序的数据量小很多
awk '$9==200{count[$7]++}END{for(i in count) print a[i],i}'
2012年1月 6日 18:39 | 档案 | 引用
左岸读书 说:
其实大家还想看到一些其他的总结哟~
2012年1月 6日 21:48 | 档案 | 引用
ssdt 说:
10G 10秒
每秒1G
感觉是不是太快了
好象没有什么设备能读写这么快
2012年1月 7日 02:41 | 档案 | 引用
2362331978 说:
在博主部分文章的留言里发现很多人都喜欢说"非专业",不晓得"专业"如何定义,是否有点教条化。
2012年1月 7日 13:59 | 档案 | 引用
二军 说:
第一名、《人生只有900个月》
基本上每个人都会对这个话题产生兴趣,人生啊。。。
2012年1月 7日 15:28 | 档案 | 引用
cumirror 说:
佩服阮兄对技术的钻研精神。
2012年1月 7日 15:44 | 档案 | 引用
rche 说:
论开发速度: 这取决于你对各语言的熟悉程度,如果是同等熟悉的话,使用其它语言(perl、php、python、ruby)来开发也不会超过20行代码,Bash的开发速度不会比其它语言更快;
论运行速度: Bash只会更慢。因为Bash有fork,execve各小程序(awk,sort,uniq)的开销,而其它解释语言一般是单一进程内置正则、sort、uniq等函数,内置函数一般来说运行更快。
这一点运行速度上如果有人能接触到的你的log数据写一段同等的perl或python肯定更快。
只是一般意义上来说,接触过Linux的人对Shell(Bash)都有不同程度的熟悉,而其它解释语言都需要专门的学习才能熟悉、不是所有人都会。从这个角度讲,一般人使用Bash的开发速度会更快一些。
2012年1月 8日 04:46 | 档案 | 引用
阮一峰 说:
@rche:
看来我对Bash的看法还是不正确,这就再次修改原文。
2012年1月 8日 17:24 | 档案 | 引用
小可 说:
文章写的很多啊,都写了近百篇,厉害,反正我是写不出来!
2012年1月10日 17:44 | 档案 | 引用
车东 说:
awstats有extend 配置可以得到类似统计并集成在awstats的报告中
2012年1月11日 17:10 | 档案 | 引用
李晨亮 说:
不知道这里的James是不是《Stanley博士的家2》的作者?
2012年1月11日 22:56 | 档案 | 引用
莫浅 说:
我想问博主一个问题,你这样什么都接触一点。能精通吗?感觉什么都会,但都是很表面的东西,再无下文。感觉像是写百科全书。
2012年1月12日 16:47 | 档案 | 引用
BlackJack 说:
博主是“博”士,即使做研究,博一些也很有益处。“博”是一种能力,跟“专”没有冲突,你没有不能证明别人没有
2012年1月17日 23:50 | 档案 | 引用
MGhostSoft 说:
我想知道你的服务器配置到底是怎样的,怎么能办到20秒处理 10GB 的数据?那就是 500MB/s ,就算是纯粹读取也很困难吧。
2012年2月26日 01:04 | 档案 | 引用
Chris Song 说:
squawk可以很方便的直接在access log上运行sql脚本,支持group by等aggregtion操作,下面是我在原版上修改过的版本http://github.com/fakechris/squawk
2012年3月 8日 21:28 | 档案 | 引用
Marty 说:
同意,想起赫胥黎一句话:know something about everything and everything about something
2012年3月19日 14:39 | 档案 | 引用
云�w 说:
刚刚测试了下,分析7G的maillog用了90s额,比之前用grep,uniq快了点,主要是loding也小了
2012年3月30日 15:57 | 档案 | 引用
bash 说:
脚本确实很快
脚本的缺陷在于IO不好,需要浪费大量时间在读写上
所以如果你的读写设备不是很好,用脚本等于找别扭
2012年4月 2日 13:32 | 档案 | 引用
bash 说:
在数据很大的情况下
我认为差距可以忽略了
不过这里的脚本速度明显不是真实速度
bash直接调用的程序会被hang住很久
所以真实速度没这么快
2012年4月 3日 13:30 | 档案 | 引用
Vaporz 说:
hi,博主你好,10GB的log还是挺多的,假如您手边有两三台电脑的话,可以尝试搭一个hadoop集群,用Hive来查询数据,可以像用SQL查询关系型数据库一样方便
2012年5月 7日 21:50 | 档案 | 引用
lucky 说:
以我的使用经验来看,awk处理文本是最快的。难道是我PHP写的太烂了?
不知道楼上各位从哪来的数据,看的很专业似的。。。。
而且调试肯定也是最方便的。我不知道各位有没有试过管道。。。
再一个10G数据的排序。。。。。我真想看看你们20行代码怎么用PHP出来。
唉。。。说的有点多,实在忍不住吐槽
2012年7月 3日 17:44 | 档案 | 引用
阿来 说:
这样统计会不准确,没有刨去爬虫的访问
2012年12月19日 14:54 | 档案 | 引用
adam 说:
我找了个其他日志测试,用的是下面的语句,6G的数据用了120秒,和云飞的测试结果差不多,不知道楼主的10G文件为什么20秒能算出来?
awk '$9==200{count[$7]++}END{for(i in count) print a[i],i}'
2013年3月29日 18:41 | 档案 | 引用