使用 shell 脚本处理文本文件(超过10K行)真的很慢吗?

3

我有一个包含超过10K行记录的文件。每行中都有两个日期+时间信息。以下是一个示例: "aaa bbb ccc 170915 200801 12;ddd e f; g; hh; 171020 122030 10; ii jj kk;" 我想要过滤掉这两个日期之间相差少于30天的行。 以下是我的源代码:

#!/bin/bash
filename="$1"
echo $filename
touch filterfile
totalline=`wc -l $filename | awk '{print $1}'`
i=0
j=0
echo $totalline lines
while read -r line
do
  i=$[i+1]
  if [ $i -gt $[j+9] ]; then
    j=$i
    echo $i
  fi
  shortline=`echo $line | sed 's/.*\([0-9]\{6\}\)[ ][0-9]\{6\}.*\([0-9]\{6\}\)[ ][0-9]\{6\}.*/\1 \2/'`
  date1=`echo $shortline | awk '{print $1}'`
  date2=`echo $shortline | awk '{print $2}'`
  if [ $date1 -gt 700000 ]
  then
    continue 
  fi
  d1=`date -d $date1 +%s`
  d2=`date -d $date2 +%s`
  diffday=$[(d2-d1)/(24*3600)]
  #diffdays=`date -d $date2 +%s` - `date -d $date1 +%s`)/(24*3600)
  if [ $diffday -lt 30 ]
  then
    echo $line >> filterfile
  fi
done < "$filename"

我正在cygwin中运行它。处理10行大约需要10秒钟。我使用echo $i来显示进度。

这是因为我的脚本使用了错误的方法吗?


time 函数返回系统时间是什么?(即实际开销) - user202729
1
使用Shell循环处理文本为什么被认为是不好的实践?可能会有所帮助。 - Sundeep
2个回答

3
这个答案并没有回答你的问题,但提供了一个替代方法来编写你的shell脚本。关于你的问题,Sundeep的评论已经给出了答案: 为什么使用shell循环处理文本被认为是不好的实践? 此外,你应该知道每次调用sedawkechodate等命令时,都会请求系统执行需要加载到内存中的二进制文件等等。因此,如果你在循环中这样做,效率非常低下。 替代方案 awk程序通常用于处理包含时间戳信息的日志文件,指示特定日志记录的写入时间。 gawk通过添加时间处理函数扩展了awk标准。你感兴趣的是:

mktime(datespec [, utc-flag ])将日期字符串datespec转换为与使用systime()函数返回相同形式的时间戳。它类似于ISO C中同名的功能。参数datespec是一个字符串,格式为"YYYY MM DD HH MM SS [DST]"。该字符串由六个或七个数字组成,分别代表包括世纪在内的完整年份、从1到12的月份、从1到31的日、从0到23的小时、从0到59的分钟、从0到60的秒以及一个可选的夏令时标志。

这些数字的值不需要在指定范围内;例如,-1小时表示午夜前1小时。假定使用0起始的公历日历,其中年份0在年份1之前,年份-1在年份0之前。如果utc-flag存在且非零或非空,则假定时间处于UTC时区;否则,假定时间处于本地时区。如果DST夏令时标志为正,则假定时间处于夏令时;如果为零,则假定时间处于标准时间;如果为负(默认值),mktime()会尝试确定指定时间是否处于夏令时。

如果datespec不包含足够的元素或生成的时间超出范围,则mktime()返回-1。

作为您的日期格式为yymmdd HHMMSS,因此我们需要编写一个解析器函数convertTime。请注意,在此函数中,我们将传递yymmddHHMMSS形式的时间。此外,使用以空格分隔的字段,您的时间位于字段$4$5$11$12中。由于mktime将时间转换为从1970-01-01以来的秒数,因此我们只需要检查时间差是否小于30*24*3600秒即可。
awk 'function convertTime(t) {
       s="20"substr(t,1,2)" "substr(t,3,2)" "substr(t,5,2)" "
       s=  s substr(t,7,2)" "substr(t,9,2)" "substr(t,11,2)"
       return mktime(s)
     }
     { t1=convertTime($4$5); t2=convertTime($11$12)}
     (t2-t1 < 30*3600*24) { print }' <file>

如果你对实际的时间不感兴趣(你的sed命令删除了当天的实际时间),那么你可以采用以下方式进行调整:
awk 'function convertTime(t) {
       s="20"substr(t,1,2)" "substr(t,3,2)" "substr(t,5,2)" "
       s=  s "00 00 00"
       return mktime(s)
     }
     { t1=convertTime($4); t2=convertTime($11)}
     (t2-t1 < 30*3600*24) { print }' <file>

如果日期不在字段中,你可以使用match来查找它们:
awk 'function convertTime(t) {
       s="20"substr(t,1,2)" "substr(t,3,2)" "substr(t,5,2)" "
       s=  s substr(t,7,2)" "substr(t,9,2)" "substr(t,11,2)"
       return mktime(s)
     }
     { match($0,/[0-9]{6} [0-9]{6}/);
       t1=convertTime(substr($0,RSTART,RLENGTH));
       a=substr($0,RSTART+RLENGTH)
       match(a,/[0-9]{6} [0-9]{6}/)
       t2=convertTime(substr(a,RSTART,RLENGTH))}
     (t2-t1 < 30*3600*24) { print }' <file>

感谢您的回答。 对于我的情况,第一个日期和第二个日期之间的“文本”长度和内容是随机的。这就是为什么我使用“sed”从行中搜索这两个“日期”,然后进行比较的原因。 那么,是否有可能使用awk搜索日期字符串并将其分配给变量? - Tao Huang
我现在找到了一种处理它的方法。 我使用“sed”命令获取“日期”,并将其放置在“行”的开头。 然后,我可以使用您的代码来过滤出我需要的行。 这样,我就不需要使用while read了。 - Tao Huang
@TaoHuang,如果您不知道字段,我已经添加了一个纯awk的解决方案。 - kvantour
1
以防万一,在awk中,有人可以使用FPAT来捕获所需的字段,就像这样:awk '{print $1,$3}' FPAT="[0-9]{6}" input-file - George Vasiliou

1

通过一些修改,通常不考虑速度,我可以将处理时间减少50% - 这是非常多的:

#!/bin/bash
filename="$1"
echo "$filename"
# touch filterfile
totalline=$(wc -l < "$filename")
i=0
j=0
echo "$totalline" lines
while read -r line
do
  i=$((i+1))
  if (( i > ((j+9)) )); then
    j=$i
    echo $i
  fi
  shortline=($(echo "$line" | sed 's/.*\([0-9]\{6\}\)[ ][0-9]\{6\}.*\([0-9]\{6\}\)[ ][0-9]\{6\}.*/\1 \2/'))
  date1=${shortline[0]}
  date2=${shortline[1]}
  if (( date1 > 700000 ))
  then
    continue
  fi
  d1=$(date -d "$date1" +%s)
  d2=$(date -d "$date2" +%s)
  diffday=$(((d2-d1)/(24*3600)))
  # diffdays=$(date -d $date2 +%s) - $(date -d $date1 +%s))/(24*3600)
  if (( diffday < 30 ))
  then
    echo "$line" >> filterfile
  fi
done < "$filename"

一些注释:

# touch filterfile

好的 - 后面的CMD >> filterfile会覆盖这个文件并创建一个新的,如果它不存在。

totalline=$(wc -l < "$filename")

你不需要使用awk。如果wc没有看到文件名,那么文件名输出会被抑制。
将输出捕获到数组中:
  shortline=($(echo "$line" | sed 's/.*\([0-9]\{6\}\)[ ][0-9]\{6\}.*\([0-9]\{6\}\)[ ][0-9]\{6\}.*/\1 \2/'))
  date1=${shortline[0]}
  date2=${shortline[1]}

允许我们使用数组访问并节省了对awk的另一次调用。

在我的机器上,您的代码处理2880行大约需要42秒(在您的机器上是2880秒?),而使用我的代码处理相同的文件只需要大约19秒。

因此,我怀疑如果您不是在i486机器上运行它,那么cygwin可能会减慢速度。它是Windows上的Linux环境,不是吗?好吧,我在一个核心Linux系统上。也许您可以尝试使用Windows的gnu-utils-上次我为它们寻找时,它们被称为gnu-utils x32或类似的东西,也许现在已经有a64版本可用了。

接下来我会看一下日期计算-这可能也会减慢速度。

2880行并不算太多,所以我不认为我的SDD驱动器在游戏中扮演了很重要的角色。


感谢您详细的解释和建议。我会在我的电脑上尝试您的代码。;-) - Tao Huang

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