免费实现从web服务器日志中计数用户会话的方法?

3 投票
1 回答
1694 浏览
提问于 2025-04-16 04:28

网页服务器的日志分析工具(比如Urchin)通常会显示一些“会话”。会话的定义是指在一个有限的、连续的时间段内,某个用户访问网页或点击链接的一系列操作。为了识别这些会话,通常会使用IP地址,以及一些额外的信息,比如用户代理和操作系统,再加上一个会话超时的阈值,比如15分钟或30分钟。

对于某些网站和应用,用户可以登录并且通过cookie被追踪,这样服务器就能准确知道会话何时开始。但我这里说的不是这种情况,而是在服务器没有追踪会话的情况下,如何通过推测来重建会话(“会话重建”)。

我可以用Python写一些代码,尝试根据上述标准来重建会话,但我不想重复造轮子。我正在查看大约40万行的日志文件,所以我得小心使用一个可扩展的算法。

我的目标是从日志文件中提取出唯一的IP地址列表,并且对于每个IP地址,计算出从该日志中推测出的会话数量。绝对的精确和准确并不是必须的……大致的估计就可以了。

根据这个描述

如果满足两个条件,就可以把一个新的请求放入现有的会话中:

  • 请求的IP地址和用户代理与已经放入会话中的请求相同,
  • 这个请求是在上一个请求之后不到十五分钟内发出的。

理论上,写一个Python程序来构建一个字典(以IP为键)是简单的,字典的值是另一个字典(以用户代理为键),值是一个对: (会话数量,最新会话的最新请求)。

但我更倾向于使用现有的实现,如果有的话,因为否则我可能会花很多时间来调整性能。

顺便提一下,以防有人问样本输入,这里有我们日志文件中的一行(已处理):

#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status 
2010-09-21 23:59:59 215.51.1.119 GET /graphics/foo.gif - 80 - 128.123.114.141 Mozilla/5.0+(Windows;+U;+Windows+NT+5.1;+en-US;+rv:1.9.2)+Gecko/20100115+Firefox/3.6+(.NET+CLR+3.5.30729) http://www.mysite.org/blarg.htm 200 0 0

1 个回答

2

好的,如果没有其他答案的话,我来分享一下我的Python实现。我并不是Python方面的专家,欢迎大家提出改进建议。

#!/usr/bin/env python

"""Reconstruct sessions: Take a space-delimited web server access log
including IP addresses, timestamps, and User Agent,
and output a list of the IPs, and the number of inferred sessions for each."""

## Input looks like:
# Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status
# 2010-09-21 23:59:59 172.21.1.119 GET /graphics/foo.gif - 80 - 128.123.114.141 Mozilla/5.0+(Windows;+U;+Windows+NT+5.1;+en-US;+rv:1.9.2)+Gecko/20100115+Firefox/3.6+(.NET+CLR+3.5.30729) http://www.site.org//baz.htm 200 0 0

import datetime
import operator

infileName = "ex100922.log"
outfileName = "visitor-ips.csv"

ipDict = {}

def inputRecords():
    infile = open(infileName, "r")

    recordsRead = 0
    progressThreshold = 100
    sessionTimeout = datetime.timedelta(minutes=30)

    for line in infile:
        if (line[0] == '#'):
            continue
        else:
            recordsRead += 1

            fields = line.split()
            # print "line of %d records: %s\n" % (len(fields), line)
            if (recordsRead >= progressThreshold):
                print "Read %d records" % recordsRead
                progressThreshold *= 2

            # http://www.dblab.ntua.gr/persdl2007/papers/72.pdf
            #   "a new request is put in an existing session if two conditions are valid:
            #    * the IP address and the user-agent are the same of the requests already
            #      inserted in the session,
            #    * the request is done less than fifteen minutes after the last request inserted."

            theDate, theTime = fields[0], fields[1]
            newRequestTime = datetime.datetime.strptime(theDate + " " + theTime, "%Y-%m-%d %H:%M:%S")

            ipAddr, userAgent = fields[8], fields[9]

            if ipAddr not in ipDict:
                ipDict[ipAddr] = {userAgent: [1, newRequestTime]}
            else:
                if userAgent not in ipDict[ipAddr]:
                    ipDict[ipAddr][userAgent] = [1, newRequestTime]
                else:
                    ipdipaua = ipDict[ipAddr][userAgent]
                    if newRequestTime - ipdipaua[1] >= sessionTimeout:
                        ipdipaua[0] += 1
                    ipdipaua[1] = newRequestTime
    infile.close()
    return recordsRead

def outputSessions():
    outfile = open(outfileName, "w")
    outfile.write("#Fields: IPAddr Sessions\n")
    recordsWritten = len(ipDict)

    # ipDict[ip] is { userAgent1: [numSessions, lastTimeStamp], ... }
    for ip, val in ipDict.iteritems():
        # TODO: sum over on all keys' values  [(v, k) for (k, v) in d.iteritems()].
        totalSessions = reduce(operator.add, [v2[0] for v2 in val.itervalues()])
        outfile.write("%s\t%d\n" % (ip, totalSessions))

    outfile.close()
    return recordsWritten

recordsRead = inputRecords()

recordsWritten = outputSessions()

print "Finished session reconstruction: read %d records, wrote %d\n" % (recordsRead, recordsWritten)

更新:处理342K条记录并写入21K条记录花了39秒。这个速度对我来说已经足够了。显然,这39秒中有大约3/4的时间都是在执行strptime()这个函数!

撰写回答