本文的作者是 Nikolay Koldunov,本文原文是
Apache log analysis with Pandas

注本文的图有问题,没法引用,还是去原文看下,这里作为一个引子。

%pylab inline

欢迎来到 pylab,一个基于 matplotlib 的 Python 环境【backend: module://IPython.kernel.zmq.pylab.backend_inline】。想要了解更多信息,请键入 \’help(pylab)\’。

在这个笔记中,我们将展示一个使用 pandas 分析 Apache 访问日志的简单示例。这是我第一次使用 pandas,并且我确定会有更好以及更有效率的方式来做这里展示的事情。所以评论,建议和修正我的蹩脚英语是非常欢迎的。你可以给我发送邮件或者是为这个笔记的 github 创建一个 PR。

加载和解析数据

我们将需要 apachelog 模块,用来解析日志。我们也需要知道设置在 Apache 配置中的日志格式。在我的案例中,我没有访问 Apache 配置,但是主机托管服务提供商在他的帮助页提供了日志格式的描述。下面是它自己的格式以及每个元素的简单描述:

format = r\'%V %h  %l %u %t \\\"%r\\\" %>s %b \\\"%i\\\" \\\"%{User-Agent}i\\\" %T\'

这里(大部分拷贝自这个 SO 文章):

%V          - 根据 UseCanonicalName 设置的服务器名字
%h          - 远程主机(客户端 IP)
%l          - identity of the user determined by identd (not usually used since not reliable)
%u          - 由 HTTP authentication 决定的 user name
%t          - 服务器完成处理这个请求的时间
%r          - 来自客户端的请求行。 (\"GET / HTTP/1.0\")
%>s         - 服务器端返回给客户端的状态码(200, 404 等等。)
%b          - 响应给客户端的响应报文大小 (in bytes)
\\\"%i\\\"      - Referer is the page that linked to this URL.
User-agent  - the browser identification string
%T          - Apache 请求时间
In [3]:import apachelog, sys

设置格式:

In [4]:fformat = r\'%V %h %l %u %t \\\"%r\\\" %>s %b \\\"%i\\\" \\\"%{User-Agent}i\\\" %T\'

创建一个解析器:

In [5]:p = apachelog.parser(fformat)

简单字符串:

koldunov.net 85.26.235.202 - - [16/Mar/2013:00:19:43 +0400] \"GET /?p=364 HTTP/1.0\" 200 65237 \"http://koldunov.net/?p=364\" \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11\" 0
In [6]:sample_string = \'koldunov.net 85.26.235.202 - - [16/Mar/2013:00:19:43 +0400] \"GET /?p=364 HTTP/1.0\" 200 65237 \"http://koldunov.net/?p=364\" \"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11\" 0\'
In [7]:data = p.parse(sample_string)
In [8]:data
Out[8]:
{\'%>s\': \'200\',
 \'%T\': \'0\',
 \'%V\': \'koldunov.net\',
 \'%b\': \'65237\',
 \'%h\': \'85.26.235.202\',
 \'%i\': \'http://koldunov.net/?p=364\',
 \'%l\': \'-\',
 \'%r\': \'GET /?p=364 HTTP/1.0\',
 \'%t\': \'[16/Mar/2013:00:19:43 +0400]\',
 \'%u\': \'-\',
 \'%{User-Agent}i\': \'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11\'}

这就是解释器的工作。现在让我们加载真实世界的数据(示例文件位于这里和这里):

In [9]:log = open(\'access_log_for_pandas\').readlines()

解析每一行,并且创建一个字典列表:

In [10]:
log_list = []
for line in log:
       try:
          data = p.parse(line)
       except:
          sys.stderr.write(\"Unable to parse %s\" % line)
       data[\'%t\'] = data[\'%t\'][1:12]+\' \'+data[\'%t\'][13:21]+\' \'+data[\'%t\'][22:27]

       log_list.append(data)

我们不得不调整时间格式位,否则的话 pandas 将不能解析它。

创建和调整数据帧

这将创建一个字典列表,可以转化到一个数据帧:

import pandas as pd
import numpy as np
from pandas import Series, DataFrame, Panel
df = DataFrame(log_list)

展示数据帧的前两行:

df[0:2]
%>s %T %V %b %h %i %l %r %t %u %{User-Agent}i
0 200 0 www.oceanographers.ru 26126 109.165.31.156 GET /index.php?option=com_content&task=section… 16/Mar/2013 08:00:25 +0400 Mozilla/5.0 (Windows NT 6.1; rv:19.0) Gecko/20…
1 200 0 www.oceanographers.ru 10532 109.165.31.156 http://www.oceanographers.ru/index.php?option=… GET /templates/ja_procyon/css/template_css.css… 16/Mar/2013 08:00:25 +0400 Mozilla/5.0 (Windows NT 6.1; rv:19.0) Gecko/20…

我们不准备使用所有的数据,因此让我们删除一些列:

del df[\'%T\']; del df[\'%V\']; del df[\'%i\']; del df[\'%l\']; del df[\'%u\']; del df[\'%{User-Agent}i\']

并且把这些列重命名成人类可理解的格式:

df = df.rename(columns={\'%>s\': \'Status\', \'%b\':\'b\', 
                        \'%h\':\'IP\', \'%r\':\'Request\', \'%t\': \'Time\'})

结果数据帧的前 5 行:

df.head()
Status b IP Request Time
0 200 26126 109.165.31.156 GET /index.php?option=com_content&task=section… 16/Mar/2013 08:00:25 +0400
1 200 10532 109.165.31.156 GET /templates/ja_procyon/css/template_css.css… 16/Mar/2013 08:00:25 +0400
2 200 1853 109.165.31.156 GET /templates/ja_procyon/switcher.js HTTP/1.0 16/Mar/2013 08:00:25 +0400
3 200 37153 109.165.31.156 GET /includes/js/overlib_mini.js HTTP/1.0 16/Mar/2013 08:00:25 +0400
4 200 3978 109.165.31.156 GET /modules/ja_transmenu/transmenuh.css HTTP/1.0 16/Mar/2013 08:00:25 +0400

转换时间列成 datetime 格式并做一个索引出来(pop 将丢弃原始的 Time 列):

df.index = pd.to_datetime(df.pop(\'Time\'))

Status 变量是一个 string 类型,因此我们需要把它转换成 int:

df[\'Status\'] = df[\'Status\'].astype(\'int\')

一些 b 列的行包含 \’-\’ 字符,我们需要使用 astype 转换它们:

df[\'b\'][93]
Out[19]:
\'-\'

我们可以为该列使用一个通用的函数,它们将把所有的破折号转换成 NaN,并且剩余的转换成 floats,另外把 bytes 转换成 megabytes:

def dash2nan(x):
    if x == \'-\':
        x = np.nan
    else:
        x = float(x)/1048576.

    return x
df[\'b\'] = df[\'b\'].apply(dash2nan)

我相信有一个更优雅的方式来做到这一点。

流量分析

首先,最简单的散点:从该网站的出口流量:

df[\'b\'].plot()

看起来在早上 9 点左右有人从网站下载了一些大的东西。

但是实际上你想知道的第一件事是你的网站有多少的访问量,以及它们的时间分布。我们从 b 变量的 5 分钟间隔重新取样,并计算每个时间跨度的请求数。实际上,在这个示例中不管我们使用哪个变量,这些数字将表明有多少次请求该网站的信息请求。

df_s = df[\'b\'].resample(\'5t\', how=\'count\')
df_s.plot()
Out[23]:

![此处输入图片的描述][8]

我们不仅仅计算每个时间的请求数,也计算每个时间段的总流量:

df_b = df[\'b\'].resample(\'10t\', how=[\'count\',\'sum\'])
df_b[\'count\'].plot( color=\'r\')
legend()
df_b[\'sum\'].plot(secondary_y=True)
Out[24]:

![此处输入图片的描述][9]

正如你所看到的,服务器请求数和流量是不一致的,相关性其实并不是非常高:

df_b.corr()

|-| count| sum
|count| 1.000000| 0.512629
|sum| 0.512629| 1.000000

我们可以仔细看下早高峰:

df_b[\'2013-03-16 6:00\':\'2013-03-16 10:00\'][\'sum\'].plot()
Out[26]:

![此处输入图片的描述][10]

看起来流量峰值是由一个请求引起的。让我们找出这个请求。选择所有响应大于 20 Mb 的请求:

df[df[\'b\']>20]
Status b IP Request
Time
2013-03-16 09:02:59 200 21.365701 77.50.248.20 GET /books/Bondarenko.pdf HTTP/1.0

这是一本书的 pdf 文件,这就解释了在 2013-03-16 09:02:59 的流量出口峰值。

接近 20 Mb 是一个大的请求(至少对于我们网站),但是服务器响应的典型大小是?响应大小(小于 20Mb)的立方图看起来像这样:

cc = df[df[\'b\']<20]
cc.b.hist(bins=10)
Out[28]:

![此处输入图片的描述][11]

因此,大部分的文件是小于 0.5 Mb。实际上它们甚至更小:

cc = df[df[\'b\']<0.3]
cc.b.hist(bins=100)
Out[29]:

![此处输入图片的描述][12]