我有一个大小约为10 GB的字符串(大量的RAM使用...)。 问题是,我需要执行像gsub和split这样的字符串操作。 我注意到Ruby会在某个时刻“停止工作”(但不会产生任何错误)。
示例:
str = HUGE_STRING_10_GB
# I will try to split the string using .split:
str.split("\r\n")
# but Ruby will instead just return an array with
# the full unsplitted string itself...
# let's break this down:
# each of those attempts doesn't cause problems and
# returns arrays with thousands or even millions of items (lines)
str[0..999].split("\r\n")
str[0..999_999].split("\r\n")
str[0..999_999_999].split("\r\n")
# starting from here, problems will occur
str[0..1_999_999_999].split("\r\n")
我正在使用Ruby MRI 1.8.7, 这有什么不对? 为什么Ruby无法对巨大的字符串执行字符串操作? 什么是解决方案?
我想出的唯一解决方案是使用[0..9],[10..19],“循环”字符串,并逐个执行字符串操作。然而,这似乎是不可靠的,例如,如果我的拆分分隔符很长并且落在两个“部分”之间。
实际上正常工作的另一个解决方案是像str.each_line {..}一样迭代字符串。 但是,这只是替换了换行符分隔符。
编辑: 感谢所有这些答案。 就我而言,“巨大的10 GB STRING”实际上是从互联网上下载的。 它包含由特定序列分隔的数据(在大多数情况下是简单的换行符)。 在我的场景中,我将10 GB文件的EACH ELEMENT与我脚本中已有的另一个(较小的)数据集进行比较。我感谢所有的建议。
答案 0 :(得分:8)
这是针对现实日志文件的基准测试。在用于读取文件的方法中,只有使用foreach
的方法是可扩展的,因为它避免了文件的污染。
使用lazy
会增加开销,导致比单独map
更慢的时间。
请注意,就处理速度而言,foreach
就在那里,并产生可扩展的解决方案。 Ruby不会关心这个文件是数万亿还是数十亿的TB,它仍然只能看到一行一行。有关阅读文件的一些相关信息,请参阅“Why is "slurping" a file not a good practice?”。
人们常常倾向于使用能够同时拉入整个文件的东西,然后将其分成几部分。这忽略了Ruby必须要做的工作,即使用split
或类似的方式基于行结束重建数组。这就加起来了,这也是我认为foreach
向前推进的原因。
另请注意,两次基准测试之间的结果略有不同。这可能是因为作业正在运行时,我的Mac Pro上运行的系统任务。重要的是,显示区别是洗,确认使用foreach
是处理大文件的正确方法,因为如果输入文件超出可用内存,它不会杀死机器。
require 'benchmark'
REGEX = /\bfoo\z/
LOG = 'debug.log'
N = 1
# each_line: "Splits str using the supplied parameter as the record separator
# ($/ by default), passing each substring in turn to the supplied block."
#
# Because the file is read into a string, then split into lines, this isn't
# scalable. It will work if Ruby has enough memory to hold the string plus all
# other variables and its overhead.
def lazy_map(filename)
File.open("lazy_map.out", 'w') do |fo|
fo.puts File.readlines(filename).lazy.map { |li|
li.gsub(REGEX, 'bar')
}.force
end
end
# each_line: "Splits str using the supplied parameter as the record separator
# ($/ by default), passing each substring in turn to the supplied block."
#
# Because the file is read into a string, then split into lines, this isn't
# scalable. It will work if Ruby has enough memory to hold the string plus all
# other variables and its overhead.
def map(filename)
File.open("map.out", 'w') do |fo|
fo.puts File.readlines(filename).map { |li|
li.gsub(REGEX, 'bar')
}
end
end
# "Reads the entire file specified by name as individual lines, and returns
# those lines in an array."
#
# As a result of returning all the lines in an array this isn't scalable. It
# will work if Ruby has enough memory to hold the array plus all other
# variables and its overhead.
def readlines(filename)
File.open("readlines.out", 'w') do |fo|
File.readlines(filename).each do |li|
fo.puts li.gsub(REGEX, 'bar')
end
end
end
# This is completely scalable because no file slurping is involved.
# "Executes the block for every line in the named I/O port..."
#
# It's slower, but it works reliably.
def foreach(filename)
File.open("foreach.out", 'w') do |fo|
File.foreach(filename) do |li|
fo.puts li.gsub(REGEX, 'bar')
end
end
end
puts "Ruby version: #{ RUBY_VERSION }"
puts "log bytes: #{ File.size(LOG) }"
puts "log lines: #{ `wc -l #{ LOG }`.to_i }"
2.times do
Benchmark.bm(13) do |b|
b.report('lazy_map') { lazy_map(LOG) }
b.report('map') { map(LOG) }
b.report('readlines') { readlines(LOG) }
b.report('foreach') { foreach(LOG) }
end
end
%w[lazy_map map readlines foreach].each do |s|
puts `wc #{ s }.out`
end
结果是:
Ruby version: 2.0.0
log bytes: 733978797
log lines: 5540058
user system total real
lazy_map 35.010000 4.120000 39.130000 ( 43.688429)
map 29.510000 7.440000 36.950000 ( 43.544893)
readlines 28.750000 9.860000 38.610000 ( 43.578684)
foreach 25.380000 4.120000 29.500000 ( 35.414149)
user system total real
lazy_map 32.350000 9.000000 41.350000 ( 51.567903)
map 24.740000 3.410000 28.150000 ( 32.540841)
readlines 24.490000 7.330000 31.820000 ( 37.873325)
foreach 26.460000 2.540000 29.000000 ( 33.599926)
5540058 83892946 733978797 lazy_map.out
5540058 83892946 733978797 map.out
5540058 83892946 733978797 readlines.out
5540058 83892946 733978797 foreach.out
使用gsub
是无害的,因为每种方法都使用它,但不需要它,并且添加了一些无聊的阻性负载。
答案 1 :(得分:4)
如果你想逐行处理一个大文件,这将更有弹性,更少占用内存:
File.open('big_file.log') do |file|
file.each_line do |line|
# Process the line
end
end
这种方法不允许您交叉引用行,但如果您需要,请考虑使用临时数据库。
答案 2 :(得分:2)
之前我遇到过这个问题。不幸的是,Ruby没有相应的Perl Tie::File
,它处理磁盘上的文件行。如果您在机器上安装了Perl并且不担心只对Ruby不忠,请提供以下代码:
use strict;
use Tie::File;
my $filename = shift;
tie my @lines, 'Tie::File', $filename
or die "Coud not open $filename\n";
for (@lines) { # process all the lines as you see fit
s/RUBY/ruby/g;
}
# you can cross reference lines if necessary
$lines[0] = $lines[99] . "!"; # replace the content of the first line with that 100th + "!"
untie @lines;
您可以根据需要处理文件(差不多)。
如果你可以使用Ruby 2.0,那么解决方案就是构建一个枚举器(即使是懒惰的,在处理时会减少内存消耗)。比如说这样(根据需要进行处理,比没有.lazy
时更快,所以我猜文件没有完全加载到内存中,并且每行都在我们处理时被释放):
File.open("dummy.txt") do |f|
f.lazy.map do |l|
l.gsub(/ruby/, "RUBY")
end.first(10)
end
所有这些还取决于您如何处理输出。
我做了一些基准测试。在Ruby 2.0.0上,至少each_line
保持内存消耗相当低:64 MB以下处理512 MB文件(其中每行有“RUBY”字样)。懒惰(在下面的代码中用each_line
替换lazy.each
)并没有提供内存使用和执行时间方面的任何改进。
File.open("dummy", "w") do |out|
File.open("DUMMY") do |f|
f.each_line do |l|
out.puts l.gsub(/RUBY/, "ruby")
end
end
end
答案 3 :(得分:1)
你甚至有10 + GB的字符串在内存中吗?
我假设字符串是从文件加载的,所以考虑直接使用each_line处理文件或者某个东西来处理该文件......
答案 4 :(得分:1)
我注意到Ruby会在某些时候“停止工作”(...)我正在使用Ruby MRI 1.8.7,这里有什么问题?
除非你有很多内存,否则这是因为你在你的应用程序级别遇到thrashing,也就是说,每次获得CPU控制时都不会做太多事情,因为它已经交换了磁盘中的内存始终存在。
为什么Ruby无法对巨大的字符串执行字符串操作?
我怀疑没有人,除非从文件中读取部分内容。
这里的解决方案是什么?
我不禁注意到你试图将文件拆分为字符串,然后想要匹配正则表达式中的子字符串。所以我可以看到两个替代方案
(简单):如果您的正则表达式只使用一行,您可以在文本文件中使用此文本执行更好的操作并执行grep
系统调用以检索您需要的任何内容 - 已经创建了grep处理大文件,所以你不必自己担心。
(复杂):但是,如果您的正则表达式是多行正则表达式,则必须使用read
调用来读取文件的某些部分,指定您希望一次读取多少字节。然后,您将必须管理匹配的内容,并连接不匹配的字符串的结尾,因为将其与下一部分字节连接起来可以创建匹配模式。在这一点上,正如@Dogbert建议的那样,你可能会开始考虑改为使用静态语言,因为无论如何你都会在低级编程。也许创建一个ruby C扩展?
如果您需要有关您的方法的更多详细信息,请告诉我,我可以写更多关于上述两个中的一个。
答案 5 :(得分:1)
假设从磁盘读取字符串,您可以使用foreach
一次读取和处理一行,将每一行写回磁盘。类似的东西:
File.open("processed_file", "w") do |dest|
File.foreach("big_file", "\r\n") do |line|
# processing goes here
dest << line
end
end