HUGE String上的Ruby String操作
我有一个大小约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与我脚本中已有的另一个(较小的)数据集进行比较。 我感谢所有的建议。
这是针对现实日志文件的基准测试。 在用于读取文件的方法中,只有使用foreach
是可伸缩的,因为它避免了文件的淤塞。
使用lazy
会增加开销,导致比单独map
更慢的时间。
请注意,就处理速度而言, foreach
就在那里,并产生可扩展的解决方案。 Ruby不会关心文件是数万亿还是数十亿TB,它仍然只能看到一行一行。 有关阅读文件的一些相关信息,请参阅“ 为什么”啜饮“文件不是一个好的做法? ”。
人们经常倾向于使用能够同时拉入整个文件的东西,然后将其分成几部分。 这忽略了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
是无害的,因为每种方法都使用它,但它不需要,并且添加了一些无聊的电阻负载。
如果你想逐行处理一个大文件,这将更有弹性,更少内存消耗:
File.open('big_file.log') do |file| file.each_line do |line| # Process the line end end
这种方法不允许您交叉引用行,但如果您需要,请考虑使用临时数据库。
我以前遇到过这个问题。 不幸的是,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
的情况.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”字样)。 懒惰(在下面的代码中用lazy.each
替换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
你甚至有10 + GB的字符串在内存中?
我假设字符串是从文件加载的,所以考虑直接使用each_line或该命令处理文件…
假设从磁盘读取字符串,您可以使用foreach
读取和处理一行,将每一行写回磁盘。 就像是:
File.open("processed_file", "w") do |dest| File.foreach("big_file", "\r\n") do |line| # processing goes here dest << line end end
我注意到Ruby会在某些时候“停止工作”(…)我正在使用Ruby MRI 1.8.7,这里有什么问题?
除非你有很多内存,否则这是因为你正在经历你的应用程序级别的颠簸 ,也就是说,每次获得CPU控制时都不会做太多事情,因为它一直在交换磁盘中的内存。
为什么Ruby无法对巨大的字符串执行字符串操作?
我怀疑没有人,除非从文件中读取部分内容。
什么是解决方案?
我不禁注意到你试图将文件拆分为字符串,然后想要在正则表达式中匹配子字符串。 所以我可以看到两种选择
-
(简单):如果您的正则表达式只使用一行,您可以在文本文件中使用此文本执行更好的操作并执行
grep
系统调用以检索您需要的任何内容 – 已经创建了grep来处理大文件,因此您不需要不得不担心自己。 -
(复杂):但是,如果您的正则表达式是多行正则表达式,则必须使用
read
调用read
文件的某些部分,指定一次要读取的字节数。 然后你将必须管理匹配的内容,并连接不匹配的字符串的结尾,因为它与下一部分字节连接它可以创建匹配模式。 在这一点上,正如@Dogbert所建议的那样,你可能会开始考虑改用静态语言,因为无论如何你都会编写一个低级编程。 也许创建一个ruby C扩展?
如果您需要有关您的方法的更多详细信息,请告诉我,我可以写更多关于上述两个中的一个。