3

我有一个大小约为 10 GB 的字符串(巨大的 RAM 使用量……)。问题是,我需要执行 gsub 之类的字符串操作并对其进行拆分。我注意到 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 {..} 来迭代字符串。然而,这只是替换换行符。

编辑:感谢所有这些答案。就我而言,“HUGE 10 GB STRING”实际上是从 Internet 下载的。它包含由特定序列(在大多数情况下是简单的换行符)分隔的数据。在我的场景中,我将 10 GB 文件的每个元素与脚本中已有的另一个(较小的)数据集进行比较。我感谢所有建议。

4

6 回答 6

8

这是针对真实日志文件的基准。在用于读取文件的方法中,只有一种使用foreach是可扩展的,因为它避免了对文件的破坏。

使用lazy会增加开销,导致时间比map单独使用要慢。

请注意,foreach就处理速度而言,它就在那里,并产生了可扩展的解决方案。Ruby 不在乎文件是数以亿计的行还是数以亿计的 TB,它仍然一次只能看到一行。有关读取文件的一些相关信息,请参阅“为什么“slurping”文件不是一个好习惯? ”。

人们经常倾向于使用一次提取整个文件,然后将其拆分为多个部分的东西。这忽略了 Ruby 然后必须根据行结束使用split或类似的东西来重建数组的工作。这加起来,这就是我认为foreach领先的原因。

另请注意,两次基准运行之间的结果略有不同。这可能是由于作业正在运行时在我的 Mac Pro 上运行的系统任务所致。重要的是显示差异是清洗,向我确认 usingforeach是处理大文件的正确方法,因为如果输入文件超过可用内存,它不会杀死机器。

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是无害的,因为每种方法都使用它,但它不是必需的,并且是为了一些无聊的电阻负载而添加的。

于 2013-05-08T17:28:29.790 回答
4

如果你想逐行处理一个大文件,这将更有弹性并且更少占用内存:

File.open('big_file.log') do |file|
  file.each_line do |line|
     # Process the line
  end
end

这种方法不允许您交叉引用行,但如果您需要,请考虑使用临时数据库。

于 2013-05-08T11:41:13.177 回答
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_linelazy.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
于 2013-05-08T12:30:47.377 回答
1

你甚至有 10+GB 的空间来适应内存中的字符串吗?

我假设字符串是从文件中加载的,因此请考虑使用 each_line 或按该顺序直接处理文件...

于 2013-05-08T11:41:24.190 回答
1

我注意到 Ruby 会在某个时候“停止工作”(...)我正在使用 Ruby MRI 1.8.7,这里有什么问题?

除非您有大量 RAM,否则这是因为您在应用程序级别遇到了抖动,也就是说,每次获得 CPU 控制权时它都无法完成太多工作,因为它一直在交换磁盘中的内存.

为什么 Ruby 不能对大字符串执行字符串操作?

我怀疑没有人,除非从文件中部分读取它。

这里有什么解决方案?

我不禁注意到您正在尝试将文件拆分为字符串,然后想要匹配正则表达式中的子字符串。所以我可以看到两种选择

  1. (简单):如果您的正则表达式仅使用一行,您可以在文本文件中使用此文本更好地执行并执行grep系统调用以检索您需要的任何内容 - 已经创建了 grep 来处理大文件,所以您没有自己担心。

  2. (复杂):但是,如果您的正则表达式是多行正则表达式,则必须通过read调用读取文件的部分内容,指定一次要读取的字节数。然后你必须管理正在匹配的内容,并连接不匹配的字符串的末尾,因为将它与下一部分字节连接起来可以创建匹配模式。此时,正如@Dogbert 所建议的那样,您可能会开始考虑更改为静态语言,因为无论如何您都将在低级别进行编程。也许创建一个 ruby​​ C 扩展?

如果您需要有关您的方法的更多详细信息,请告诉我,我可以写更多关于上述两种方法之一的信息。

于 2013-05-08T12:28:20.673 回答
1

假设从磁盘读取字符串,您可以使用foreach一次读取和处理一行,将每一行写回磁盘。就像是:

File.open("processed_file", "w") do |dest|
  File.foreach("big_file", "\r\n") do |line|
    # processing goes here
    dest << line
  end
end
于 2013-05-08T13:31:45.877 回答