如何有效地列出恰好具有“n”行的文件?

Posted

技术标签:

【中文标题】如何有效地列出恰好具有“n”行的文件?【英文标题】:How to efficiently list files that have exactly `n` lines? 【发布时间】:2017-01-18 19:13:37 【问题描述】:

为了列出恰好具有n 行的文件,可以这样做

n=5
find . -name "*.txt" | xargs wc -l | awk -v n=$n -F" " 'if ($1==n) print $2 '

但是这个解决方案非常慢,因为它首先计算每个文件的行数,然后只选择具有n 行的那些。计算行数并在达到n+1 行时停止的进程会更有效率(尤其是在处理具有大量行的大文件时)。

如何有效地列出恰好具有n 行的文件?

注意,对于特殊情况,每行的大小完全相同,那么可能会这样做

n=5
sizePerLine=500
find . -name '*.txt' -size $(( $n * $sizePerLine ))

【问题讨论】:

【参考方案1】:

我认为以下会更快:

find . -name "*.txt" -exec awk -v n="$n" 'FILENAME != prevfile if(prevfnr==n) print prevfile prevfile = FILENAME; prevfnr = FNR; if(FNR>n) nextfile; ENDif (FNR==n) print FILENAME '  +

它是如何工作的:

使用-exec ... + 使用find 对每个文件执行命令,并让它在每次调用时传递许多参数 awk -v n="$n" 调用 awk 并定义一个名为 nawk 变量,使其具有与 shell 变量 n 相同的值 FILENAME != prevfile if(prevfnr==n) print prevfile 检查当前文件是否与上一条记录相同,如果不是,则查看前一个文件是否完全具有 n 记录,如果是,则打印该文件的名称 prevfile = FILENAME; prevfnr = FNR; if(FNR>n) nextfile; 用当前的FILENAME 更新prevfile 变量,用当前的FNR 更新prevfnr 变量。另外,如果我们当前的文件记录超过n,则直接跳转到下一个文件,这里不再进行任何处理 ENDif (FNR==n) print FILENAME 最后看看最后一个文件是否也正好有 n 记录

有趣的是,我发现这实际上给出了与使用wc -l 的版本不同的结果,尽管我认为这个实际上可能更正确。对于我的目录中最后一行不包含行结束字符wc -l 的文件,将报告行数,不计算最后一个“未终止”行,但这里的解决方案会计算它。

Arg,我没有意识到nextfile 是 GNU 主义。如果我已经把自己限制在这个范围内,我们可以让这个更干净

find . -name '*.txt' -exec  awk -v n="$n" 'FNR > n nextfile; ENDFILEif (FNR==n) print FILENAME '  +

在我看来,POSIX awk 没有很好的跳转到下一个文件的快捷方式,这是该解决方案提高效率所需的关键

【讨论】:

如果它让您感觉更好,技术上不以换行符结尾的文件不是文本文件。请参阅 POSIX 规范中的 3.206 和 3.397。 你是对的,在 POSIX awk 中没有办法停止处理文件(exit 除外),这就是为什么 nextfile 被引入作为 gawk 的扩展。您最终得到的解决方案是looks familiar... @EdMorton 是的,我想这几乎是您的解决方案。 . .我经常最终找到你的!【参考方案2】:
find . -name '*.txt' -print0 |
xargs -0 -n 1 awk -v n="$n" 'NR>nexit ENDif (NR==n) print FILENAME'

使用 GNU awk for ENDFILE 甚至更有效:

find . -name '*.txt' -exec \
awk -v n="$n" 'FNR>nnextfile ENDFILEif (FNR==n) print FILENAME'  +

上述脚本中的主要效率是在您遇到大于n 的行号时立即退出 awk 工作循环(即跳转到 END/ENDFILE 部分),而不是等到之前读取整个文件检查读取的行数。

在 gawk 脚本中显示 nextfileexit 之间的区别:

$ seq 10 | awk 'print; nextfile ENDFILEprint "x" ENDprint "y"'
1
x
y

$ seq 10 | awk 'print; exit ENDFILEprint "x" ENDprint "y"'
1
y

【讨论】:

我相信exit 会永远退出awk,而不是寻找更多的5 行文件。你可以试试gawknextfile 而不是exit;我自己试过了,但我没能让它可靠地工作,显然是因为ENDFILEnextfile 之后没有被调用。 @SatoKatsura 你说得对,在第二个(gawk)示例中应该是nextfilenextfile 将您带到 ENDFILE,而在 awk 主体中使用的 exit 将您带到 END。我已经更新了那个脚本。【参考方案3】:

使用grep:

n=5
find . -name '*.txt' | xargs grep '.+' -m $((n+1)) -c | grep ':'$n'$'

这告诉grep 只检查前n+1 行,并只显示具有n 行的文件。

替换 xargs ag '\n' -m$n -c 如果你有一个很好的加速 - ag 是一个比 grep 更快的搜索器。注意-m 只能在 GNU grep 上正常工作;在 BSD grep 上,它是一个全局选项(使用 ag 代替,或者获取 GNU grep)。

【讨论】:

我的grep (GNU grep 2.16) 匹配任何后跟+ 的字符,除非我添加-E 或使用egrep。它也不计算空行,因此会给出与wc -l不同的答案【参考方案4】:

你可能过于复杂了,你可以简单地使用 for 循环和 test 条件来评估,例如

for f in *.txt; do [ $(wc -l <"$f") -eq "5" ] && echo "$f"; done

这将找到当前目录中包含5 行的所有.txt 文件。

【讨论】:

但这不会下降到嵌套目录中,因此可能会错过很多要考虑的文件 要进入下面的目录,只需使用find 输入while 循环,执行完全相同的操作,例如while read -r f; do ....; done &lt; &lt;(find . -type f -name "*.txt") 这里更严重的问题是wc -l会读取所有文件到最后,找出行数。大多数其他发布的解决方案都会在阅读前几行后尝试退出,以节省时间。【参考方案5】:

使用 awk 本身:

n=5
find . -name '*.txt' | xargs -n 1 awk -e " n++; if (n > $n) exit 1  END  if ( n == $n ) print FILENAME"

一旦文件有 +5 行,它将立即退出,否则如果正好 5 行,则会打印。

【讨论】:

这似乎只是为我搜索它遇到的第一个文件,然后退出而不考虑下一个文件,尽管如果我将 -n 1 添加到 xargs 它会全部获取它们——但前提是文件名使用-print0xargs -0 是“不错”的,不过也有帮助 你是对的。 -n 1 是强制性的,我会更正,也许这里更好的方法应该使用 -exec ,正如您在解决方案中指出的那样。我会做出改变。谢谢 但是,xargs -n 1 将为每个文件生成一个 awk 进程。【参考方案6】:

使用grepawk

$ grep -cr "^" *|awk -F: '$2==6 print $1'

细分:

grep -c 统计文件中匹配行的数量 -r 是 --recursive "^" 匹配(即计数)行的开头

grep 的输出是:

foo:6
dir/bar:7
awk 使用 : 作为字段分隔符并打印给定行数的文件的文件名(和相关路径)。

【讨论】:

你用grep重新发明了wc -l。这和wc -l 有同样的问题:它将所有文件读到最后,而不是在前几行之后退出。 有效点,完全没有考虑,只是想提供find的替代方案。【参考方案7】:

perl:

n=5 find /some/dir -type f -name '*.txt' -exec \
    perl -lnE ' $. == $ENVn and eof and say $ARGV  continue  close ARGV if($. == $ENVn or eof) '  +

【讨论】:

【参考方案8】:

在 Bash≥4 的情况下,这是一种检查文本文件是否有 5 行的相当有效的方法:

mapfile -n 6 -t lines < file
if (( $#lines[@] == 5 )); then
    echo "has 5 lines"
else
    echo "doesn't have 5 lines"
fi

我们将mapfile-n 6 一起使用,这样读取的行数不会超过6 行(为了提高效率)。

find 命令一起,我们得到:

find . -name '*.txt' -type f -exec bash -c 'mapfile -n 6 -t lines < "$1"; (($#lines[@]==5))' _  \; -print

您还可以在 bash 语句中使用-exec ... + 和循环(练习留给读者)。

【讨论】:

【参考方案9】:

更有效的解决方案是使用findgawk,条件是ENDFILEFNR

find . -name '*.txt' -exec awk -v n=$n 'ENDFILEif(FNR==n) print FILENAME'  +

对于需要将wc -l 的整个输出通过管道传输到解析其输出的另一个进程的任何解决方案,这将文件数量的时间减少了一半。也就是说,使用ENDFILEnextfile 的其他答案甚至更有效,因为它们允许在达到所需的行数时跳到下一个文件。

假设您使用的是 Bash > 4.0,可以消除对 find 的需求,以利用允许递归扩展文件名的 globstar bash 选项。只要参数的数量不超过 gawkARGC 限制,这应该可以工作。

$ shopt -s globstar
$ gawk 'ENDFILEif(FNR==n) print FILENAME' **/*.txt

【讨论】:

这会将所有文件读取到最后,这本质上与wc -l一样糟糕。 @SatoKatsura 没有wc -l 解决方案那么糟糕。事实上,它可以将时间缩短一半,因为任何使用wc -l 的解决方案都必须像awk 这样的另一个进程再次解析其输出的每一行。也就是说,如果它像@EricRenouf 的解决方案那样使用nextfile,肯定会更有效。

以上是关于如何有效地列出恰好具有“n”行的文件?的主要内容,如果未能解决你的问题,请参考以下文章

如何有效地输出具有相同内容的文件列表?

Spark中具有固定向量的数据帧行的点积

如何有效地编辑大文件 XML?

如何有效地读取 LARGE 文本文件中的行数

如何有效地将大型 .tsv 文件上传到 pyspark 中具有拆分列的 Hive 表?

如何有效地乘以重复行的火炬张量而不将所有行存储在内存中或迭代?