网站的访问日志是一个非常重要的文件,通过分析访问日志,能够挖掘出很多有价值的信息。本文介绍如何利用Python对一个真实网站的访问日志进行分析,文中将综合运用Python文件操作、字符串处理、列表、集合、字典等相关知识点。本文所用的访问日志access_log来自我个人的云服务器,大家可以从文末的附件中下载。
1.提取指定日期的日志
下面是一条典型的网站访问日志,客户端访问网站中的每个资源都会产生一条日志。
193.112.9.107 - - [25/Jan/2020:06:32:58 +0800] "GET /robots.txt HTTP/1.1" 404 208 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:61.0) Gecko/20100101 Firefox/61.0"
每条日志都由空格分隔为九部分,其中比较重要的是:
- 第1部分,193.112.9.107 ,客户端的IP地址。
- 第4部分,[25/Jan/2020:06:32:58 +0800],用户访问请求发生的时间。
- 第5部分,GET /robots.txt HTTP/1.1,客户端发来的HTTP请求报文首部的第一行信息。这部分采用“请求方法 请求资源 请求协议”的格式表示,是日志中最重要的部分。“GET /robots.txt HTTP/1.1”表示客户端以GET方法请求访问服务器的/robots.txt文件,所使用的HTTP协议版本为HTTP/1.1。
- 第6部分, “404”,HTTP响应状态码。状态码用于表示用户的请求是否成功,如果该值为200,则表示用户的访问成功,否则就可能存在问题。一般来说,以2开头的状态码均可以表示用户的访问成功,以3开头的状态码表示用户的请求被页面重新定向到了其它位置,以4开头的状态码表示客户端遇到了错误,以5开头的状态码表示服务器遇到了错误。
- 第7部分,“208”,响应报文的大小,单位字节,这个数值不包括响应报文的首部。把日志记录中的这些值加起来就可以得知服务器在一定时间内发送了多少数据。
- 第9部分, "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:61.0) Gecko/20100101 Firefox/61.0",表示客户端发来的HTTP请求报文中首部“User-Agent”的值,即发出请求的应用程序,通常都是浏览器。
一个日志文件中会包含很多天的日志记录,而我们通常都是针对某一天进行日志分析,所以首先需要从日志文件中把我们要分析的那一天的日志提取出来。
比如要提取1月25日产生的日志,可以执行下面的代码:
>>> with open(‘access_log‘,‘r‘) as f1, open(‘access_log-0125‘,‘w‘) as f2:
... for line in f1:
... if ‘25/Jan/2020‘ in line:
... f2.write(line)
在这段代码中,以r读取模式打开日志文件access_log,作为文件对象f1。以w写入模式创建文件access_log-0125,作为文件对象f2。
然后遍历f1中的每一行,并判断是否包含关键字“25/Jan/2020”,如果有的话,就将这行写入到f2中。
这样就提取出了1月25日的所有日志记录,并保存到了文件access_log-0125中。下面我们就针对文件access_log-0125进行分析。
2.统计PV和UV
PV是指PageView,网站的访问请求数。用户每次对网站中的一个页面的请求或访问均被记录为1个PV,例如某个用户访问了网站中的4个页面,那么PV就+4。而且用户对同一页面的多次访问,PV也是累计的。
UV是指UniqueView,网站的独立访客数,访问网站的一个IP被视为一个访客,在同一天内相同的IP只被计算一次。
因而,我们只要取出每条日志中的IP并统计数量,那么就可以得到PV,将IP去重,就可以得到UV。
执行下面的代码,将每条日志的IP提取出来,并存放到列表ips中。
>>> ips = []
>>> with open(‘access_log-0125‘,‘r‘) as f:
... for line in f:
... ips.append(line.split()[0])
在这段代码中,首先定义了一个空列表ips,然后打开文件access_log-0125并对其进行遍历,每遍历一行,就将该行以空格作为分隔符分割成一个列表,并取出列表中的第一个元素(也就是IP地址),再追加到列表ips中。
下面我们只要统计列表ips的长度就是PV,将列表元素去重之后,再统计长度就是UV。去重这里采用了set()函数,将列表转换为集合,利用Python集合本身的特性,简单高效的完成去重操作。
>>> pv = len(ips)
>>> uv = len(set(ips))
>>> print(pv,uv)
1011 48
3.统计网站出错页面比例
网站的出错比例是很重要的一份数据,直接关系到网站的用户体验。要统计用户访问出错的比例,可以通过统计每个请求的HTTP状态码得到,状态码为2xx或3xx的,视为访问正确,状态码为4xx或5xx,则视为访问出错。
首先可以提取所有页面的状态码,并保存到列表中。
>>> codes = []
>>> with open(‘access_log-0125‘,‘r‘) as f:
... for line in f:
... codes.append(line.split()[8])
再统计出每种状态码的出现次数,保存到字典中:
>>> ret = {}
>>> for i in codes:
... if i not in ret:
... ret[i] = codes.count(i)
...
>>>
>>> ret
{‘200‘: 192, ‘404‘: 796, ‘"-"‘: 4, ‘400‘: 13, ‘403‘: 3, ‘401‘: 2, ‘405‘: 1}
上面这段代码用到了字典,这里是对存放状态码的列表codes进行遍历,从中取出状态码作为字典的键,并统计这种状态码在列表codes中出现的次数,作为字典的值。
如果要统计404页面的比例,可以执行下面的代码:
>>> ret[‘404‘]/sum(ret.values())
0.7873392680514342
在这段代码中,ret[‘404‘]表示从字典ret中取出键为‘404’的元素的值,也就是404状态码的个数。ret.values()表示取出字典中所有元素的值,再用sum()函数求和,得到所有状态码的总数量。两者的比值也就是错误页面的比例了。
从结果中可以看出,我这个网站的页面出错比例特别高,竟然达到了78.7%,如果是一个正常网站,这肯定是有问题的。但我这并不是一个公开网站,也没有什么有价值的页面,因而大部分访问日志其实都是由一些漏洞扫描软件产生的,这也提醒我们,随时都有人在对我们线上的网站进行着各种扫描测试。
4.统计网站热门资源
下面我们继续统计出每个页面的用户访问量,并进行排序。
首先仍然是遍历日志文件,取出用户访问的所有页面,并保存到列表中:
>>> webs = []
>>> with open(‘access_log-0125‘,‘r‘) as f:
... for line in f:
... webs.append(line.split()[6])
接着再统计出每个页面的访问次数,并存放到字典中:
>>> counts = {}
>>> for i in webs:
... if i not in counts:
... counts[i] = webs.count(i)
...
按页面的访问量降序排序:
>>> sorted(counts.items(),key=lambda x:x[1],reverse=True)
[(‘/‘, 175), (‘/robots.txt‘, 25), (‘/phpinfo.php‘, 6), (‘/Admin13790d6a/Login.php‘, 4),
……
为了更好地理解上面这个sorted()函数的用法,下面举例说明。比如我们定义一个名叫services的字典,如果直接用sorted()函数对这个字典排序,默认是按照键进行升序排序。为了显示字典中的所有内容,可以使用items()方法,此时,字典中的每个键值对会被组合成一个元组,并且默认是按照元组中的第一个元素,也就是字典的键进行排序的。
>>> services = {‘http‘:80,‘ftp‘:21,‘https‘:443,‘ssh‘:22}
>>> sorted(services)
[‘ftp‘, ‘http‘, ‘https‘, ‘ssh‘]
>>> sorted(services.items())
[(‘ftp‘, 21), (‘http‘, 80), (‘https‘, 443), (‘ssh‘, 22)]
如果希望按照字典中的值进行排序,也就是要按照元组中的第二个元素排序,可以用key参数指定一个lambda表达式,以每个元组中的第二个元素作为关键字。
>>> sorted(services.items(),key=lambda x:x[1])
[(‘ftp‘, 21), (‘ssh‘, 22), (‘http‘, 80), (‘https‘, 443)]
所以这也就解释了之前那个sorted()函数的含义。至于lambda表达式,其实就是一个根据需要可以随时定义使用的小函数,“lambda x:x[1]”,冒号左侧的x是函数要处理的参数,冒号右侧的表达式是函数要执行的操作,最后再将这个表达式的结果返回。
本文属于“Python安全与运维”系列课程的一部分,该系列课程目前已更新到第二部,感兴趣的朋友可以参考:
第一部 Python基本语法 https://edu.51cto.com/sd/53aa7
第二部 Python编程基础 https://edu.51cto.com/sd/d100c
原文地址:https://blog.51cto.com/yttitan/2469575