使用命令行工具按日期拆分access.log文件

19

我有一个大小约为35GB的Apache访问日志文件。 使用grep进行搜索已经不再是一种可行的选择,需要等待很长时间。

我想按照日期作为分割标准,将它拆分成许多小文件。

日期格式为[15/Oct/2011:12:02:02 +0000]。 有没有什么方法可以仅使用bash脚本、标准文本操作程序(如grep、awk、sed等)、管道和重定向来完成?

输入文件名为access.log。 我希望输出文件的格式为access.apache.15_Oct_2011.log(这样会起到效果,但在排序时不太好看。)

7个回答

24

使用 awk 的一种方法:

awk 'BEGIN {
    split("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec ", months, " ")
    for (a = 1; a <= 12; a++)
        m[months[a]] = sprintf("%02d", a)
}
{
    split($4,array,"[:/]")
    year = array[3]
    month = m[array[2]]

    print > FILENAME"-"year"_"month".txt"
}' incendiary.ws-2009

这将输出类似以下的文件:

incendiary.ws-2010-2010_04.txt
incendiary.ws-2010-2010_05.txt
incendiary.ws-2010-2010_06.txt
incendiary.ws-2010-2010_07.txt

在一个150MB大小的日志文件上,使用chepner的回答在一台3.4GHz 8核Xeon E31270处理了70秒,而使用这种方法只需5秒。

原始灵感来源:"如何按月份拆分现有的Apache日志文件?"


先生,您是对的。我刚刚测试了Perl解决方案,awk解决方案快了3倍。我怀疑这是因为awk示例没有使用正则表达式而是简单的字符串分割,这可能更有效率。标记为已接受的答案。 - mr.b
哦,而且我现在绝对会在生产环境中使用它来处理20 GB的文件,没有任何问题。在我的系统上大约需要每分钟2 GB的速度。 - Theodore R. Smith
这里的表现也相似:大约1分钟/2.5GB。谢谢! - mr.b
唯一的问题是,我还需要提取日期 - 我的日志文件大小现在已经超过了400MB.. 你能修改脚本以包括日期吗? - mr.b
“split($4,array,"[:/]");”指令不应该在“year = array[3]”之前吗? - Ceki
@TheodoreR.Smith,你的输出文件名有误,因为你使用了两位数字sprintf("%02d", a)来编码month变量。请修复你的输出文件名以避免混淆。 - SebMa

10

纯Bash实现,只需一次遍历访问日志:

while read; do
    [[ $REPLY =~ \[(..)/(...)/(....): ]]

    d=${BASH_REMATCH[1]}
    m=${BASH_REMATCH[2]}
    y=${BASH_REMATCH[3]}

    #printf -v fname "access.apache.%s_%s_%s.log" ${BASH_REMATCH[@]:1:3}
    printf -v fname "access.apache.%s_%s_%s.log" $y $m $d

    echo "$REPLY" >> $fname
done < access.log

5
我的回答方法速度显著更快:对于一个150 MB的日志文件,在3.4 GHz 8核Xeon E31270上,我的方法只需要5秒,而其他方法需要70秒。 - Theodore R. Smith
然而,这个答案会按天创建日志文件而不是按月创建。这样做会减少一些工作量,难怪速度更快。 - i.am.michiel
@i.am.michiel 这个程序运行较慢的原因是,在awk中迭代输入要比在bash中快得多;输出文件的数量并不是真正相关的因素。 - chepner

5
这里有一个awk版本,可以输出按字典顺序排序的日志文件。
一些效率的提升:只需一次遍历即可完成所有操作,仅在fname与之前不同时生成fname,在切换到新文件时关闭fname(否则可能会用尽文件描述符)。
awk -F"[]/:[]" '
BEGIN {
  m2n["Jan"] =  1;  m2n["Feb"] =  2; m2n["Mar"] =  3; m2n["Apr"] =  4;
  m2n["May"] =  5;  m2n["Jun"] =  6; m2n["Jul"] =  7; m2n["Aug"] =  8;
  m2n["Sep"] =  9;  m2n["Oct"] = 10; m2n["Nov"] = 11; m2n["Dec"] = 12;
}
{
  if($4 != pyear || $3 != pmonth || $2 != pday) {
    pyear  = $4
    pmonth = $3
    pday   = $2

    if(fname != "")
      close(fname)

    fname  = sprintf("access_%04d_%02d_%02d.log", $4, m2n[$3], $2)
  }
  print > fname
}' access-log

4
我结合了Theodore和Thor的解决方案,利用了Thor的效率改进和每日文件,但保留了以组合格式文件支持IPv6地址的原始支持。
awk '
BEGIN {
  m2n["Jan"] =  1;  m2n["Feb"] =  2; m2n["Mar"] =  3; m2n["Apr"] =  4;
  m2n["May"] =  5;  m2n["Jun"] =  6; m2n["Jul"] =  7; m2n["Aug"] =  8;
  m2n["Sep"] =  9;  m2n["Oct"] = 10; m2n["Nov"] = 11; m2n["Dec"] = 12;
}
{
  split($4, a, "[]/:[]")
  if(a[4] != pyear || a[3] != pmonth || a[2] != pday) {
    pyear  = a[4]
    pmonth = a[3]
    pday   = a[2]

    if(fname != "")
      close(fname)

    fname  = sprintf("access_%04d-%02d-%02d.log", a[4], m2n[a[3]], a[2])
  }
  print >> fname
}'

1
这真的很令人印象深刻!谢谢。 - Theodore R. Smith

4

Perl 来解救了我们:

cat access.log | perl -n -e'm@\[(\d{1,2})/(\w{3})/(\d{4}):@; open(LOG, ">>access.apache.$3_$2_$1.log"); print LOG $_;'

嗯,它并不是完全符合“标准”的操作程序,但它仍然是为文本操作而设计的。

我还改变了文件名中参数的顺序,这样文件的命名方式就像access.apache.yyyy_mon_dd.log,更易于排序。


1

有点丑陋,这就是Bash的风格:

    for year in 2010 2011 2012; do
       for month in jan feb mar apr may jun jul aug sep oct nov dec; do
           for day in 1 2 3 4 5 6 7 8 9 10 ... 31 ; do
               cat access.log | grep -i $day/$month/$year > $day-$month-$year.log
            done
        done
     done

非常聪明,谢谢 ;) 这对于小文件(文件大小小于内存量)非常有效,因为它大约循环遍历整个文件1,116次 :) - mr.b
2
非常正确,这不是一个高效的脚本。它适合偶尔使用。谢谢! - ncultra
1
将外层循环展开并在两个步骤中处理文件可能会更快- 在第一步中,按年份将文件拆分为条目。然后,在第二步中,处理每个年份的文件并按日期拆分条目。甚至可以将第二个循环展开并在三个步骤中处理文件可能会更快。 - ncultra
在使用grep查找日期时,会意外删除堆栈跟踪等内容,即任何不包含日期的行都将被删除。通常这些行才是最有趣的。 - nby

1

我对Theodore的回答进行了轻微改进,以便在处理非常大的日志文件时可以看到进展。

#!/usr/bin/awk -f

BEGIN {
    split("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec ", months, " ")
    for (a = 1; a <= 12; a++)
        m[months[a]] = a
}
{
    split($4, array, "[:/]")
    year = array[3]
    month = sprintf("%02d", m[array[2]])

    current = year "-" month
    if (last != current)
        print current
    last = current

    print >> FILENAME "-" year "-" month ".txt"
}

此外我发现我需要使用gawk(如果你没有,可以brew install gawk)才能在Mac OS X上运行。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接