python - 分析 access 日志文件

nginx 的 access 日志格式约定:

    #全局变量
# $args 这个变量等于请求行中的参数,同$query_string
# $content_length 请求头中的Content-length字段。
# $content_type 请求头中的Content-Type字段。
# $document_root 当前请求在root指令中指定的值。
# $host 请求主机头字段,否则为服务器名称。
# $http_user_agent 客户端agent信息
# $http_cookie 客户端cookie信息
# $limit_rate 这个变量可以限制连接速率。
# $request_method 客户端请求的动作,通常为GET或POST。
# $remote_addr 客户端的IP地址。
# $remote_port 客户端的端口。
# $remote_user 已经经过Auth Basic Module验证的用户名。
# $request_filename 当前请求的文件路径,由root或alias指令与URI请求生成。
# $scheme HTTP方法(如http,https)。
# $server_protocol 请求使用的协议,通常是HTTP/1.0或HTTP/1.1。
# $server_addr 服务器地址,在完成一次系统调用后可以确定这个值。
# $server_name 服务器名称。
# $server_port 请求到达服务器的端口号。
# $request_uri 包含请求参数的原始URI,不包含主机名,如:”/foo/bar.php?arg=baz”。
# $uri 不带请求参数的当前URI,$uri不包含主机名,如”/foo/bar.html”。
# $document_uri 与$uri相同。
#参数注释:
# $time_local #访问的时间
# $http_host #访问的服务端域名
# $request #用户的http请求起始行信息
# $status #http状态码,记录请求返回的状态码,例如:200、301、404等
# $body_bytes_sent #服务器发送给客户端的响应body字节数
# $http_referer #记录此次请求是从哪个连接访问过来的,可以根据该参数进行防盗链设置。
# $http_x_forwarded_for #当前端有代理服务器时,设置web节点记录客户端地址的配置,此参数生效的前提是代理服务器也要进行相关的x_forwarded_for设置
# $request_time #nginx处理请求的时间
# $upstream_response_time #后端应用响应时间
log_format main '"$time_local","$request","$remote_user",'
'"$http_user_agent","$http_referer","$remote_addr","$status","$body_bytes_sent","$upstream_response_time"';
'''
约定:
nginx 的 log 目录下有两个目录bac、analyze
bac 每日备份的 access log,文件命名格式:qmw_access-200425.log
analyze 存放分析完的结果文件。
调用:
python nginx_logs_spliter.py --nginxConf=nginx.conf --nginxDir=/Users/site/nginx --logPrefix=qmw_access
参数:
args.nginxConf nginx配置文件名
args.nginxDir nginx配置文件目录
args.logPrefix access log 前缀
args.signal 信号:today=分析今天的log yestoday=分析昨天的log,不传默认分析昨日数据。
windows 部署:
系统执行计划,调用 bat 。
批处理文件内容:
python nginx_logs_spliter.py --nginxConf=nginx.conf --nginxDir=/Users/site/nginx --logPrefix=qmw_access
'''
nginx_access_analyze.py 源码
#!/usr/bin/env python3
# coding=utf-8
import os
import sys
import argparse
import codecs
import time,datetime
import re

_version='200426.1'
_debugCount = 50000
_isDebug = True
_isDebug = False
_ipDic = dict() # ip 统计
_reqTimes = dict() # 并发统计(秒)
_statusDic = dict() # 响应码
_spiderDic = dict() # 爬虫统计
_userAgentDic = dict() # ua 统计

class requestInfo():
dateTime = None # 请求时间
path = "" # 请求地址
ip = ""
refererUrl = "" # 引用地址
userAgent = ""
scStatus = 0 # 状态码
scBytes = 0 # 返回的字节数
timeTaken = 0 # 处理时间
cookie = ""

class reqTimesModel:
times = 0
scBytes = 0
timeTaken = 0
statusDic = None
ipDic = None

class spiderInfo:
name = ''
total = 0
total_404 = 0
subDic = None # dict() # 子站点统计
ipDic = None # dict() # 爬虫ip 统计

class userAgentInfo:
userAgent=""
total = 0
codeDic = None # 状态码统计
ipDic = None # dict() # ip 统计,# 统计时必须新建字典

'''
log 格式:
"2020-04-25 00:25:41","GET /xxx/xxx.jpg HTTP/1.1","-","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36","-","127.0.0.1","499","0","0.402"
log field:
'"$time_local","$request","$remote_user",'
'"$http_user_agent","$http_referer","$remote_addr","$status","$body_bytes_sent","$upstream_response_time"';
'''
dtEnMatch = re.compile('\d+/[a-zA-Z]+/\d+:\d+:\d+:\d+')
dtCnMatch = re.compile('\d+-\d+-\d+\s\d+:\d+:\d+')

clearMatch = re.compile('^(,?)"|"$')
def getContent(txt):
if not txt:
return txt
return clearMatch.sub('',txt)

urlMatch = re.compile('\s.+\s')
def getPath(txt):
if not txt:
return txt
# re.findall('\s.+\s',txt)[0].strip()
arr = urlMatch.findall(txt)
if len(arr) > 0:
return arr[0].strip()

'''
python中时间日期格式化符号:
%y 两位数的年份表示(00-99)
%Y 四位数的年份表示(000-9999)
%m 月份(01-12)
%d 月内中的一天(0-31)
%H 24小时制小时数(0-23)
%I 12小时制小时数(01-12)
%M 分钟数(00=59)
%S 秒(00-59)
%a 本地简化星期名称
%A 本地完整星期名称
%b 本地简化的月份名称
%B 本地完整的月份名称
%c 本地相应的日期表示和时间表示
%j 年内的一天(001-366)
%p 本地A.M.或P.M.的等价符
%U 一年中的星期数(00-53)星期天为星期的开始
%w 星期(0-6),星期天为星期的开始
%W 一年中的星期数(00-53)星期一为星期的开始
%x 本地相应的日期表示
%X 本地相应的时间表示
%Z 当前时区的名称
'''
def getDatetime(txt):
#兼容两种时间格式
'''
"26/Apr/2020:15:18:33 +0800"
"2020-04-25 00:25:41"
'''
# "26/Apr/2020:15:18:33 +0800"
arr = dtEnMatch.findall(txt)
if len(arr)>0:
dt = datetime.datetime.strptime(arr[0],'%d/%b/%Y:%H:%M:%S')
return dt
# "2020-04-25 00:25:41"
arr = dtCnMatch.findall(txt)
if len(arr)>0:
dt = datetime.datetime.strptime(arr[0],'%Y-%m-%d %H:%M:%S')
return dt


colMatch = re.compile(',?".+?"')
def getInfo(row):
arr = colMatch.findall(row)
if len(arr)<9:
print('arr len < 9 of row spilit')

reqInfo = requestInfo()
reqInfo.dateTime = getDatetime(arr[0])
reqInfo.path = getPath(arr[1])
reqInfo.userAgent = getContent(arr[3])
reqInfo.refererUrl = getContent(arr[4])
reqInfo.ip = getContent(arr[5])
reqInfo.scStatus = getContent(arr[6])
reqInfo.scBytes = getContent(arr[7])
reqInfo.timeTaken = getContent(arr[8])

return reqInfo

def dictTostring(dic,sort=False,limit=100):
items = None
index = 0
if sort:
#按 value 倒序
items = sorted(dic.items(),key=lambda x:x[1],reverse=True)
else:
items = dic.items()
sbr = 'c=%s\t' % str(len(dic))
for (k,v) in items:
if len(sbr)>0:
sbr +='|'
if index > limit:
sbr += 'top %s ...' % limit
break;
index += 1
sbr += '{}:{}'.format(k,v)
return sbr

def analyze(logFileFullName,outputFileFullName,rows,timeCost):
curTime = datetime.datetime.now()
f = open(outputFileFullName,'w+',encoding='utf-8')
#
f.writelines("分析文件 = {}\n".format(logFileFullName))
f.writelines("输出文件 = {}\n".format(outputFileFullName))
f.writelines("版本号 = +{}\n".format(_version))
f.writelines("\n{}\n".format('~'*80))
#---------------------------------------------------------
f.writelines("#汇总时间\t{}\n".format(curTime.strftime('%Y-%m-%d %H:%M:%S')))
f.writelines("#总记录数 = {}\n".format(rows))
f.writelines("#搜索爬虫 = {}\n".format(len(_spiderDic)))
f.writelines("#总ip数= {}\n".format(len(_ipDic)))
f.writelines("#总userAgent数= {}\n".format(len(_userAgentDic)))
# f.writelines("#总目录量 = {}\n".format(len(_dirDic)))
# f.writelines("#总小时数 = {}\n".format(len(_hourDic)))
f.writelines("#_isDebug = {}\n".format(_isDebug))
f.writelines('#分析耗时 = %s\n' % timeCost)
#---------------------------------------------------------
# status输出
f.writelines("\n{}\n".format('~'*80))
statusTotal = sum(_statusDic.values())
f.writelines("#响应码 {}种 共 {} 条\n".format(len(_statusDic),statusTotal))
for (k,v) in _statusDic.items():
f.writelines('{}\t{}%\t{}\n'.format(k
,round(v/statusTotal*100,5)
,v))
#---------------------------------------------------------
# 爬虫统计出参
spiderList = sorted(_spiderDic.items(),key=lambda x:x[1].total,reverse=True)
print('sort\tspiderSort.size={}\t{}\n'.format(len(spiderList),type(spiderList)))
f.writelines("\n{}\n".format('~'*80))
f.writelines("\n{}\n".format('#爬虫统计'))
# 爬虫
for (k,v) in spiderList:
f.writelines('总={}\terr={}\t{}\n'.format(str(v.total).zfill(6)
,str(v.total_404).zfill(6)
,k))

f.writelines("\n{}\n".format('#爬虫统计 - IP'))
# 爬虫
for (k,v) in spiderList:
f.writelines('{}\t{}\n'.format(k
,dictTostring(v.ipDic)))
#---------------------------------------------------------
#位置 userAgent
f.writelines("\n{}\n".format('~'*80))
f.writelines("\n{}\ttotal={}\n".format('#userAgent top',len(_userAgentDic)))
userAgentList = sorted(_userAgentDic.items(),key=lambda x:x[1].total,reverse=True)
index = 0
for (k,v) in userAgentList:
if index > 30:
f.writelines('top 30 ...\n')
break
index += 1
f.writelines('{}\t{}\n'.format(str(v.total).zfill(6)
,v.userAgent))
#codd 输出
f.writelines('------\t响应码\t%s\n'%(dictTostring(v.codeDic)))
# for (code,ct) in v.codeDic.items():
# f.writelines('------\t\t%s\t%s\n' % (code,ct))
#ip 次数输出
f.writelines('------\tips统计\t%s\n' % (dictTostring(v.ipDic)))


f.writelines("\n{}\n".format('~'*80))
f.writelines("\n{}\ttotal={}\n".format('#userAgent all',len(_userAgentDic)))
for (k,v) in userAgentList:
index += 1
f.writelines('{}\t{}\n'.format(str(v.total).zfill(6)
,v.userAgent))
#---------------------------------------------------------
# 输出 所有ip
print("\n\n所有ip - 按请求次数排序 total = {}",len(_ipDic))
# 利用 lambda 定义一个匿名函数 key,参数为 x 元组(k,v),x[1]是值,x[0]是键。reverse参数接受False 或者True 表示是否逆序
ipList = sorted(_ipDic.items(),key=lambda x:x[1],reverse=True) # 按次数排序
print('sort\tipList.size={}\t{}\n'.format(len(ipList),type(ipList)))
f.writelines("\n{}\n".format('~'*80))
f.writelines("#所有ip - 按请求次数排序 = {}\n"
.format(len(ipList)))
top = 0
for (k,v) in ipList:
top += 1
if top >= 100:
f.writelines("print top 100 ... \n")
break
f.writelines('{}\t{}\n'.format(k,v))

#---------------------------------------------------------
#---------------------------------------------------------

print('分析汇总输出完成\t%s'%outputFileFullName)

def ipTotal(reqInfo):
if reqInfo.ip not in _ipDic.keys():
_ipDic[reqInfo.ip] = 0

_ipDic[reqInfo.ip] += 1


def timesSecondTotal(reqInfo):
key = reqInfo.dateTime.strftime('%Y-%m-%d %H:%M:%S') # datetime to 字符串
if key not in _reqTimes.keys():
item = reqTimesModel()
item.scBytes = reqInfo.scBytes
item.times = 0
item.timeTaken = reqInfo.timeTaken
item.ipDic = dict()
item.statusDic = dict()
_reqTimes[key] = item
item = _reqTimes[key]
item.scBytes += reqInfo.scBytes
item.times += 1
item.timeTaken += reqInfo.timeTaken
# 记录ip及次数
ip = reqInfo.ip
# dic = item.ipDic
if ip not in item.ipDic.keys():
item.ipDic[ip]=0
item.ipDic[ip] += 1
# 记录状态码
st = reqInfo.scStatus
#dic = item.statusDic
if st not in item.statusDic.keys():
item.statusDic[st]=0
item.statusDic[st] += 1

def statusTotal(reqInfo):
key = reqInfo.scStatus
if key not in _statusDic.keys():
_statusDic[key] = 0
_statusDic[key] += 1


def getSpiderKey(ua):
key = 'other'
if not ua:
return key
# 百度 baidu
if bool(re.search('baidu', ua , re.IGNORECASE)):
return 'baidu-百度'
# 神马搜索 YisouSpider
if bool(re.search('YisouSpider', ua , re.IGNORECASE)):
return 'shenma-神马'
# 谷歌 google
if bool(re.search('google', ua , re.IGNORECASE)):
return 'google-谷歌'
# 搜狗 (sogou)
if bool(re.search('sogou', ua , re.IGNORECASE)):
return 'sogou-搜狗'
# 360搜索(360Spider、.360.)
if bool(re.search('360Spider|\.360\.', ua , re.IGNORECASE)):
return '360-搜索'
# 必应 bing
if bool(re.search('bing', ua , re.IGNORECASE)):
return 'bing-必应'
# 搜搜 soso
if bool(re.search('soso', ua , re.IGNORECASE)):
return 'soso-搜搜'
# 雅虎 yahoo
if bool(re.search('yahoo', ua , re.IGNORECASE)):
return 'yahoo-雅虎'
# ahrefs 通过抓取网页建立索引库,并提供反向链接分析和服务
if bool(re.search('ahrefs', ua , re.IGNORECASE)):
return 'ahrefsBot-爬虫'
# Semrush 国外的SEO分析爬虫
if bool(re.search('Semrush', ua , re.IGNORECASE)):
return 'SemrushBot-爬虫'
# python 爬虫 https://scrapy.org
if bool(re.search('Scrapy', ua , re.IGNORECASE)):
return 'Scrapy-爬虫'

return key

def userAgentTotal(reqInfo):
key = reqInfo.userAgent
ip = reqInfo.ip
code = reqInfo.scStatus
if key not in _userAgentDic.keys():
uaInfo = userAgentInfo()
uaInfo.userAgent = key
uaInfo.codeDic = dict()
uaInfo.ipDic = dict() # 必须新建字典,否则统计时将重复
_userAgentDic[key] = uaInfo

uaInfo = _userAgentDic[key]
#ip 统计
if ip not in uaInfo.ipDic.keys():
uaInfo.ipDic[ip] = 0
# code 统计
if code not in uaInfo.codeDic.keys():
uaInfo.codeDic[code] = 0

uaInfo.total += 1
uaInfo.ipDic[ip] += 1
uaInfo.codeDic[code] += 1

def spiderTotal(reqInfo):
ua = reqInfo.userAgent
key = getSpiderKey(ua)
ip = reqInfo.ip

info = spiderInfo()
if key not in _spiderDic.keys():
info.ipDic = dict()
_spiderDic[key] = info
# 总
info = _spiderDic[key]
info.total += 1
# ip
if ip not in info.ipDic.keys():
info.ipDic[ip] = 0
info.ipDic[ip] += 1
# 状态
if reqInfo.scStatus != '200' and reqInfo.scStatus != '301':
info.total_404 += 1
# #不识别的 ua 统计
# if key == 'other':
# userAgentTotal(reqInfo)

def main(nginxConf,nginxDir, logPrefix,signal):
if not nginxDir or not logPrefix:
print("参数为空:--nginxDir={} --logPrefix={}".format(nginxDir, logPrefix))
return
if not os.path.exists(nginxDir):
print("文件不存在:--nginxDir={} ".format(nginxDir))
return
conf = os.path.join(nginxDir,nginxConf)
if not os.path.exists(conf):
print("nginx config 不存在:--nginxConf={} ".format(conf))
return

# 今日
today = datetime.datetime.now();
# 日期后缀
yymmdd = ""
if signal == 'today':
yymmdd = today.strftime('%y%m%d')
else:
yestoday = datetime.date.today()-datetime.timedelta(days=1)
yymmdd = yestoday.strftime('%y%m%d')
# 备份文件是否存在
logFileName = "%s-%s.log" % (logPrefix,yymmdd)
logFileFullName = os.path.join(nginxDir,'logs','bac',logFileName)
if not os.path.exists(logFileFullName):
print("log不存在:%s "%logFileFullName)
return
# 读log
start = datetime.datetime.now()
totalCount = 0
with open(logFileFullName,'r',encoding='utf-8') as f:
for line in f.readlines():
totalCount += 1

with open(logFileFullName,'r',encoding='utf-8') as f:
rows = 0
# 按行统计
while True:
rows += 1
if rows % 10000 == 0:
print('已分析\t%s/%s\t耗时\t%ss' % (rows
,totalCount
,(datetime.datetime.now() - start).seconds))
# ------
if _isDebug and rows>=_debugCount:
print('_isDebug = ',_isDebug)
break
# ------
line = f.readline()
if not line: #等价于if line == "":
break
if line.startswith('#'):
print("跳过注释内容=>",line)
continue
reqInfo = getInfo(line)
if not reqInfo:
print("解析失败 reqInfo is null,row = {}".format(line))
continue
# ip 统计,访问量
ipTotal(reqInfo)
# 并发统计
timesSecondTotal(reqInfo)
# 错误码统计
statusTotal(reqInfo)
# 爬虫统计
spiderTotal(reqInfo)
# ua 统计
userAgentTotal(reqInfo)

print('已分析完成\t%s/%s' % (rows,totalCount))
#统计时间
timeCost = datetime.datetime.now() - start
# 执行分析
outputFileName = "analyze-%s-%s.log" % (logPrefix,yymmdd)
outputFileFullName = os.path.join(nginxDir,'logs','analyze',outputFileName)
analyze(logFileFullName,outputFileFullName,rows,timeCost)

if __name__ == '__main__':
parser = argparse.ArgumentParser(description='manual to this script')
parser.add_argument('--nginxConf', type=str, default = None)
parser.add_argument('--nginxDir', type=str, default = None)
parser.add_argument('--logPrefix', type=str, default= None)
parser.add_argument('--signal', type=str, default= None)
args = parser.parse_args()
sys.exit(main(args.nginxConf,args.nginxDir,args.logPrefix,args.signal))
'''
约定:
nginx 的 log 目录下有两个目录bac、analyze
bac 每日备份的 access log,文件命名格式:qmw_access-200425.log
analyze 存放分析完的结果文件。
调用:
python nginx_access_analyze.py --nginxConf=nginx.conf --nginxDir=/Users/site/nginx --logPrefix=qmw_access
参数:
args.nginxConf nginx配置文件名
args.nginxDir nginx配置文件目录
args.logPrefix access log 前缀
args.signal 信号:today=分析今天的log yestoday=分析昨天的log,不传默认分析昨日数据。
windows 部署:
系统执行计划,调用 bat 。
批处理文件内容:
python nginx_logs_spliter.py --nginxConf=nginx.conf --nginxDir=/Users/site/nginx --logPrefix=qmw_access
'''

 

分析结果,部分内容

分析文件 = /Users/site/temp/nginx/logs/bac/qmw_access-200425.log
输出文件 = /Users/site/temp/nginx/logs/analyze/analyze-qmw_access-200425.log
版本号 = +200426.1

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#汇总时间 2020-04-25 16:29:39
#总记录数 = 394802
#搜索爬虫 = 9
#总ip数= 107888
#总userAgent数= 1239
#_isDebug = False
#分析耗时 = 0:00:39.931531

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#响应码 11种 共 394801 条
504 0.07219% 285
200 7.73706% 30546
403 68.78782% 271575
301 23.30136% 91994
304 0.01444% 57
500 0.02736% 108
499 0.01089% 43
400 0.00253% 10
206 0.0228% 90
503 0.0228% 90
408 0.00076% 3

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

#爬虫统计
总=357502 err=255417 other
总=024131 err=015927 google-谷歌
总=009713 err=000051 SemrushBot-爬虫
总=001947 err=000426 baidu-百度
总=001075 err=000196 sogou-搜狗
总=000239 err=000239 shenma-神马
总=000191 err=000004 bing-必应
总=000002 err=000000 360-搜索
总=000001 err=000001 ahrefsBot-爬虫

。。。。。。