计算行数或枚举行号,以便我可以遍历它们 - 为什么这是一种反模式?

问题描述

我贴了下面的代码被骂了。为什么这是不可接受的?

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 但这几乎没有效率更高,是吗?

有没有更好的方法来做到这一点?为什么我要避免这种特殊方法

解决方法

shell(基本上所有高于汇编语言的编程语言)已经知道如何遍历文件中的行;它不需要知道将有多少行来获取下一行——令人惊讶的是,在你的例子中,sed 已经这样做了,所以如果 shell 不能这样做,你可以循环从sed 代替。

在 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 循环更有效,因为 { {1}} 已实现。这就是为什么 while read is also frequently regarded as an antipattern.)

read 脚本中的 q 是一种非常局部的补救措施;您经常会看到 sed 脚本每次都会读取整个输入文件直到最后的变化,即使它只想从文件中取出最开始的一行。

对于一个小的输入文件,影响可以忽略不计,但是仅仅因为当输入文件很小时它不会立即有害而延续这种不良做法是不负责任的。只是不要将这种技术教给初学者。完全没有。

如果你真的需要显示输入文件中的行数,至少确保你不要为了获得那个数字而花费大量时间寻找到最后。也许 sed 文件并跟踪每行有多少字节,因此您可以预测剩余的行数(而不是 stat 显示类似 line 1/10345234? ) ... 或使用像 pv.

这样的外部工具

顺便说一下,你也想避免一个模糊相关的反模式;当您一次只处理一行时,您希望避免将整个文件读入内存。在 line 1/approximately 10000000 循环中这样做也有一些额外的问题,所以也不要这样做;见https://mywiki.wooledge.org/DontReadLinesWithFor

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...