我们一次又一次地看到询问如何读取文本文件以逐行处理它的问题,这些问题使用以下变体read
, or readlines
,一次将整个文件拉入内存。
的文档read says:
打开文件,可选择查找给定的偏移量,然后返回长度字节(默认为文件的其余部分)。 [...]
的文档readlines says:
将按名称指定的整个文件读取为单独的行,并以数组形式返回这些行。 [...]
拉入小文件没什么大不了的,但是随着传入数据缓冲区的增长,内存必须被重新洗牌,这会消耗 CPU 时间。此外,如果数据消耗太多空间,操作系统就必须介入才能保持脚本运行并开始假脱机到磁盘,这将使程序崩溃。在 HTTPd(网络主机)或需要快速响应的东西上,它会削弱整个应用程序。
Slurping 通常是基于对文件 I/O 速度的误解,或者认为先读取然后分割缓冲区比一次读取一行更好。
这里有一些测试代码来演示“slurping”引起的问题。
将其保存为“test.sh”:
echo Building test files...
yes "abcdefghijklmnopqrstuvwxyz 123456890" | head -c 1000 > kb.txt
yes "abcdefghijklmnopqrstuvwxyz 123456890" | head -c 1000000 > mb.txt
yes "abcdefghijklmnopqrstuvwxyz 123456890" | head -c 1000000000 > gb1.txt
cat gb1.txt gb1.txt > gb2.txt
cat gb1.txt gb2.txt > gb3.txt
echo Testing...
ruby -v
echo
for i in kb.txt mb.txt gb1.txt gb2.txt gb3.txt
do
echo
echo "Running: time ruby readlines.rb $i"
time ruby readlines.rb $i
echo '---------------------------------------'
echo "Running: time ruby foreach.rb $i"
time ruby foreach.rb $i
echo
done
rm [km]b.txt gb[123].txt
它创建五个大小不断增加的文件。 1K 文件很容易处理,而且很常见。过去 1MB 文件被认为是大文件,但现在它们很常见。 1GB 在我的环境中很常见,并且会定期遇到超过 10GB 的文件,因此了解 1GB 及以上会发生什么情况非常重要。
将其保存为“readlines.rb”。它不做任何事情,只是在内部逐行读取整个文件,并将其附加到然后返回的数组中,而且看起来它会很快,因为它都是用 C 编写的:
lines = File.readlines(ARGV.shift).size
puts "#{ lines } lines read"
将其保存为“foreach.rb”:
lines = 0
File.foreach(ARGV.shift) { |l| lines += 1 }
puts "#{ lines } lines read"
Running sh ./test.sh
在我的笔记本电脑上我得到:
Building test files...
Testing...
ruby 2.1.2p95 (2014-05-08 revision 45877) [x86_64-darwin13.0]
读取1K文件:
Running: time ruby readlines.rb kb.txt
28 lines read
real 0m0.998s
user 0m0.386s
sys 0m0.594s
---------------------------------------
Running: time ruby foreach.rb kb.txt
28 lines read
real 0m1.019s
user 0m0.395s
sys 0m0.616s
读取1MB文件:
Running: time ruby readlines.rb mb.txt
27028 lines read
real 0m1.021s
user 0m0.398s
sys 0m0.611s
---------------------------------------
Running: time ruby foreach.rb mb.txt
27028 lines read
real 0m0.990s
user 0m0.391s
sys 0m0.591s
读取1GB文件:
Running: time ruby readlines.rb gb1.txt
27027028 lines read
real 0m19.407s
user 0m17.134s
sys 0m2.262s
---------------------------------------
Running: time ruby foreach.rb gb1.txt
27027028 lines read
real 0m10.378s
user 0m9.472s
sys 0m0.898s
读取2GB文件:
Running: time ruby readlines.rb gb2.txt
54054055 lines read
real 0m58.904s
user 0m54.718s
sys 0m4.029s
---------------------------------------
Running: time ruby foreach.rb gb2.txt
54054055 lines read
real 0m19.992s
user 0m18.765s
sys 0m1.194s
读取3GB文件:
Running: time ruby readlines.rb gb3.txt
81081082 lines read
real 2m7.260s
user 1m57.410s
sys 0m7.007s
---------------------------------------
Running: time ruby foreach.rb gb3.txt
81081082 lines read
real 0m33.116s
user 0m30.790s
sys 0m2.134s
注意如何readlines
每次文件大小增加时,运行速度都会慢两倍,并且使用foreach
线性减慢。在 1MB 时,我们可以看到有一些东西会影响“slurping”I/O,但不会影响逐行读取。而且,由于 1MB 文件如今非常常见,因此很容易看出,如果我们不提前考虑,它们会在程序的生命周期内减慢文件的处理速度。这里只有几秒钟,或者当它们发生一次时没有太多,但如果它们每分钟发生多次,到年底就会对性能产生严重影响。
几年前,我在处理大型数据文件时遇到了这个问题。我使用的 Perl 代码会定期停止,因为它在加载文件时重新分配内存。重写代码以不读取数据文件,而是逐行读取和处理它,这极大地提高了运行速度,从运行五分钟多到不到一分钟,这给了我一个重要的教训。
“slurping”文件有时很有用,特别是当您必须跨行边界执行某些操作时,但是,如果必须这样做,则值得花一些时间考虑读取文件的替代方法。例如,考虑维护一个由最后“n”行构建的小缓冲区并扫描它。这将避免由于尝试读取和保存整个文件而导致的内存管理问题。这在 Perl 相关博客中进行了讨论“Perl Slurp-Eaze”其中涵盖了“何时”和“为什么”来证明使用完整文件读取的合理性,并且非常适用于 Ruby。
对于不要“吞食”文件的其他绝佳理由,请阅读“如何在文件文本中搜索模式并将其替换为给定值".