目录
1、背景介绍 2、思路演进 2.1、第一步读取日志 2.2、第二步解析日志 2.3、第三步分析日志 2.4、第四步生成报告 2.5、第五步日志采集 2.6、结果展示 2.7、可扩展方向
大佬请自觉路过~ ~ ~
1、背景介绍
本文以我的博客站点其中一段时间的访问日志为例进行分析
用到的知识点 基本数据类型列表,基本数据类型字典, re 模块正则匹配, pandas 模块数据处理, xlwt 模块 excel 写入等
最终实现的功能 分析得到日志中访问 ip 的 top20 ,访问地址的 top20 ,访问客户端 ua 的排名,并且生成 excel 报表
2、思路演进
2.1、第一步读取日志
对 nginx 进行日志分析,首先拿到需要分析的 nginx 日志文件,日志文件的内容具有固定的定义方法,每一行的日志中每一个特殊的字段都代表着具体的含义,例如:
95.143.192.110?-?-?[15/Dec/2019:10:22:00?+0800]?"GET?/post/pou-xi-he-jie-jue-python-zhong-wang-luo-nian-bao-de-zheng-que-zi-shi/?HTTP/1.1"?304?0?"https://HdhCmsTestssgeek测试数据/"?"Mozilla/5.0?(Macintosh;?Intel?Mac?OS?X?10_15_1)?AppleWebKit/537.36?(KHTML,?like?Gecko)?Chrome/78.0.3904.108?Safari/537.36"
上面的日志内容的字段信息依次代表着:访问者来源 ip 、访问时间、 http 请求方法、请求地址、 http 状态码、本次请求的字节大小、 refer 信息、客户端 ua 标识
因此,首先提炼出一行内容,对这行内容进行分组统计并记录每个字段的具体信息,然后把对这一行的分析手段去对整个日志文件进行分析,为了匹配日志中的每个字段,需要用到 re 模块进行正则匹配,代码如下:
import?re obj?=?re测试数据pile(r'(?P<ip>.*?)-?-?\[(?P<time>.*?)\]?"(?P<request>.*?)"?(?P<status>.*?)?(?P<bytes>.*?)?"(?P<referer>.*?)"?"(?P<ua>.*?)"') def?load_log(path): ????with?open(path,?mode="r",?encoding="utf-8")?as?f: ????????for?line?in?f: ????????????line?=?line.strip() ????????????parse(line) def?parse(line): ????#?解析单行nginx日志 ????try: ????????result?=?obj.match(line) ????????print(result.group("ip")) ????except: ????????pass if?__name__?==?'__main__': ????load_log("nginx_access.log")
通过 re 模块依次分组匹配为: ip 、 time 、 request 、 status 、 bytes 、 referer 、 ua 上面的内容最终打印出来了所有的访问者来源 ip
进一步加强,输出所有字段,直接打印 print(result.groupdict()) 即可,输出结果是多个字典,如下所示:
{'ip':?'46.229.168.150?',?'time':?'24/Dec/2019:13:21:39?+0800',?'request':?'GET?/post/zabbix-web-qie-huan-wei-nginx-ji-https?HTTP/1.1',?'status':?'301',?'bytes':?'178',?'referer':?'-',?'ua':?'Mozilla/5.0?(compatible;?SemrushBot/6~bl;?+http://HdhCmsTestsemrush测试数据/bot.html)'}
2.2、第二步解析日志
精准分析单行日志,并且加入一些格式化输出和过滤的手段
load_log() 函数: 在 load_log() 函数中,为了避免有错误的日志(类似于“脏数据”),因此定义了两个空列表 lst 和 error_lst 用来记录匹配的结果,列表中的每一个元素表示匹配的一行日志,最后打印了总行数,匹配到的行数,不能匹配到的行数(错误日志行数)
parse() 函数: 在 parse() 函数中,传入参数 line ,一次对每行中分组匹配到的每一个字段进行处理,处理完成后赋值到列表元素,其中客户端ua标识仅仅列出了一些常见的,如果想要匹配的更为精确,可以参考常用浏览器(PC/移动)user-agent参考对照表,把匹配规则写的更精确即可
import?re import?datetime obj?=?re测试数据pile( ????r'(?P<ip>.*?)-?-?\[(?P<time>.*?)\]?"(?P<request>.*?)"?(?P<status>.*?)?(?P<bytes>.*?)?"(?P<referer>.*?)"?"(?P<ua>.*?)"') def?load_log(path): ????lst?=?[] ????error_lst?=?[] ????i?=?0 ????with?open(path,?mode="r",?encoding="utf-8")?as?f: ????????for?line?in?f: ????????????line?=?line.strip() ????????????dic?=?parse(line) ????????????if?dic:??#?正确的数据添加到lst列表中 ????????????????lst.append(dic) ????????????else: ????????????????error_lst.append(line)??#?脏数据添加到error_lst列表中 ????????????i?+=?1 ????print(i) ????print(len(error_lst)) ????print(len(lst)) def?parse(line): ????#?解析单行nginx日志 ????dic?=?{} ????try: ????????result?=?obj.match(line) ????????#?ip处理 ????????ip?=?result.group("ip") ????????if?ip.strip()?==?'-'?or?ip.strip()?==?"":??#?如果是匹配到没有ip就把这条数据丢弃 ????????????return?False ????????dic['ip']?=?ip.split(",")[0]??#?如果有两个ip,取第一个ip ????????#?状态码处理 ????????status?=?result.group("status")??#?状态码 ????????dic['status']?=?status ????????#?时间处理 ????????time?=?result.group("time")??#?21/Dec/2019:21:45:31?+0800 ????????time?=?time.replace("?+0800",?"")??#?替换+0800为空 ????????t?=?datetime.datetime.strptime(time,?"%d/%b/%Y:%H:%M:%S")??#?将时间格式化成友好的格式 ????????dic['time']?=?t ????????#?request处理 ????????request?=?result.group( ????????????"request")??#?GET?/post/pou-xi-he-jie-jue-python-zhong-wang-luo-nian-bao-de-zheng-que-zi-shi/?HTTP/1.1 ????????a?=?request.split()[1].split("?")[0]??#?往往url后面会有一些参数,url和参数之间用?分隔,取出不带参数的url ????????dic['request']?=?a ????????#?user_agent处理 ????????ua?=?result.group("ua") ????????if?"Windows?NT"?in?ua: ????????????u?=?"windows" ????????elif?"iPad"?in?ua: ????????????u?=?"ipad" ????????elif?"Android"?in?ua: ????????????u?=?"android" ????????elif?"Macintosh"?in?ua: ????????????u?=?"mac" ????????elif?"iPhone"?in?ua: ????????????u?=?"iphone" ????????else: ????????????u?=?"其他设备" ????????dic['ua']?=?u ????????#?refer处理 ????????referer?=?result.group("referer") ????????dic['referer']?=?referer ????????return?dic ????except: ????????return?False if?__name__?==?'__main__': ????load_log("nginx_access.log")
执行代码,查看打印的结果,控制台输出:
9692 542 9150
依次表示日志文件中的总行数、匹配错误(没有匹配到的)的行数、匹配正确的行数
2.3、第三步分析日志
利用 pandas 模块进行日志的分析 analyse() 函数: 将解析过滤得到的 lst 列表作为参数传入,列表中的数据格式形如 [{ip:xxx, api:xxx, status:xxxx, ua:xxx}]
df = pd.DataFrame(lst) 将解析得到的列表转换成为类似表格的类型,控制台的输出 df 如下,处理后为每个数据加上了序号,第一行相当于表头,表头就是前面得到的字典中的 key
????????????????????ip?status??...???????ua??????????????????referer 0??????95.143.192.110?????200??...??????mac????????????????????????- 1??????95.143.192.110?????304??...??????mac????????????????????????- 2??????95.143.192.110?????304??...??????mac????????????????????????- 3??????95.143.192.110?????304??...??????mac??https://HdhCmsTestssgeek测试数据/ 4??????203.208.60.122?????200??...??android????????????????????????- ...????????????????...????...??...??????...??????????????????????... 9145??????46.4.60.249?????404??...?????其他设备????????????????????????- 9146??????46.4.60.249?????404??...?????其他设备????????????????????????- 9147??????46.4.60.249?????404??...?????其他设备????????????????????????- 9148??????46.4.60.249?????404??...?????其他设备????????????????????????- 9149??154.223.188.124?????404??...??windows????????????????????????-
pd.value_counts(df['ip']) 取出 ip 并统计数 ip 的次数;得到的结果第一列是 ip ,第二列是次数, pandas 默认将第一列认为是行索引,因此需要将数据整体右移,通过 reset_index() 重新定义一个索引即可,效果形如:
?????????????????index???ip 0??????89.163.242.228???316 1?????207.180.220.114???312 2?????????78.46.90.53???302 3????????144.76.38.10???301 4????????78.46.61.245???301 ...????????????????...??... 1080????203.208.60.85?????1 1081??????66.249.72.8?????1 1082?????141.8.132.13?????1 1083????207.46.13.119?????1 1084?????203.208.60.7?????1
这个时候发现索引有了,但是表头也跟着右移了,不对应了,需要重新设置一个表头 reset_index().rename(columns={"index": "ip", "ip": "count"}) ,效果形如
????????????????????ip??count 0??????89.163.242.228?????316 1?????207.180.220.114?????312 2?????????78.46.90.53?????302 3????????78.46.61.245?????301 4????????144.76.38.10?????301 ...????????????????...????... 1080?????47.103.17.71???????1 1081????42.156.254.92???????1 1082??220.243.136.156???????1 1083???180.163.220.61???????1 1084???106.14.215.243???????1
往往分析日志只需要得到访问次数的前几名,例如前 20 名, pandas 同样给出了很方便的 iloc 通过切片实现这个需求, iloc[:20, :] :取出前 20 行,取出所有列,最终的处理代码为
????ip_count?=?pd.value_counts(df['ip']).reset_index().rename(columns={"index":?"ip",?"ip":?"count"}).iloc[:20,?:] ????print(ip_count)
得到的数据结果为
??????????????????ip??count 0????89.163.242.228?????316 1???207.180.220.114?????312 2???????78.46.90.53?????302 3??????144.76.38.10?????301 4??????78.46.61.245?????301 5?????144.76.29.148?????301 6????204.12.208.154?????301 7?????148.251.92.39?????301 8?????????5.9.70.72?????286 9?????223.71.139.28?????218 10?????95.216.19.59?????209 11????221.13.12.147?????131 12?????117.15.90.21?????130 13??175.184.166.181?????129 14???148.251.49.107?????128 15????171.37.204.72?????127 16???124.95.168.140?????118 17????171.34.178.76??????98 18???60.216.138.190??????97 19????141.8.142.158??????87
同样,可以把 request 、 ua 等进行相同的操作
2.4、第四步生成报告
利用 xlwt 模块将pandas分析得到的数据写入到 excel 表格中,写入前需要将pandas处理后的数据转化成普通的数据
????ip_count_values?=?ip_count.values ????request_count_values?=?request_count.values ????ua_count_values?=?ua_count.values
这个数据类型是:数组对象 numpy.ndarray ,形如:
[['89.163.242.228?'?316] ?['207.180.220.114?'?312] ?['78.46.90.53?'?302] ?['204.12.208.154?'?301] ?['144.76.29.148?'?301] ?['144.76.38.10?'?301] ?['78.46.61.245?'?301] ?['148.251.92.39?'?301] ?['5.9.70.72?'?286] ?['223.71.139.28?'?218] ?['95.216.19.59?'?209] ?['221.13.12.147?'?131] ?['117.15.90.21?'?130] ?['175.184.166.181?'?129] ?['148.251.49.107?'?128] ?['171.37.204.72?'?127] ?['124.95.168.140?'?118] ?['171.34.178.76?'?98] ?['60.216.138.190?'?97] ?['141.8.142.158?'?87]]
通过 xlwt 模块写入 sheet 页,每个 sheet 页中写入对应处理的数据
#?写入excel wb?=?xlwt.Workbook()??#?打开一个excel文档 sheet?=?wb.add_sheet("ip访问top20")??#?新建一个sheet页 #?写入头信息 row?=?0 sheet.write(row,?0,?"ip")??#?写入行,列,内容 sheet.write(row,?1,?"count")??#?写入行,列,内容 row?+=?1??#?行号加一 for?item?in?ip_count_values: ????sheet.write(row,?0,?item[0]) ????sheet.write(row,?1,?item[1]) ????row?+=?1
2.5、第五步日志采集
日志分析完了,回过头来需要的是采集到日志文件,并且定时的去进行分析,可以利用 time 模块得到时间并且判断,实现定时的分析,例如,每月3号的凌晨1点进行日志分析
import?time if?__name__?==?'__main__': ????while?1: ????????stime?=?datetime.datetime.now().strftime("%d:%H:%M:%S") ????????if?stime?==?"03:01:00:00": ????????????lst,?error_lst?=?load_log("nginx_access.log") ????????????analyse(lst) ????????time.sleep(1)
当然也可以通过服务器级别的定时任务功能定时的调用脚本分析
2.6、结果展示
按照前面的演进过程,最终的代码如下:
import?re import?datetime import?pandas?as?pd import?xlwt obj?=?re测试数据pile( ????r'(?P<ip>.*?)-?-?\[(?P<time>.*?)\]?"(?P<request>.*?)"?(?P<status>.*?)?(?P<bytes>.*?)?"(?P<referer>.*?)"?"(?P<ua>.*?)"') def?load_log(path): ????lst?=?[] ????error_lst?=?[] ????i?=?0 ????with?open(path,?mode="r",?encoding="utf-8")?as?f: ????????for?line?in?f: ????????????line?=?line.strip() ????????????dic?=?parse(line) ????????????if?dic:??#?正确的数据添加到lst列表中 ????????????????lst.append(dic) ????????????else: ????????????????error_lst.append(line)??#?脏数据添加到error_lst列表中 ????????????i?+=?1 ????return?lst,?error_lst def?parse(line): ????#?解析单行nginx日志 ????dic?=?{} ????try: ????????result?=?obj.match(line) ????????#?ip处理 ????????ip?=?result.group("ip") ????????if?ip.strip()?==?'-'?or?ip.strip()?==?"":??#?如果是匹配到没有ip就把这条数据丢弃 ????????????return?False ????????dic['ip']?=?ip.split(",")[0]??#?如果有两个ip,取第一个ip ????????#?状态码处理 ????????status?=?result.group("status")??#?状态码 ????????dic['status']?=?status ????????#?时间处理 ????????time?=?result.group("time")??#?21/Dec/2019:21:45:31?+0800 ????????time?=?time.replace("?+0800",?"")??#?替换+0800为空 ????????t?=?datetime.datetime.strptime(time,?"%d/%b/%Y:%H:%M:%S")??#?将时间格式化成友好的格式 ????????dic['time']?=?t ????????#?request处理 ????????request?=?result.group( ????????????"request")??#?GET?/post/pou-xi-he-jie-jue-python-zhong-wang-luo-nian-bao-de-zheng-que-zi-shi/?HTTP/1.1 ????????a?=?request.split()[1].split("?")[0]??#?往往url后面会有一些参数,url和参数之间用?分隔,取出不带参数的url ????????dic['request']?=?a ????????#?user_agent处理 ????????ua?=?result.group("ua") ????????if?"Windows?NT"?in?ua: ????????????u?=?"windows" ????????elif?"iPad"?in?ua: ????????????u?=?"ipad" ????????elif?"Android"?in?ua: ????????????u?=?"android" ????????elif?"Macintosh"?in?ua: ????????????u?=?"mac" ????????elif?"iPhone"?in?ua: ????????????u?=?"iphone" ????????else: ????????????u?=?"其他设备" ????????dic['ua']?=?u ????????#?refer处理 ????????referer?=?result.group("referer") ????????dic['referer']?=?referer ????????return?dic ????except: ????????return?False def?analyse(lst):?#?[{ip:xxx,?api:xxx,?status:xxxx,?ua:xxx}] ????df?=?pd.DataFrame(lst)??#?转换成表格 ????#?print(df) ????#?print(df['ip'])??#?只取出ip这一列 ????ip_count?=?pd.value_counts(df['ip']).reset_index().rename(columns={"index":?"ip",?"ip":?"count"}).iloc[:20,?:] ????request_count?=?pd.value_counts(df['request']).reset_index().rename(columns={"index":?"request",?"request":?"count"}).iloc[:20,?:] ????ua_count?=?pd.value_counts(df['ua']).reset_index().rename(columns={"index":?"ua",?"ua":?"count"}).iloc[:,?:] ????#?从pandas转化成我们普通的数据 ????ip_count_values?=?ip_count.values ????request_count_values?=?request_count.values ????ua_count_values?=?ua_count.values ????#?print(type(ip_count_values)) ????#?写入excel ????wb?=?xlwt.Workbook()??#?打开一个excel文档 ????sheet?=?wb.add_sheet("ip访问top20")??#?新建一个sheet页 ????#?写入头信息 ????row?=?0 ????sheet.write(row,?0,?"ip")??#?写入行,列,内容 ????sheet.write(row,?1,?"count")??#?写入行,列,内容 ????row?+=?1??#?行号加一 ????for?item?in?ip_count_values: ????????sheet.write(row,?0,?item[0]) ????????sheet.write(row,?1,?item[1]) ????????row?+=?1 ????sheet?=?wb.add_sheet("request访问top20")??#?新建一个sheet页 ????#?写入头信息 ????row?=?0 ????sheet.write(row,?0,?"request")??#?写入行,列,内容 ????sheet.write(row,?1,?"count")??#?写入行,列,内容 ????row?+=?1??#?行号加一 ????for?item?in?request_count_values: ????????sheet.write(row,?0,?item[0]) ????????sheet.write(row,?1,?item[1]) ????????row?+=?1 ????sheet?=?wb.add_sheet("ua访问top")??#?新建一个sheet页 ????#?写入头信息 ????row?=?0 ????sheet.write(row,?0,?"ua")??#?写入行,列,内容 ????sheet.write(row,?1,?"count")??#?写入行,列,内容 ????row?+=?1??#?行号加一 ????for?item?in?ua_count_values: ????????sheet.write(row,?0,?item[0]) ????????sheet.write(row,?1,?item[1]) ????????row?+=?1 ????wb.save("abc.xls") if?__name__?==?'__main__': ????lst,?error_lst?=?load_log("nginx_access.log") ????analyse(lst)
生成的 excel 报表内容如下
ip 排名
访问地址排名
客户端 ua 排名
2.7、可扩展方向
本文进行日志的分析算是入门之作,可以进一步扩展的方向比如:分析报表的定时消息邮件等推送,分析报表的图形化展示等等