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

时间:2015-08-25 15:57:43

标签: ruby json parsing memory yajl

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

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

  

对于较大的文档,我们可以使用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解析器的功能?

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

3 个答案:

答案 0 :(得分:5)

问题

  

json = Yajl :: Parser.parse(file_stream)

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

解决方案

Yajl提供Parser#parse_chunkParser#on_parse_complete以及其他相关方法,使您能够在流上触发解析事件,而无需一次解析整个IO流。有关如何使用分块的自述文件contains an example

自述文件中给出的示例是:

  

或者假设您无法访问包含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数据。否则,你只是在内存中实例化一个巨大的哈希,这正是你描述的行为。

在不知道您的数据是什么样的以及您的JSON对象是如何组成的情况下,不可能提供比这更详细的解释;因此,您的里程可能会有所不同。但是,这至少应该指向正确的方向。

答案 1 :(得分:3)

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

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

答案 2 :(得分:2)

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

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

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

给出一个类似的json文档:

&#13;
&#13;
def parse_dudes file_io, chunk_size
  parser = Yajl::FFI::Parser.new
  object_nesting_level = 0
  current_row = {}
  current_key = nil

  parser.start_object { object_nesting_level += 1 }
  parser.end_object do
    if object_nesting_level.eql? 2
      yield current_row #here, we yield the fully collected record to the passed block
      current_row = {}
    end
    object_nesting_level -= 1
  end

  parser.key do |k|
    if object_nesting_level.eql? 2
      current_key = k
    elsif object_nesting_level.eql? 1
      current_row["id"] = k
    end
  end

  parser.value { |v| current_row[current_key] = v }

  file_io.each(chunk_size) { |chunk| parser << chunk }
end

File.open('dudes.json') do |f|
  parse_dudes f, 1024 do |dude|
    pp dude
  end
end
&#13;
&#13;
&#13;

可以使用yajl-ffi来解析它:

{{1}}