高效编辑大文件

Editing large files efficiently

我有一些大型日志文件,它们具有来自 RFC3162 (MMM dd HH:mm:ss) 的旧系统日志格式,我想将其转换为来自 RFC5424 (YYYY-mm-ddT[=) 的新系统日志格式36=] +TMZ)。我创建了以下 bash 脚本:

#!/bin/bash

#Loop over directories
for i in 
do
    echo "Processing directory $i"
    if [ -d $i ]
    then
        cd $i
        #Loop over log files inside the directory
        for j in *.2021
        do
            echo "Processing file $j"
            #Read line by line and perform transformation on dates and append to new file
            cat $j | \
                while read CMD; do
                    tmpdate=$(printf '%s\n' "$CMD" | awk -F" $i" 'BEGIN {ORS=""}; {print }')
                    newdate=$(date +'%Y-%m-%dT%H:%M:%S+02:00' -d "$tmpdate")

                    printf '%s\n' "$CMD" | sed 's/'"$tmpdate"'/'"$newdate"'/g' >> $j.new
                done
            mv $j.new $j
        done
        cd ..
    fi
done

但这需要很长时间才能执行,因为我有几百万行的文件(例如,邮件服务器上的日志可以追溯到一年多以前)。到目前为止,这已经 运行 好几天了,还有很多行需要解析:-)

所以两个问题。

  1. 为什么执行此脚本需要这么长时间?
  2. 有更快的方法吗?使用 GNU 实用程序之一(sed、awk 等),bash 或 python.

======== 编辑 =======

以下是旧格式的示例:

Feb  1 21:59:44 calendar os-prober: debug: running /usr/lib/os-probes/50mounted-tests on /dev/sda2
Feb  1 21:59:44 calendar 50mounted-tests: debug: /dev/sda2 type not recognised; skipping
Feb  1 21:59:44 calendar os-prober: debug: os detected by /usr/lib/os-probes/50mounted-tests

请注意,2 月和 1 日之间有 2 个 space,如果日期为 10 或更高,space 仅为 1,如

Feb 10 10:39:53 calendar os-prober: debug: running /usr/lib/os-probes/50mounted-tests on /dev/sda2

在新格式中它看起来像这样:

2021-02-01T21:59:44+02:00 calendar os-prober: debug: running /usr/lib/os-probes/50mounted-tests on /dev/sda2
2021-02-01T21:59:44+02:00 calendar 50mounted-tests: debug: /dev/sda2 type not recognised; skipping
2021-02-01T21:59:44+02:00 calendar os-prober: debug: os detected by /usr/lib/os-probes/50mounted-tests

TIA。

Why is this script taking such a long time to execute?

Bash 是一种脚本语言,旨在 运行 其他程序。因此,bash 本身作为一种语言并不是很快。但如果您反复启动其他进程,情况会变得更糟。启动一个过程是非常昂贵的。每次执行 sedawkdate 甚至 $(...)... | ... 之类的操作时,您都会启动一个进程。在一个循环中,这加起来。

比较 time for ((i=0; i<1000; ++i)); do true; donetime for ((i=0; i<1000; ++i)); do /bin/true; done。前者使用 bash 的内置命令,因此不会启动其他进程;它立即完成。后者使用外部程序,因此重复启动一个进程;在我的系统上需要 4.5 秒。

Is there a faster way to do this? Using one of GNU utils (sed, awk etc), bash or python.

是的。如果您在 python 中重写您的脚本,它会 运行 非常快,假设您使用 python 的内置函数,而不是重复调用 sp = subprocess.run(["date", ...], stdout=subprocess.PIPE])newDate = sp.stdout等等:)
这样写的时候,你会马上发现这不是有效的。 bash 使 运行 其他程序变得如此简单,以至于您常常会忘记在幕后完成的所有工作。

但是由于您将问题标记为 bash,让我们坚持使用脚本解决方案。

MMMMM 的转换(例如 Jan01)对于 sed 来说有点棘手。我们每个月都必须使用单独的替代品。幸运的是,月份总是在开头,所以我们可以将它与日期的其余部分分开替换。
要在一位数天前添加前导零,我们使用额外的替换。

sed -i.bak -E -e's/^Jan/01/;s/^Feb/02/;s/^Mar/03/;...' \
  -e's/^(..)  / 0/' \
  -e's/^([0-9]+)  ?([0-9]+) ([0-9]+:[0-9]+:[0-9]+)/2021--T+02:00/' */*.2021

第一个表达式可以自动生成:

monthNameToNumber=$(
   printf %s\n Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec |
   awk '{printf "s/^%s/%02d/;", [=11=], NR}'
)
sed -i.bak -E -e"$monthNameToNumber" \
  -e's/^(..)  / 0/' \
  -e's/^([0-9]+)  ?([0-9]+) ([0-9]+:[0-9]+:[0-9]+)/2021--T+02:00/' */*.2021

这将替换所有日志行开头的所有日期,在当前目录下的所有日志文件中的一个目录中。日志将就地修改。创建每个日志的备份,后缀为 .bak.

您正在用 sed 重写整个文件的次数与文件中的行数一样多。这是一个巨大但不幸的是相当常见的初学者反模式。

创建 sed 命令的管道也非常复杂且效率低下。

当结果以不同的顺序包含完全相同的信息时,您并不真的需要 date 在日期格式之间进行转换。试试像

awk -vyyyy="$(date +%Y)" 'BEGIN {
    split("Jan:Feb:Mar:Apr:May:Jun:Jul:Aug:Sep:Oct:Nov:Dec", _m, ":");
    for(i=1; i<=12; ++i) m[_m[i]] = i }
{ printf "%04i-%02i-%02iT%s+02:00 %s",
    yyyy, m[], , , substr([=10=], 17) }' "$j" >"$j.new"

演示:https://ideone.com/VBDqB8