计算行数或枚举行号以便我可以遍历它们 - 为什么这是反模式?
Counting lines or enumerating line numbers so I can loop over them - why is this an anti-pattern?
我发了下面的代码被骂了。为什么这是不可接受的?
numberOfLines=$(wc -l <"")
for ((i=1; $i<=$numberOfLines; ++$i)); do
lineN=$(sed -n "$i!d;p;q" "")
# ... do things with "$lineN"
done
我们将输入文件中的行数收集到 numberOfLines
中,然后从 1 循环到该数字,在每次迭代中从文件中提取下一行 sed
。
我收到的反馈抱怨说,在循环中使用 sed
重复读取同一个文件以获得下一行是低效的。我想我可以使用 head -n "$i" "" | 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 <""
除了比您的 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
我发了下面的代码被骂了。为什么这是不可接受的?
numberOfLines=$(wc -l <"")
for ((i=1; $i<=$numberOfLines; ++$i)); do
lineN=$(sed -n "$i!d;p;q" "")
# ... do things with "$lineN"
done
我们将输入文件中的行数收集到 numberOfLines
中,然后从 1 循环到该数字,在每次迭代中从文件中提取下一行 sed
。
我收到的反馈抱怨说,在循环中使用 sed
重复读取同一个文件以获得下一行是低效的。我想我可以使用 head -n "$i" "" | 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 <""
除了比您的 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