计算行数或枚举行号,以便我可以遍历它们 - 为啥这是一种反模式?
Posted
技术标签:
【中文标题】计算行数或枚举行号,以便我可以遍历它们 - 为啥这是一种反模式?【英文标题】:Counting lines or enumerating line numbers so I can loop over them - why is this an anti-pattern?计算行数或枚举行号,以便我可以遍历它们 - 为什么这是一种反模式? 【发布时间】:2021-04-08 20:17:18 【问题描述】:我贴出以下代码被骂了。为什么这是不可接受的?
numberOfLines=$(wc -l <"$1")
for ((i=1; $i<=$numberOfLines; ++$i)); do
lineN=$(sed -n "$i!d;p;q" "$1")
# ... do things with "$lineN"
done
我们将输入文件中的行数收集到 numberOfLines
中,然后从 1 循环到该数字,在每次迭代中从文件中提取带有 sed
的下一行。
我收到的反馈抱怨说,在循环中使用sed
反复读取同一个文件以获取下一行效率低下。我想我可以使用head -n "$i" "$1" | tail -n 1
,但这几乎没有效率,是吗?
有没有更好的方法来做到这一点?为什么我要避免这种特殊方法?
【问题讨论】:
What is an anti-pattern? 【参考方案1】:shell(以及基本上所有高于汇编语言的编程语言)已经知道如何循环文件中的行;它不需要知道要获取下一行将有多少行 - 引人注目的是,在您的示例中,sed
已经执行此操作,因此如果 shell 无法执行此操作,您可以遍历 @987654326 的输出@ 代替。
在 shell 中循环遍历文件中的行的正确方法是使用while read
。有一些复杂的情况 - 通常,您重置 IFS
以避免让 shell 不必要地将输入拆分为标记,并且您使用 read -r
来避免在原始 Bourne shell 的 read
实现中使用反斜杠的一些讨厌的遗留行为,为了向后兼容而保留。
while IFS='' read -r lineN; do
# do things with "$lineN"
done <"$1"
除了比您的sed
脚本简单得多之外,这还避免了您读取整个文件一次以获得行数,然后在每次循环迭代中一次又一次地读取同一文件的问题。使用典型的现代磁盘驱动程序,可以通过缓存来避免一些重复读取,但基本事实仍然是,当您可以避免时,从磁盘读取信息比不这样做要慢 1000 倍。尤其是对于一个大文件,缓存最终会填满,因此您最终会一遍又一遍地读取和丢弃相同的字节,这会增加大量的 CPU 开销,甚至更多的 CPU 只是在执行其他操作时等待磁盘传送您读取的字节。
在 shell 脚本中,如果可以的话,您还希望避免外部进程的开销。在紧密循环中数千次调用sed
(或功能等效但更昂贵的两进程head -n "$i"| tail -n 1
)将为任何重要的输入文件增加大量开销。 (另一方面,如果您的循环体可以在例如sed
或Awk 中完成,这将比本机shell while read
循环更有效,因为read
的实现方式. 这就是为什么while read
is also frequently regarded as an antipattern.
并确保您相当熟悉Unix text processing tools - cut
、paste
、nl
、pr
等的标准调色板)
sed
脚本中的q
是一个非常部分的补救措施;通常,您会看到 sed
脚本每次都会读取整个输入文件直到最后的变化,即使它只想从文件中提取第一行中的一行。
对于较小的输入文件,影响可以忽略不计,但是仅仅因为当输入文件较小时它不会立即造成伤害而延续这种不良做法是不负责任的。只是不要将这种技术教给初学者。完全没有。
如果您确实需要显示输入文件中的行数,至少要确保您不会为了获得该数字而花费大量时间直到最后。也许stat
文件并跟踪每行有多少字节,所以你可以投影你剩下的行数(而不是line 1/10345234
显示类似line 1/approximately 10000000
?)...或使用像pv
.这样的外部工具
顺便说一句,你也想避免一个模糊相关的反模式;当您一次只处理一行时,您希望避免将整个文件读入内存。在for
循环中这样做还有一些额外的问题,所以也不要这样做;见https://mywiki.wooledge.org/DontReadLinesWithFor
【讨论】:
在显着开销上扩展一点。通过执行 OP 中提供的操作,您可以一遍又一遍地执行各种操作。这包括,打开文件,将文件读取到感兴趣的行,关闭文件。这确保了原始程序的复杂性是 O(N^2),而在这个答案中它只是 O(N)(您只打开、读取和关闭文件)。对于大文件以及存储在基于网络的文件系统上的文件,原始过程将变得非常缓慢。以上是关于计算行数或枚举行号,以便我可以遍历它们 - 为啥这是一种反模式?的主要内容,如果未能解决你的问题,请参考以下文章