列出包含“ n”行或更少行的文件

时间:2018-10-03 17:27:55

标签: bash performance shell file awk

问题

在一个文件夹中,我想打印每个包含.txt行或更少行的n=27文件的名称。我能做

wc -l *.txt | awk '{if ($1 <= 27){print}}'

问题在于文件夹中的许多文件都是数百万行(并且这些行很长),因此命令wc -l *.txt非常慢。原则上,一个过程可以对行数进行计数,直到找到至少n行,然后进入下一个文件。

什么是更快的选择?

仅供参考,我在MAC OSX 10.11.6

尝试

这里是awk

的尝试
#!/bin/awk -f

function printPreviousFileIfNeeded(previousNbLines, previousFILENAME)
{
  if (previousNbLines <= n) 
  {
    print previousNbLines": "previousFILENAME
  }
}

BEGIN{
  previousNbLines=n+1
  previousFILENAME=NA
} 


{
  if (FNR==1)
  {
    printPreviousFileIfNeeded(previousNbLines, previousFILENAME)
    previousFILENAME=FILENAME
  }
  previousNbLines=FNR
  if (FNR > n)
  {
    nextfile
  }
}

END{
  printPreviousFileIfNeeded(previousNbLines, previousFILENAME)
}

可以称为

awk -v n=27 -f myAwk.awk *.txt

但是,代码无法打印出完全空白的文件。我不确定该如何解决,也不确定要使用的awk脚本。

10 个答案:

答案 0 :(得分:8)

对于下一个文件和ENDFILE,使用GNU awk:

awk -v n=27 'FNR>n{f=1; nextfile} ENDFILE{if (!f) print FILENAME; f=0}' *.txt

任何awk:

awk -v n=27 '
    { fnrs[FILENAME] = FNR }
    END {
        for (i=1; i<ARGC; i++) {
            filename = ARGV[i]
            if ( fnrs[filename] < n ) {
                print filename
            }
        }
    }
' *.txt

无论输入文件是否为空,这两者都可以工作。非gawk版本的警告与您当前其他awk答案的警告相同:

  1. 它依赖于同一文件名不出现多次(例如awk 'script' foo bar foo),而您希望它显示多次,并且
  2. 它依赖于arg列表中未设置任何变量(例如awk 'script' foo FS=, bar

gawk版本没有此类限制。

更新:

要测试上述GNU awk脚本和the GNU grep+sed script posted by xhienne之间的时间间隔,因为她说她的解决方案是faster than a pure awk script,所以我使用此脚本创建了10,000个输入文件,长度在0至1000行之间:

$ awk -v numFiles=10000 -v maxLines=1000 'BEGIN{for (i=1;i<=numFiles;i++) {numLines=int(rand()*(maxLines+1)); out="out_"i".txt"; printf "" > out; for (j=1;j<=numLines; j++) print ("foo" j) > out} }'

然后在它们上运行2个命令,并获得以下第3个运行计时结果:

$ time grep -c -m28 -H ^ *.txt | sed '/:28$/ d; s/:[^:]*$//' > out.grepsed

real    0m1.326s
user    0m0.249s
sys     0m0.654s

$ time awk -v n=27 'FNR>n{f=1; nextfile} ENDFILE{if (!f) print FILENAME; f=0}' *.txt > out.awk

real    0m1.092s
user    0m0.343s
sys     0m0.748s

两个脚本都产生相同的输出文件。上面是在cygwin上以bash运行的。我希望在不同的系统上,时序结果可能会有所不同,但差异始终可以忽略不计。


要打印10行,每行最多20个随机字符(请参阅注释):

$ maxChars=20
    LC_ALL=C tr -dc '[:print:]' </dev/urandom |
    fold -w "$maxChars" |
    awk -v maxChars="$maxChars" -v numLines=10 '
        { print substr($0,1,rand()*(maxChars+1)) }
        NR==numLines { exit }
    '
0J)-8MzO2V\XA/o'qJH
@r5|g<WOP780
^O@bM\
vP{l^pgKUFH9
-6r&]/-6dl}pp W
&.UnTYLoi['2CEtB
Y~wrM3>4{
^F1mc9
?~NHh}a-EEV=O1!y
of

要在awk内完成所有操作(这会慢得多):

$ cat tst.awk
BEGIN {
    for (i=32; i<127; i++) {
        chars[++charsSize] = sprintf("%c",i)
    }
    minChars = 1
    maxChars = 20
    srand()
    for (lineNr=1; lineNr<=10; lineNr++) {
        numChars = int(minChars + rand() * (maxChars - minChars + 1))
        str = ""
        for (charNr=1; charNr<=numChars; charNr++) {
            charsIdx = int(1 + rand() * charsSize)
            str = str chars[charsIdx]
        }
        print str
    }
}

$ awk -f tst.awk
Heer H{QQ?qHDv|
Psuq
Ey`-:O2v7[]|N^EJ0
j#@/y>CJ3:=3*b-joG:
?
^|O.[tYlmDo
TjLw
`2Rs=
!('IC
hui

答案 1 :(得分:4)

如果您使用的是GNU grep(不幸的是MacOSX> = 10.8提供了BSD grep,其-m-c选项act globally,而不是每个文件),您可能会发现另一种有趣的方法(比纯awk脚本还快):

grep -c -m28 -H ^ *.txt | sed '/:28$/ d; s/:[^:]*$//'

说明:

  • grep -c -m28 -H ^ *.txt输出每个文件的名称以及每个文件的行数,但读取的行数不得超过28行
  • sed '/:28$/ d; s/:[^:]*$//'删除至少包含28行的文件,并打印其他文件的文件名

备用版本:顺序处理而不是并行处理

res=$(grep -c -m28 -H ^ $files); sed '/:28$/ d; s/:[^:]*$//' <<< "$res"

基准化

爱德华·莫顿(Ed Morton)质疑我的说法,即该答案可能比awk更快。他在回答中添加了一些基准,尽管他没有给出任何结论,但我认为他发布的结果具有误导性,显示出我回答的挂钟时间更长,而与用户和系统时间无关。因此,这是我的结果。

首先是测试平台:

  • 运行Linux的四核Intel i5笔记本电脑,可能与OP的系统(Apple iMac)非常接近。

  • 一个全新的目录,包含100.000个文本文件,平均约400行,总计640 MB,该目录完全保留在我的系统缓冲区中。这些文件是使用以下命令创建的:

    for ((f = 0; f < 100000; f++)); do echo "File $f..."; for ((l = 0; l < RANDOM & 1023; l++)); do echo "File $f; line $l"; done > file_$f.txt; done
    

结果:

结论:

在撰写本文时,在类似于OP机器的常规Unix多核笔记本电脑上,此答案是最快的,可提供准确结果的答案。在我的机器上,它的速度是最快的awk脚本的两倍。

注意:

  • 平台为何重要?因为我的答案依赖于grepsed之间的并行处理。当然,为了获得公正的结果,如果您只有一个CPU内核(VM?)或操作系统在CPU分配方面的其他限制,则应该对备用(顺序)版本进行基准测试。

  • 很显然,您不能仅就挂墙时间得出结论,因为它取决于请求CPU的并发进程数与计算机上的内核数。因此,我添加了user + sys计时

  • 这些时间平均超过20次运行,但命令花费的时间超过1分钟(仅一次运行)除外

  • 对于所有少于10秒的答案,shell处理*.txt所花费的时间不可忽略,因此我对文件列表进行了预处理,将其放入变量中,并附加了我正在基准测试的命令的变量内容。

  • 所有答案给出的结果均相同。1.三元组的答案的结果中包含argv[0](“ awk”)(在我的测试中已固定); 2. kvantour的答案,其中仅列出了空文件(以-v n=27固定);和3.找不到空文件的find + sed答案(不固定)。

  • 我手头没有GNU sed 4.5,因此无法测试ctac_'s answer。这可能是所有方法中最快的,但也会丢失空文件。

  • python答案不会关闭其文件。我必须先做ulimit -n hard

答案 2 :(得分:3)

您可以尝试将awk移到下一个文件,只要行数超过27

awk -v n=27 'BEGIN{for (i=1; i<ARGC; i++) f[ARGV[i]]}
FNR > n{delete f[FILENAME]; nextfile}
END{for (i in f) print i}' *.txt

awk逐行处理文件,因此它不会尝试读取完整的文件来获取行数。

答案 3 :(得分:3)

这怎么样?

awk 'BEGIN { for(i=1;i<ARGC; ++i) arg[ARGV[i]] }
  FNR==28 { delete arg[FILENAME]; nextfile }
  END { for (file in arg) print file }' *.txt

我们将文件名参数列表复制到关联数组,然后从中删除所有具有第28行的文件。空文件显然不符合此条件,因此最后,我们剩下的所有行较少的文件,包括空行。

nextfile是许多Awk变体的通用扩展,然后在POSIX于2012年进行了编纂。 /或尝试使用GNU Awk。

答案 4 :(得分:3)

虽然似乎是最有趣的方式,但是tripleeanubhavaEd Morton的解决方案又是另外一种。在三元组和anubhava解决方案的哪里使用output.write(version, sizeof(VERSION_NR)); 语句,而Ed Morton的POSIX证明解决方案正在读取完整文件,而我提供的解决方案是不读取完整文件。

nextfile

答案 5 :(得分:1)

with sed(GNU sed)4.5:

sed -n -s '28q;$F' *.txt

答案 6 :(得分:1)

您可以在一些bash内联脚本的帮助下使用find

find -type f -exec bash -c '[ $(grep -cm 28 ^ "${1}") != "28" ] && echo "${1}"' -- {} \;

命令[ $(grep -cm 28 ^ "${1}") != "28" ] && echo "${1}"使用grep最多搜索28行(^)。如果该命令返回!=“ 28”,则文件必须少于28行。

答案 7 :(得分:0)

如果必须单独调用awk,请要求它在第28行停止:

for f in ./*.txt
do
  if awk 'NR > 27 { fail=1; exit; } END { exit fail; }' "$f"
  then
    printf '%s\n' "$f"
  fi
done

awk变量的默认值为零,因此,如果我们从不执行第28行,则退出代码为零,从而使if测试成功,并因此打印文件名。

答案 8 :(得分:0)

python -c "import sys; print '\n'.join([of.name for of in [open(fn) for fn in sys.argv[1:]] if len(filter(None, [of.readline() for _ in range(28)])) <= 27])" *.txt

答案 9 :(得分:0)

软件工具和 GNU sed v4.5 之前的旧版本)混搭:

find *.txt -print0 | xargs -0 -L 1 sed -n '28q;$F'

这会丢失0字节的文件,以包括这些文件,请这样做:

find *.txt \( -exec sed -n '28{q 1}' '{}' \; -or -size 0 \) -print

(由于某种原因,通过sed运行-execxargs 12%。)


sed代码从ctac's answer被盗。

注意:在我系统较旧的sed v4.4-2 上,q uit 命令与{{1} } switch不仅退出当前文件,而且还完全退出--separate。这意味着每个文件都需要一个sed的单独实例。