如何在不消耗所有内存的情况下将巨大的JSON文件作为Ruby中的流处理?

thi*_*ign 9 ruby memory parsing json yajl

我在Ruby中处理一个巨大的JSON文件时遇到了麻烦.我正在寻找的是一种逐个处理它的方法,而不会在内存中保留太多数据.

我认为yajl-ruby gem会做这项工作,但它会消耗我所有的记忆.我也看过Yajl :: FFI和JSON:流宝石,但有明确说明:

对于较大的文档,我们可以使用IO对象将其流式传输到解析器中.我们仍然需要解析对象的空间,但文档本身永远不会完全读入内存.

这是我用Yajl做的事情:

file_stream = File.open(file, "r")
json = Yajl::Parser.parse(file_stream)
json.each do |entry|
    entry.do_something
end
file_stream.close
Run Code Online (Sandbox Code Playgroud)

内存使用量持续增加,直到进程被终止.

我不明白为什么Yajl会在内存中保留已处理的条目.我可以以某种方式释放它们,还是我误解了Yajl解析器的功能?

如果无法使用Yajl完成:有没有办法在Ruby中通过任何库?

Tod*_*obs 5

问题

json = Yajl :: Parser.parse(file_stream)

当您像这样调用Yajl :: Parser时,整个流将加载到内存中以创建数据结构.不要那样做.

Yajl提供了Parser#parse_chunk,Parser#on_parse_complete以及其他相关方法,使您能够在流上触发解析事件,而无需一次解析整个IO流.README 包含一个如何使用分块的示例.

README中给出的示例是:

或者假设您无法访问包含JSON数据的IO对象,而是一次只能访问它的块.没问题!

(假设我们在EventMachine :: Connection实例中)

def post_init
  @parser = Yajl::Parser.new(:symbolize_keys => true)
end

def object_parsed(obj)
  puts "Sometimes one pays most for the things one gets for nothing. - Albert Einstein"
  puts obj.inspect
end

def connection_completed
  # once a full JSON object has been parsed from the stream
  # object_parsed will be called, and passed the constructed object
  @parser.on_parse_complete = method(:object_parsed)
end

def receive_data(data)
  # continue passing chunks
  @parser << data
end
Run Code Online (Sandbox Code Playgroud)

或者,如果您不需要对其进行流式传输,它只会在完成后从解析中返回构建的对象.注意:如果输入中将有多个JSON字符串,则必须指定一个块或回调,因为这是yajl-ruby将每个对象传递给您(调用者)的方式,因为它是从输入中解析出来的.

obj = Yajl::Parser.parse(str_or_io)
Run Code Online (Sandbox Code Playgroud)

无论如何,您必须一次只解析一部分JSON数据.否则,您只是在内存中实例化一个巨大的Hash,这正是您描述的行为.

如果不知道您的数据是什么样的以及JSON对象是如何组成的,那么就不可能给出更详细的解释.因此,您的里程可能会有所不同.但是,这至少应该指向正确的方向.

  • 我喜欢使用EventMachine的想法,但不知道如何将文件发送到EM.你介意给出一个完整的例子吗?有趣的是,JSON是一个对象数组,不确定是否会导致任何问题:[{"key":"value"},{"key":"value"} ...] (3认同)

thi*_*ign 5

@CodeGnome 和 @A。Rager 的回答帮助我理解了解决方案。

我最终创建了 gem json-streamer,它提供了一种通用方法,无需为每个场景手动定义回调。