python - 分析 nginx access 日志文件

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-爬虫

。。。。。。

 

你可能感兴趣的:(python,python,nginx)