内容


使用 Web 服务器日志

学习如何解析和处理 HTTP 访问日志标准格式

Comments

一名用户在浏览器中输入站点的 URL。该用户从 http://cool.si.te/ 定向到 http://cool.si.te/index.html,这导致 HTML 的加载,接着又导致 CSS、JavaScript 和图像的加载。Web 服务器监视所有这些活动,甚至还能监视大多数用户不知道的一些细微之处。如果出现任何错误,从看上去不合常理的模式到缺少图像文件,您已经知道如何从日志文件中发现踪迹。也许您还使用 Web 流量分析器查看日志文件,以监视站点上的流量趋势。但是,大部分情况下,系统管理员都没有使用 Web 服务器日志,这是很可惜的,因为这些日志能够在许多方面发挥作用。在本文中,我将探索从 Web 服务器日志中获取额外价值的一些思想和技巧。我将在流行的 Apache 服务器上测试这些技巧。由于很多其他的工具也使用相同的格式,所以本文中的信息的适用范围应该要广阔得多。

Apache 日志剖析

错误日志报告 Apache 配置或操作中的主要错误,但是在本文中,我将主要讨论访问日志。对于每个 HTTP 请求,访问日志都有一个条目。经典的格式源于美国国家超级计算中心(National Center for Supercomputing Applications,NCSA),这也是一些关键的 Web 革新,例如 Mosaic(后来成为 Netscape 浏览器)、HTTPd(第一个 Apache 发行版的主代码基)和公共网关接口(Common Gateway Interface,CGI)的发源地,其中 CGI 是第一个用于动态 Web 内容的机制。NCSA HTTPd 默认使用所谓的 Common 日志格式,这种格式后来被 Apache 采用。很多 Web 工具仍然在使用 Common 日志格式。

Common 日志格式

清单 1 是 Common 日志格式中的一行:

清单 1. Common 日志格式中的一行
    125.125.125.125 - uche [20/Jul/2008:12:30:45 +0700] "GET /index.html HTTP/1.1" 200 
2345

表 1 解释这些字段。

表 1. common 日志格式一行中的字段
字段名例值描述
host125.125.125.125发送请求的 HTTP 客户机的 IP 地址或主机名
identd-客户机的 Authentication Server Protocol(RFC 931)标识符;该字段很少使用。如果不使用,则以 “-” 的形式给出
usernameucheHTTP 认证的用户名(通过 401 响应握手);这是您在某些网站上看到的登录和密码对话框,而不是嵌入在 Web 页面中的登录表单,ID 信息存储在服务器端会话中。如果不使用(例如,请求无限制的资源),则以 “-” 的形式给出
date/time[20/Jul/2008:12:30:45 +0700]日期、时间和时区,格式为 [dd/MMM/yyyy:hh:mm:ss +-hhmm]
request line"GET /index.html HTTP/1.1"HTTP 请求的首行,包括方法(“GET”)、请求的资源和 HTTP 协议版本
status code200响应中用于表明对请求的处置的数字代码,例如表明成功、失败、重定向或认证需求
bytes响应主体中传输的字节数

组合日志格式

很多工具现在默认使用一种更丰富的变体,即组合日志格式。清单 2 是这种格式的一个例子。

清单 2. 组合日志格式的一行
    125.125.125.125 - uche [20/Jul/2008:12:30:45 +0700] "GET /index.html HTTP/1.1" 200 
2345
"http://www.ibm.com/" "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9a8) 
Gecko/2007100619
GranParadiso/3.0a8" "USERID=Zepheira;IMPID=01234"

这应该都在同一行,但是由于本文排版的限制,我将它拆分成几行。combined 日志格式是 common 格式加上 3 个附加的字段 —— referrer、user agent 和 cookie。可以省略 cookie 字段,或者省略 cookie 和 user agent,甚至还可以省略所有这 3 个字段。表 2 更详细地描述了这几个附加的字段。

表 2. combined 日志格式中附加的字段
字段名例值描述
referrer"http://www.ibm.com/"当一个用户代理从一个站点中指向另一个站点的链接时,它通常向第二个站点报告哪个 URL 引用它
user agent"Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9a8) Gecko/2007100619 GranParadiso/3.0a8"一个字符串,提供关于发出请求的用户代理的信息(例如浏览器版本或 Web crawler)
cookie"USERID=Zepheira;IMPID=01234"HTTP 服务器发送的任何 cookie 的实际键/值对可以在响应中发送回客户机

大多数人使用 Web 服务器的默认格式,但是也可以轻松地定制 Apache 日志的格式。显然,如果对日志格式进行定制,那么必须做相应的调整,包括调整本文显示的代码中的很多部分。

将日志信息提供给程序

您已经看到这些格式的结构是多么良好。很容易使用正则表达式获得这些信息。清单 3 是一个示例程序,它解析一个日志行,并写出对日志信息的总结。该程序是用 Python 编写的,但是其中的重要部分是正则表达式,所以很容易将它移植到任何其他语言。

清单 3. 解析日志行的代码
import re

#This regular expression is the heart of the code.
#Python uses Perl regex, so it should be readily portable
#The r'' string form is just a convenience so you don't have to escape backslashes
COMBINED_LOGLINE_PAT = re.compile(
  r'(?P<origin>\d+\.\d+\.\d+\.\d+) '
+ r'(?P<identd>-|\w*) (?P<auth>-|\w*) '
+ r'\[(?P<date>[^\[\]:]+):(?P<time>\d+:\d+:\d+) (?P<tz>[\-\+]?\d\d\d\d)\] '
+ r'"(?P<method>\w+) (?P<path>[\S]+) (?P<protocol>[^"]+)" (?P<status>\d+) 
(?P<bytes>-|\d+)'
+ r'( (?P<referrer>"[^"]*")( (?P<client>"[^"]*")( (?P<cookie>"[^"]*"))?)?)?\s*\Z'
)

logline = raw_input("Paste the Apache log line then press enter: ")

match_info = COMBINED_LOGLINE_PAT.match(logline)
print #Add a new line

#Print all named groups matched in the regular expression
for key, value in match_info.groupdict().items():
    print key, ":", value

模式 COMBINED_LOGLINE_PAT 是为 combined 格式设计的,但是既然 combined 格式只比 common 格式多 3 个可选字段,那么该模式同时适用于这两种格式。该模式使用一个特定于 Python 的特性,即捕捉组(capturing groups),以便为日志行中的每个字段赋予逻辑名。如果要移植到其他风格的正则表达式,只需使用正则组,并按数字顺序引用字段。注意这个模式有多细粒度。它不是简单地以状态行为单元进行抓取,它可以分别抓取 HTTP 方法、请求路径和协议版本,从而提高更多方便。data/time 也被拆分成日期、时间和时区。图 1 显示在清单 2 所示示例日志行上运行清单 3 的输出。

图 1. 清单 3 的输出
清单 3 的输出
清单 3 的输出

避免 spider

从日志文件中可以获悉很多更有趣的事情,但是这要求能够将 spider 的访问与人的访问区分开来。一些主流搜索引擎,例如 Google 和 Yahoo!,使用非常有侵略性的索引方式,即使站点不是很流行,日志仍会受到来自这些 spider 的流量的支配。要 100% 地剔除 spider 流量几乎是不可能的,但是可以通过检查日志中常见的 spider 模式来剔除大部分 spider 流量。此处 “client” 字段是关键。清单 4 是另一个 Python 程序,该程序也非常容易移植到其他语言。它将日志文件导入到标准输入中,然后导出除认定为 spider 流量的行之外的所有行。

清单 4. 从日志文件中剔除搜索引擎 spider 流量
import re
import sys

#This regular expression is the heart of the code.
#Python uses Perl regex, so it should be readily portable
#The r'' string form is just a convenience so you don't have to escape backslashes
COMBINED_LOGLINE_PAT = re.compile(
  r'(?P<origin>\d+\.\d+\.\d+\.\d+) '
+ r'(?P<identd>-|\w*) (?P<auth>-|\w*) '
+ r'\[(?P<date>[^\[\]:]+):(?P<time>\d+:\d+:\d+) (?P<tz>[\-\+]?\d\d\d\d)\] '
+ r'"(?P<method>\w+) (?P<path>[\S]+) (?P<protocol>[^"]+)" (?P<status>\d+) 
(?P<bytes>-|\d+)'
+ r'( (?P<referrer>"[^"]*")( (?P<client>"[^"]*")( (?P<cookie>"[^"]*"))?)?)?\s*\Z'
)

#Patterns in the client field for sniffing out bots
BOT_TRACES = [
    (re.compile(r".*http://help\.yahoo\.com/help/us/ysearch/slurp.*"),
        "Yahoo robot"),
    (re.compile(r".*\+http://www\.google\.com/bot\.html.*"),
        "Google robot"),
    (re.compile(r".*\+http://about\.ask\.com/en/docs/about/webmasters.shtml.*"),
        "Ask Jeeves/Teoma robot"),
    (re.compile(r".*\+http://search\.msn\.com\/msnbot\.htm.*"),
        "MSN robot"),
    (re.compile(r".*http://www\.entireweb\.com/about/search_tech/speedy_spider/.*"),
        "Speedy Spider"),
    (re.compile(r".*\+http://www\.baidu\.com/search/spider_jp\.html.*"),
        "Baidu spider"),
    (re.compile(r".*\+http://www\.gigablast\.com/spider\.html.*"),
        "Gigabot robot"),
]

for line in sys.stdin:
    match_info = COMBINED_LOGLINE_PAT.match(line)
    if not match_info:
        sys.stderr.write("Unable to parse log line\n")
        continue
    isbot = False
    for pat, botname in BOT_TRACES:
        if pat.match(match_info.group('client')):
            isbot = True
            break
    if not isbot:
        sys.stdout.write(line)

这里的 spider 客户机正则表达式列表并不是完整的。新的搜索引擎总是不断出现。当查看流量的过程中发现新的 spider 时,您应该能仿照清单中的模式,将新的 spider 加入到列表中。

日志统计的基本工具

有很多流行的工具可用于分析 Web 服务器日志和提供关于 Web 的统计信息。使用本文到目前为止的构建块,很容易开发专门的日志信息显示方式。还有一个构建块是从 Apache 日志格式到 JavaScript Object Notation(JSON)的转换。如果有 JSON 格式的信息,就可以轻松地用 JavaScript 分析它、操纵和呈现它。

您也不需要自己编写工具。在本节中,我将展示如何将 Apache 日志文件转换成 JSON 格式,后者是来自 MIT SIMILE 项目的强大数据呈现工具 Exhibit 所使用的格式。在较早的一篇文章 “Practical linked, open data with Exhibit”(见 参考资料)中,我谈到了这个工具。只需提供 JSON,Exhibit 就可以创建一个丰富、动态的系统,以显示、过滤和搜索数据。清单 5(apachelog2exhibit.py)以之前的例子为基础,但是将 Apache 日志转换成 Exhibit 风格的 JSON。

清单 5 (apachelog2exhibit.py)。将非 spider 流量日志条目转换成 Exhibit JSON
import re
import sys
import time
import httplib
import datetime
import itertools

# You'll need to install the simplejson module
# http://pypi.python.org/pypi/simplejson
import simplejson

# This regular expression is the heart of the code.
# Python uses Perl regex, so it should be readily portable
# The r'' string form is just a convenience so you don't have to escape backslashes
COMBINED_LOGLINE_PAT = re.compile(
  r'(?P<origin>\d+\.\d+\.\d+\.\d+) '
+ r'(?P<identd>-|\w*) (?P<auth>-|\w*) '
+ r'\[(?P<ts>(?P<date>[^\[\]:]+):(?P<time>\d+:\d+:\d+)) (?P<tz>[\-\+]?\d\d\d\d)\] '
+ r'"(?P<method>\w+) (?P<path>[\S]+) (?P<protocol>[^"]+)" (?P<status>\d+) 
(?P<bytes>-|\d+)'
+ r'( (?P<referrer>"[^"]*")( (?P<client>"[^"]*")( (?P<cookie>"[^"]*"))?)?)?\s*\Z'
)

# Patterns in the client field for sniffing out bots
BOT_TRACES = [
    (re.compile(r".*http://help\.yahoo\.com/help/us/ysearch/slurp.*"),
        "Yahoo robot"),
    (re.compile(r".*\+http://www\.google\.com/bot\.html.*"),
        "Google robot"),
    (re.compile(r".*\+http://about\.ask\.com/en/docs/about/webmasters.shtml.*"),
        "Ask Jeeves/Teoma robot"),
    (re.compile(r".*\+http://search\.msn\.com\/msnbot\.htm.*"),
        "MSN robot"),
    (re.compile(r".*http://www\.entireweb\.com/about/search_tech/speedy_spider/.*"),
        "Speedy Spider"),
    (re.compile(r".*\+http://www\.baidu\.com/search/spider_jp\.html.*"),
        "Baidu spider"),
    (re.compile(r".*\+http://www\.gigablast\.com/spider\.html.*"),
        "Gigabot robot"),
]

MAXRECORDS = 1000

# Apache's date/time format is very messy, so dealing with it is messy
# This class provides support for managing timezones in the Apache time field
# Reuses some code from: http://seehuhn.de/blog/52
class timezone(datetime.tzinfo):
    def __init__(self, name="+0000"):
        self.name = name
        seconds = int(name[:-2])*3600+int(name[-2:])*60
        self.offset = datetime.timedelta(seconds=seconds)

    def utcoffset(self, dt):
        return self.offset

    def dst(self, dt):
        return timedelta(0)

    def tzname(self, dt):
        return self.name

def parse_apache_date(date_str, tz_str):
    '''
    Parse the timestamp from the Apache log file, and return a datetime object
    '''
    tt = time.strptime(date_str, "%d/%b/%Y:%H:%M:%S")
    tt = tt[:6] + (0, timezone(tz_str))
    return datetime.datetime(*tt)

def bot_check(match_info):
    '''
    Return True if the matched line looks like a robot
    '''
    for pat, botname in BOT_TRACES:
        if pat.match(match_info.group('client')):
            return True
            break
    return False

entries = []

# enumerate lets you iterate over the lines in the file, maintaining a count variable
# itertools.islice lets you iterate over only a subset of the lines in the file
for count, line in enumerate(itertools.islice(sys.stdin, 0, MAXRECORDS)):
    match_info = COMBINED_LOGLINE_PAT.match(line)
    if not match_info:
        sys.stderr.write("Unable to parse log line\n")
        continue
    # If you want to include robot clients, comment out the next two lines
    if bot_check(match_info):
        continue
    entry = {}
    timestamp = parse_apache_date(match_info.group('ts'), match_info.group('tz'))
    timestamp_str = timestamp.isoformat()
    # To make Exhibit happy, set id and label fields that give some information
    # about the entry, but are unique across all entries (ensured by appending count)
    entry['id'] = match_info.group('origin') + ':' + timestamp_str + ':' + str(count)
    entry['label'] = entry['id']
    entry['origin'] = match_info.group('origin')
    entry['timestamp'] = timestamp_str
    entry['path'] = match_info.group('path')
    entry['method'] = match_info.group('method')
    entry['protocol'] = match_info.group('protocol')
    entry['status'] = match_info.group('status')
    entry['status'] += ' ' + httplib.responses[int(entry['status'])]
    if match_info.group('bytes') != '-':
        entry['bytes'] = match_info.group('bytes')
    if match_info.group('referrer') != '"-"':
        entry['referrer'] = match_info.group('referrer')
    entry['client'] = match_info.group('client')
    entries.append(entry)

print simplejson.dumps({'items': entries}, indent=4)

只需将 Apache 日志文件输入到 python apachelog2exhibit.py,就可以捕捉输出的 JSON。清单 6 是输出的 JSON 的一个简单的例子。

清单 6. Apache 日志转换成的 Exhibit JSON 示例
{
    "items": [
        {
            "origin": "208.111.154.16", 
            "status": "200 OK", 
            "protocol": "HTTP/1.1", 
            "timestamp": "2009-04-27T08:21:42-05:00", 
            "bytes": "2638", 
            "auth": "-", 
            "label": "208.111.154.16:2009-04-27T08:21:42-05:00:2", 
            "identd": "-", 
            "method": "GET", 
            "client": "Mozilla/5.0 (compatible; Charlotte/1.1; 
http://www.searchme.com/support/)", 
            "referrer": "-", 
            "path": "/uche.ogbuji.net", 
            "id": "208.111.154.16:2009-04-27T08:21:42-05:00:2"
        }, 
        {
            "origin": "65.103.181.249", 
            "status": "200 OK", 
            "protocol": "HTTP/1.1", 
            "timestamp": "2009-04-27T09:11:54-05:00", 
            "bytes": "6767", 
            "auth": "-", 
            "label": "65.103.181.249:2009-04-27T09:11:54-05:00:4", 
            "identd": "-", 
            "method": "GET", 
            "client": "Mozilla/5.0 (compatible; MJ12bot/v1.2.4; 
http://www.majestic12.co.uk/bot.php?+)", 
            "referrer": "-", 
            "path": "/", 
            "id": "65.103.181.249:2009-04-27T09:11:54-05:00:4"
        }
    ]
}

为了使用 Exhibit,可以创建一个 HTML 页面,该页面加载 Exhibit library JavaScript 和 JSON。清单 7 是一个非常简单的用于显示日志文件信息的 Exhibit HTML 页面。

清单 7. Exhibit 日志查看器的 HTML
<html>
<head>
  <title>Apache log entries</title>
  <link href="logview.js" type="application/json" rel="exhibit/data" />
    <script src="//static.simile.mit.edu/exhibit/api-2.0/exhibit-api.js"
          type="text/javascript"></script>
<script src="//static.simile.mit.edu/exhibit/extensions-2.0/time/time-extension.js"
          type="text/javascript"></script>
           
    <style>
       #main { width: 100%; }
       #timeline { width: 100%; vertical-align: top; }
       td { vertical-align: top; }
       .entry { border: thin solid black; width: 100%; }
       #facets  { padding: 0.5em; width: 20%; }
       .label { display: none; }
   </style>
</head> 
<body>
  <h1>Apache log entries</h1>
  <table id="main">
    <tr>
      <!-- The main display area for Exhibit -->
      <td ex:role="viewPanel">
        <div id="what-lens" ex:role="view"
             ex:viewClass="Exhibit.TileView"
             ex:label="What">
         </div>
        </div>
        <!-- Timeline view for the feed data -->
        <div id="timeline" ex:role="view"
             ex:viewClass="Timeline"
             ex:label="When"
             ex:start=".timestamp"
             ex:colorKey=".status"
             ex:topBandUnit="day"
             ex:topBandPixelsPerUnit="200"
             ex:topBandUnit="week">
         </div>
       </td>
       <!-- Boxes to allow users narrow down their view of feed data -->
       <td id="facets">
         <div ex:role="facet" ex:facetClass="TextSearch"></div>
         <div ex:role="facet" ex:expression=".path" ex:facetLabel="Path"></div>
         <div ex:role="facet" ex:expression=".referrer" ex:facetLabel="Referrer"></div>
         <div ex:role="facet" ex:expression=".origin" ex:facetLabel="Origin"></div>
         <div ex:role="facet" ex:expression=".client" ex:facetLabel="Client"></div>
         <div ex:role="facet" ex:expression=".status" ex:facetLabel="Status"></div>
       </td>
     </tr>
   </table>
</body>
</html>

图 2 显示那个简单的 HTML 源代码输出的一个内容丰富的视图。Exhibit 做了所有繁琐的工作。通过方框可以缩短显示的项。例如,可以分析一个源地址的访问模式。

图 2. Exhibit 日志文件查看器(“What” 视图)
运行清单 7 中的 HTML 后从服务器返回的 Apache 日志条目的屏幕截图
运行清单 7 中的 HTML 后从服务器返回的 Apache 日志条目的屏幕截图

图 3 显示另一个输出视图,时间线视图。要查看该视图,可单击默认视图顶部的 When

图 3. Exhibit 日志文件查看器(“When” 视图)
Timeline 视图截图,其中按时间组织各项并为每个项显示不同的细节
Timeline 视图截图,其中按时间组织各项并为每个项显示不同的细节

结束语

日志文件中的信息有很多用途。我曾使用该信息来确定应用某种新的 Web 技术(比如浏览器端 XSLT)是否合适,方法是检查正在使用 XSLT 浏览器的站点的非 spider 访问者的比例。我还曾用该信息为网络博客文章推荐有用的标签,方法是查看来自搜索引擎的 referrer 字段。有很多工具可以提供日志文件的统计信息和分析,但是现在还没有哪个工具能够提供日志的每种用法,所以对于 Web 架构师来说,学习直接处理日志是一中很有价值的技能。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=461610
ArticleTitle=使用 Web 服务器日志
publish-date=01142010