【问题标题】:Is it really slow to handle text file(more than 10K lines) with shell script?使用 shell 脚本处理文本文件(超过 10K 行)真的很慢吗?
【发布时间】:2018-09-22 05:16:43
【问题描述】:

我有一个包含超过 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"

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

是因为我在脚本中使用了错误的方式吗?

【问题讨论】:

标签: bash shell text awk


【解决方案1】:

此答案不回答您的问题,但为您的 shell 脚本提供了另一种方法。 Sundeep 的评论给出了您问题的答案:

Why is using a shell loop to process text considered bad practice?

此外,您应该知道,每次调用sedawkechodate,...您都在请求系统执行需要加载到内存中的二进制文件等.所以如果你在一个循环中这样做,它是非常低效的。

替代解决方案

awk 程序通常用于处理包含时间戳信息的日志文件,指示写入特定日志记录的时间。 gawk 扩展了 awk 标准的时间处理功能。您感兴趣的是:

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

这些数字的值不必在指定的范围内; 例如,一小时 -1 表示午夜前 1 小时。这 假定原点为零公历,前一年为 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 搜索日期字符串并将其分配给变量?
  • 我现在找到了处理它的方法。我使用“sed”命令获取“日期”并将其放在“行”的开头。然后我可以使用你的代码过滤掉我需要的行。那我就不用while read了
  • @TaoHuang 如果您不知道这些字段,我已经添加了纯awk 的解决方案。
  • 以防万一,在 awk 中有人可以使用 FPAT 来捕获像这样的必填字段:awk '{print $1,$3}' FPAT="[0-9]{6}" input-file
【解决方案2】:

通过一些修改,通常不考虑速度,我可以将处理时间减少 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 &gt;&gt; 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 驱动器在游戏中扮演了重要角色。

【讨论】:

  • 感谢您的详细解释和建议。我会在我的电脑上试试你的代码。 ;-)
猜你喜欢
  • 1970-01-01
  • 2023-04-06
  • 2011-01-28
  • 1970-01-01
  • 1970-01-01
  • 2014-05-04
  • 2016-06-01
  • 2017-12-05
  • 1970-01-01
相关资源
最近更新 更多