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

。。。。。。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章