edw*_*rkf 1 linux bash shell sh
我正在尝试计算一个非常大的文件中的行匹配数,并仅使用 BASH shell 命令将它们存储在变量中。
目前,我正在扫描一个非常大的文件的结果两次,并且每次都使用单独的 grep 语句,如下所示:
$ cat test.txt
first example line one
first example line two
first example line three
second example line one
second example line two
$ FIRST=$( cat test.txt | grep 'first example' | wc --lines ; ) ; ## first run
$ SECOND=$(cat test.txt | grep 'second example' | wc --lines ; ) ; ## second run
Run Code Online (Sandbox Code Playgroud)
我的结论是:
$ echo $FIRST
3
$ echo $SECOND
2
Run Code Online (Sandbox Code Playgroud)
希望我只想扫描大文件一次。我从未使用过Awk,也不想使用它!
这个|tee选项对我来说是新的。看来将结果传递到两个单独的 grep 语句中可能意味着我们只需扫描大文件一次。
理想情况下,我还希望能够做到这一点,而不必创建任何临时文件,随后必须记住删除它们。
我尝试了多种方法,使用如下所示的方法:
FIRST=''; SECOND='';
cat test.txt \
|tee >(FIRST=$( grep 'first example' | wc --lines ;);) \
>(SECOND=$(grep 'second example' | wc --lines ;);) \
>/dev/null ;
Run Code Online (Sandbox Code Playgroud)
并使用read:
FIRST=''; SECOND='';
cat test.txt \
|tee >(grep 'first example' | wc --lines | (read FIRST); ); \
>(grep 'second example' | wc --lines | (read SECOND); ); \
> /dev/null ;
cat test.txt \
| tee <( read FIRST < <(grep 'first example' | wc --lines )) \
<( read SECOND < <(grep 'sedond example' | wc --lines )) \
> /dev/null ;
Run Code Online (Sandbox Code Playgroud)
并带有大括号:
FIRST=''; SECOND='';
cat test.txt \
|tee >(FIRST={$( grep 'first example' | wc --lines ;)} ) \
>(SECOND={$(grep 'second example' | wc --lines ;)} ) \
>/dev/null ;
Run Code Online (Sandbox Code Playgroud)
但这些都不允许我将行数保存到变量FIRST和SECOND中。
这可能吗?
tee没有保存任何工作。每个人grep仍将对文件进行完整扫描。无论哪种方式,您都会遍历该文件三遍:两次greps 和一次Useless Use of Cat。事实上,实际上tee只是添加了第四个程序来循环整个文件。
由于一个致命缺陷,您尝试的各种| tee调用都不起作用:变量赋值在管道中不起作用。也就是说,只要为变量赋值,它们就“起作用”,只是该值几乎立即丢失。为什么?因为变量位于子 shell 中,而不是父 shell 中。
管道中的每个命令都|在不同的进程中执行,进程彼此隔离且不共享变量分配是 Linux 系统的基本事实。
根据经验,您可以将variable=$(foo | bar | baz)变量写在外部的位置。没问题。但不要尝试foo | variable=$(bar) | baz它在里面的位置。这不会起作用,你会很难过。
但不要失去希望!有很多方法可以给这只猫剥皮。让我们来看看其中的一些。
摆脱cat收益率:
first=$(grep 'first example' test.txt | wc -l)
second=$(grep 'second example' test.txt | wc -l)
Run Code Online (Sandbox Code Playgroud)
这实际上非常好并且通常足够快。Linux 在 RAM 中维护着一个大页面缓存。每当你读取一个文件时,Linux 都会将内容存储在内存中。多次读取文件通常会命中缓存而不是磁盘,速度非常快。即使是多 GB 的文件也能轻松装入现代计算机的 RAM,特别是当您在缓存的页面仍然新鲜时连续进行读取时。
grep您可以通过使用搜索两个字符串的单个调用来改进这一点。如果您实际上不需要单独的计数而只需要总数,那么它可能会起作用:
total=$(grep -e 'first example' -e 'second example' test.txt | wc -l)
Run Code Online (Sandbox Code Playgroud)
或者,如果匹配的行很少,您可以使用它将大文件过滤为一小组匹配行,然后使用原始 grep 提取单独的计数:
matches=$(grep -e 'first example' -e 'second example' test.txt)
first=$(grep 'first example' <<< "$matches" | wc -l)
second=$(grep 'second example' <<< "$matches" | wc -l)
Run Code Online (Sandbox Code Playgroud)
您还可以构建一个仅执行一次传递且不调用任何外部程序的 Bash 解决方案。分叉进程很慢,因此仅使用 和 等内置命令read可以[[提供很好的加速。
首先,让我们从一个while read循环开始逐行处理文件:
while IFS= read -r line; do
...
done < test.txt
Run Code Online (Sandbox Code Playgroud)
您可以使用双方括号[[和字符串 equals来计算匹配项==,它接受*通配符:
first=0
second=0
while IFS= read -r line; do
[[ $line == *'first example'* ]] && ((++first))
[[ $line == *'second example'* ]] && ((++second))
done < test.txt
echo "$first" ## should display 3
echo "$second" ## should display 2
Run Code Online (Sandbox Code Playgroud)
如果这些都不够快,那么您应该考虑使用“真正的”编程语言,例如 Python、Perl,或者实际上,任何您喜欢的语言。Bash 不是速度恶魔。我喜欢它,而且它确实没有得到充分的重视,但即使我也承认高性能数据处理并不是它的优势。