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

Posted

技术标签:

【中文标题】列出包含“n”行或更少行的文件【英文标题】:List files that contain `n` or fewer lines 【发布时间】:2019-03-09 00:16:57 【问题描述】:

问题

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

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 脚本是否可行。

【问题讨论】:

head -n27 *txt |厕所-l | awk 'if ($1 @newbie 那也不会处理空文件。 它在 bash linux 上运行,它返回 0 ?头-n27 空.txt | wc -l 0 哦,对,但是你需要循环运行它,一次一个文件。 【参考方案1】:

使用 GNU awk 获取 nextfile 和 ENDFILE:

awk -v n=27 'FNR>nf=1; nextfile ENDFILEif (!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 答案相同:

    它依赖于相同的文件名不会出现多次(例如awk 'script' foo bar foo)并且您希望它显示多次,并且 它依赖于 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 'BEGINfor (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>nf=1; nextfile ENDFILEif (!f) print FILENAME; f=0' *.txt > out.awk

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

两个脚本产生了相同的输出文件。以上是在 cygwin 上的 bash 中运行的。我预计在不同的系统上,计时结果可能会有所不同,但差异总是可以忽略不计。


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

$ 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\
vPl^pgKUFH9
-6r&]/-6dlpp W
&.UnTYLoi['2CEtB
Y~wrM3>4
^F1mc9
?~NHha-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 HQQ?qHDv|
Psuq
Ey`-:O2v7[]|N^EJ0
j#@/y>CJ3:=3*b-joG:
?
^|O.[tYlmDo
TjLw
`2Rs=
!('IC
hui

【讨论】:

@stack0114106 我刚刚更新了我的答案以显示一些用于生成随机字符串的选项。 感谢@Ed..您从 32 开始构建 ascii 数组并确保输出中没有控制字符。 我真的不知道 cygwin 对 CPU 的限制,抱歉。听起来您已经完成了一些全面的基准测试 - 太好了,感谢您这样做。 @EdMorton 我用您的脚本创建的数据集尝试了 any awk 版本,但它没有输出任何内容。它确实运行了大约 2 秒(与在我的笔记本电脑上运行 0.3 秒的 gnu awk 版本相反)。 @JamesBrown 我没有在命令行上显示-v n=27,我现在已经添加了。【参考方案2】:

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

结果:

grep+sed (this answer) : 561 ms elapsed, 586 ms user+sys grep+sed(此答案,顺序版本):经过 678 毫秒,688 毫秒用户+系统 awk (Ed Morton):经过 1050 毫秒,1036 毫秒用户+系统 awk (tripleee):经过 1137 毫秒,1123 毫秒用户+系统 awk (anubhava):经过 1150 毫秒,1137 毫秒用户+系统 awk (kvantour):经过 1280 毫秒,1266 毫秒用户+系统 python (Joey Harrington):经过 1543 毫秒,1537 毫秒用户+系统 find+xargs+sed (agc):91 秒过去了,10 秒用户+系统 for+awk (Jeff Schaller): 247 秒过去,83 秒用户+系统 find+bash+grep (hek2mgl): 356 秒过去了,116 秒用户+系统

结论:

在撰写本文时,在类似于 OP 机器的普通 Unix 多核笔记本电脑上,这个答案是最快的,可以给出准确的结果。在我的机器上,它的速度是最快的 awk 脚本的两倍。

注意事项:

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

显然,您不能仅靠墙时间得出结论,因为它取决于请求 CPU 的并发进程数与机器上的内核数。因此我添加了用户+系统时间

这些时间平均超过 20 次运行,除非命令运行时间超过 1 分钟(仅运行一次)

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

所有答案都给出了相同的结果,除了 1. Tripleee 的答案,其结果中包含 argv[0] ("awk")(在我的测试中已修复); 2. kvantour的回答只列出了空文件(用-v n=27修复);和 3. 丢失空文件的 find+sed 答案(未修复)。

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

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

【讨论】:

恕我直言,这是一个很好的解决方案,所以 +1 但是你声称你的 grep+sed 解决方案比纯 awk 脚本更快,我创建了一个包含 10,000 个文件的测试集,每个文件从 0 到1000 行来测试你的 grep+sed 和我的 awk,并将结果发布到 my answer。 我严重质疑你的结果(不是你的诚实,只是结果)。您应该真正在真正的多核 Unix 平台上进行测试,而不是在 Cygwin 上进行测试。 10 年前我已经注意到了类似的结果。 无论好坏,我测试的 cygwin 平台是我运行大部分 shell 脚本的平台,所以我得到的计时结果对我个人来说是唯一重要的。感谢您在其他平台上进行基准测试。 啊,很好看。 $ time res=$(grep -c -m28 -H ^ *.txt); sed '/:28$/ d; s/:[^:]*$//' &lt;&lt;&lt; "$res" &gt; out.sed; 输出 real 0m0.889s user 0m0.248s sys 0m0.624s -m 选项无法满足您对我的 Mac 的期望。它只打印前 28 个匹配项,而不是每个文件的前 28 个;使用-c,它只打印第一个输入文件的数字 28。【参考方案3】:

你可以试试这个awk,只要行数超过27,它就会移动到下一个文件:

awk -v n=27 'BEGINfor (i=1; i<ARGC; i++) f[ARGV[i]]
FNR > ndelete f[FILENAME]; nextfile
ENDfor (i in f) print i' *.txt

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

【讨论】:

如果我没记错的话,这样会打印空文件失败,对吗? 是的,这是正确的。你也想打印空文件名吗? 是的,请:) 他们的行数少于n。请注意,在帖子中强调了我在列出空文件方面的尝试也失败了。 Tripleee 提出了一个解决方案,涉及在 BEGIN 块中设置 has 映射。谢谢! 哇,刚刚从我的朋友 Tripleee 那里得到了一个非常相似的答案【参考方案4】:

怎么样?

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 变体中的常见扩展,然后在 2012 年被 POSIX 编纂。如果您需要它在真正古老的恐龙操作系统(或者,天哪,可能是 Windows)上工作,祝你好运,和/或试试 GNU awk。

【讨论】:

我很困惑地看到这与 Anubhava 的答案有多么相似,这似乎是我正在撰写的。相似之处是偶然的,诚实的。不同的是,这也应该捕获空文件。 只是想知道您关于 nextfile 是 POSIX 的一部分的声明。我在 posix 标准中找不到对此的任何引用(我发现的唯一提及的是 GNU awk 页面上的链接)。你确定这个说法吗? POSIX 信息来自GNU Awk documentation for nextfile;,它实际上表示“在 2012 年接受包含”,所以这可能意味着该过程当时已启动? @kvantour 感谢ARGC 修复;更新了脚本。 @kvantour 修复不正确。 ARGV[0] 是“awk”。那应该是for(i=1; i&lt;ARGC; ++i)【参考方案5】:

虽然awk 似乎是最有趣的继续方式,但这是现有解决方案triplee、anubhava 和Ed Morton 的另一种解决方案。三元组和 anubhava 的解决方案在哪里使用nextfile 声明和 Ed Morton 的 POSIX 证明解决方案正在读取完整文件,我提供了一个不读取完整文件的解决方案。

awk -v n=27 'BEGIN for(i=1;i<ARGC;++i) 
                       j=0; fname=ARGV[i];
                       while( ((getline < fname) > 0 ) && j<=n)  j++ 
                       if(j<=n) print fname; close(fname)
                  
                  exit
             ' *.txt

【讨论】:

你应该把它设为while( ((getline &lt; fname) &gt; 0) &amp;&amp; j&lt;=n),这样它就不会在getline失败时继续循环。见awk.freeshell.org/AllAboutGetline。否则,虽然 - 很好和适当地使用 getline!当然,它仍然依赖于在 arg 列表中没有设置变量 awk '...' file1 FS=, file2。实际上 - 在这种情况下,假设您的工作目录中没有名为 FS=, 的文件,那么当 FS=, 出现在 arg 列表中时,getline 错误可能会帮助您避免输出错误的内容:-)。 @Ed。这很有趣。我必须将 n=27 更改为 -v n=27 才能使该脚本正常工作,但无法理解原因。 与使用-v 设置变量时不同,当您在arg 列表中设置变量时,变量不会在BEGIN 部​​分设置,n=27 仅在处理 BEGIN 部​​分后生效所以n 在你的循环中不会被填充。 @EdMorton 没错,它就像 n=0 一样工作。但为什么它似乎对 kvantour 有效? @EdMorton 我确实对其进行了测试,但是将值硬编码,之后我添加了 n=27 以删除硬编码的值。我的错。但是是的,n=27 是无效的,应该在它前面作为-v n=27【参考方案6】:

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

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

命令[ $(grep -cm 28 ^ "$1") != "28" ] &amp;&amp; echo "$1" 使用 grep 最多搜索行首 (^) 28 次。如果该命令返回 != "28",则该文件的行数必须少于 28 行。

【讨论】:

您正在为找到的每个文件执行一个 bash 和一个 grep 进程;这太慢了! (请参阅我添加到答案中的基准)。如果您更改答案以一次处理多个文件,我很乐意重新对您的代码进行基准测试。 提供最快的解决方案不是我的本意。我只是想参加聚会:)(我忽略了你的回答,否则我不会发布我的)【参考方案7】:

使用 sed (GNU sed) 4.5:

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

【讨论】:

太糟糕了,我无法对其进行测试并将其添加到我的基准测试中。我敢打赌这将是最快的答案。不幸的是,您的脚本缺少空文件,因为它要求输入文件至少有一行。 这似乎不适用于sed v4.5-1 from Debian Sid,它只是在第一个文件之后退出,就像以前版本的sed【参考方案8】:

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)])) &lt;= 27])" *.txt

【讨论】:

虽然此代码可能会回答问题,但提供有关此代码为何和/或如何回答问题的额外上下文可提高其长期价值。【参考方案9】:

如果您必须单独调用 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 测试成功,并打印文件名。

【讨论】:

OP 表示他们不想读取整个文件(“问题是文件夹中的许多文件都是数百万行”)。 哎呀;我想我错过了 >27 案例的出口。稍后会修复。 添加了exit,尽管多次调用 awk 的 shell 循环总是比可以处理多个文件的 awk 慢。我会把它留在这里,以防它对没有 GNU awk 的人有所帮助。【参考方案10】:

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

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

如果缺少 0 字节文件,也包括这些文件,请执行以下操作:

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

(由于某种原因,通过-exec 运行sedxargs 慢大约12%。)


sed 代码从ctac's answer 窃取。

注意:在我自己的旧系统 sedv4.4-2 上,quit 命令与 --separate 开关结合使用不仅仅退出当前文件,它完全退出sed。这意味着每个文件都需要一个单独的 sed 实例。

【讨论】:

不幸的是,您的脚本缺少空文件,因为它要求 ($F) 输入文件至少有一行。此外,为每个文件启动一个sed 命令确实非常耗费资源,并且无法与只使用一两个命令的其他脚本竞争。 @xhienne,谢谢,我没有注意到空文件错误。同意这是效率不高的,但是当没有太多数据时,它不会有太大的不同。而当内存非常低时,sed 会使用更少的内存;比较ls -l $(realpath $(which sed awk)) 同意,但 OP 没有低内存限制 (iMac),并且正在明确寻找更快的替代 wc -l *.txt | awk 'if ($1 &lt;= 27)print' @xhienne,我不反对awk,但是...... 1)这个问题有点笼统,所以除了OP之外,可能会有或最终会有其他读者,谁有自己的需求。 2) sed 在这里比wc 快,因为每个文件在不超过 28 行后退出,而 wc 将读取一个大文件到最后。 @xhienne,另外,感谢running those benchmarks,这是我以前从未见过的,它确实显示了效率的提升。

以上是关于列出包含“n”行或更少行的文件的主要内容,如果未能解决你的问题,请参考以下文章

从文本文件中读取第一行而不将整个文本文件加载到内存中

在 PHP 中的 while / foreach 内包装 3 个或更少的对象

批量批量插入MySQL会产生GC Overhead和/或Java Heap Space错误

包裹第 n 个或更少的元素,错误的结果

SQL Server中的组合

weka 中的测试文件是不是需要与训练相同或更少的功能?