高效编辑大文件



我有一些大型日志文件,其中包含RFC3162 (MMM dd HH:mm:ss)的旧syslog格式日期,我想将其更改为RFC5424 (YYYY-mm-ddTHH:mm:ss +TMZ)的新syslog格式日期。我已经创建了以下bash脚本:

#!/bin/bash
#Loop over directories
for i in $1
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 '%sn' "$CMD" | awk -F" $i" 'BEGIN {ORS=""}; {print $1}')
newdate=$(date +'%Y-%m-%dT%H:%M:%S+02:00' -d "$tmpdate")
printf '%sn' "$CMD" | sed 's/'"$tmpdate"'/'"$newdate"'/g' >> $j.new
done
mv $j.new $j
done
cd ..
fi
done

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

有两个问题

  1. 为什么这个脚本需要这么长的时间来执行?
  2. 有更快的方法来做到这一点吗?使用一个GNU utils (sed, awk等),bash或python。

======== EDIT =======

以下是旧格式的示例:

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

请注意,在Feb和1之间有2个空格,如果日期是10或更高,则空格仅为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。

使用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[$1], $2, $3, substr($0, 17) }' "$j" >"$j.new"

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

为什么这个脚本需要这么长的时间来执行?

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秒。

是否有更快的方法来做到这一点?使用一个GNU utils (sed, awk等),bash或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/^(..)  /1 0/' 
-e's/^([0-9]+)  ?([0-9]+) ([0-9]+:[0-9]+:[0-9]+)/2021-1-2T3+02:00/' */*.2021

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

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

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

最新更新