列出包含“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 脚本的两倍。
注意事项:
为什么平台很重要?因为我的答案依赖于并行处理grep
和sed
。当然,为了获得公正的结果,如果您只有一个 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/:[^:]*$//' <<< "$res" > 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<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 < fname) > 0) && j<=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" ] && 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)])) <= 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 sed
(v4.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
运行sed
比xargs
慢大约12%。)
sed
代码从ctac's answer 窃取。
注意:在我自己的旧系统 sed
v4.4-2 上,q
uit 命令与 --separate
开关结合使用不仅仅退出当前文件,它完全退出sed
。这意味着每个文件都需要一个单独的 sed
实例。
【讨论】:
不幸的是,您的脚本缺少空文件,因为它要求 ($F
) 输入文件至少有一行。此外,为每个文件启动一个sed
命令确实非常耗费资源,并且无法与只使用一两个命令的其他脚本竞争。
@xhienne,谢谢,我没有注意到空文件错误。同意这是效率不高的,但是当没有太多数据时,它不会有太大的不同。而当内存非常低时,sed
会使用更少的内存;比较ls -l $(realpath $(which sed awk))
同意,但 OP 没有低内存限制 (iMac),并且正在明确寻找更快的替代 wc -l *.txt | awk 'if ($1 <= 27)print'
@xhienne,我不反对awk
,但是...... 1)这个问题有点笼统,所以除了OP之外,可能会有或最终会有其他读者,谁有自己的需求。 2) sed
在这里比wc
快,因为每个文件在不超过 28 行后退出,而 wc
将读取一个大文件到最后。
@xhienne,另外,感谢running those benchmarks,这是我以前从未见过的,它确实显示了效率的提升。以上是关于列出包含“n”行或更少行的文件的主要内容,如果未能解决你的问题,请参考以下文章
在 PHP 中的 while / foreach 内包装 3 个或更少的对象