高效编辑大文件

Posted

技术标签:

【中文标题】高效编辑大文件【英文标题】:Editing large files efficiently 【发布时间】:2021-10-24 11:29:00 【问题描述】:

我有一些大型日志文件具有来自 RFC3162 (MMM dd HH:mm:ss) 的旧 syslog 格式日期,我想将其更改为来自 RFC5424 (YYYY-mm-ddTHH:mm:ss) 的新 syslog 格式日期+TMZ)。我创建了以下 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 '%s\n' "$CMD" | awk -F" $i" 'BEGIN ORS=""; print $1')
                    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

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

那么两个问题。

    为什么这个脚本需要这么长时间才能执行? 有更快的方法吗?使用 GNU utils(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个空格,如果日期是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。

【问题讨论】:

您可能想要for i in "$@" 而不是for i in $1 - 根据定义,$1 只能包含一个项目。 好吧,从技术上讲,未引用的 $1 会进行分词和全局扩展,因此 for i in $1工作(或break,具体取决于您的查看)如果您致电 script.sh "dir1 dir2 dir3"script.sh "*"。是的,但是for i in "$@"; do 或只是for i; do 将是处理多个参数的明智方式。 啊,是的,那是$1 是因为我一次给脚本一个文件。由于 $things 的原因,我必须在笔记本电脑上执行此操作,并且解析一些文件需要很长时间,并且我需要在完成工作后关闭笔记本电脑的电源,然后我在尝试找出更好的方法时这样做了。 【参考方案1】:

您使用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

【讨论】:

感谢您,它运行良好。只是一个更正,sed 从不接触文件,它从stdin 获取管道并将其输出到新文件的末尾(或者它可能是您所指的新文件的附加内容?) 是的,您读取整个文件的次数与来自(useless!) cat 的输入行一样多。相关:***.com/questions/65538947/…【参考方案2】:

为什么这个脚本需要这么长时间才能执行?

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

比较 time for ((i=0; i&lt;1000; ++i)); do true; donetime for ((i=0; i&lt;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-\2T\3+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-\2T\3+02:00/' */*.2021

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

【讨论】:

-E 选项到 sed 不可移植。 OP 标记了这个linux,所以它可能对他们有用(尽管看到初学者将 Mac 和 Windows 问题错误地标记为 Linux 的情况并不少见,无论出于何种原因),但它不起作用,例如在开箱即用的 Mac 上 - 尽管 sed -r 在那里的工作方式大致相同。 @tripleee -i 也不是。但是现在,我知道的每个sed 实现都支持-E-i suffix。实际上-E来自BSD,而GNU使用-r并在后来添加-E作为同义词(在commit message中,作者甚至声称-E被添加到POSIX,但我找不到它在那里)。由于 macOS 使用来自 BSD 的sed,macOS supports -E and -i suffix 这看起来更有希望,不过是一个问题。如果日志的消息部分(即时间戳之后的文本)包含类似something 1 something else 的内容,那么新的正则表达式将取代它,是否有任何危险? @proxymoxy 不,至少如果每行都以日期开头,则不会。我使用^(行首)来防止这种情况。 s/^(..) /\1 0/' 只处理每行的前四个字符。 @socowi 太好了,这正是医生所要求的 :-)

以上是关于高效编辑大文件的主要内容,如果未能解决你的问题,请参考以下文章

Java高效读取大文件

Java高效读取大文件

大文本文件的高效迭代

两个大文本文件的高效文件比较

Java高效读取大文件

Java高效地读取大文件