如何在Ruby中处理巨大的JSON文件作为流,而不占用所有内存?

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

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

对于较大的文档,我们可以使用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 

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

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

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

问题

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 

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

 obj = Yajl::Parser.parse(str_or_io) 

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

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

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

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

你的解决方案似乎是json-stream和yajl-ffi 。 这两个例子非常相似(他们来自同一个人):

 def post_init @parser = Yajl::FFI::Parser.new @parser.start_document { puts "start document" } @parser.end_document { puts "end document" } @parser.start_object { puts "start object" } @parser.end_object { puts "end object" } @parser.start_array { puts "start array" } @parser.end_array { puts "end array" } @parser.key {|k| puts "key: #{k}" } @parser.value {|v| puts "value: #{v}" } end def receive_data(data) begin @parser << data rescue Yajl::FFI::ParserError => e close_connection end end 

在那里,他设置了流解析器可以体验的可能数据事件的回调。

给出一个json文档,如下所示:

 { 1: { name: "fred", color: "red", dead: true, }, 2: { name: "tony", color: "six", dead: true, }, ... n: { name: "erik", color: "black", dead: false, }, }