9

我无法在 Ruby 中处理一个巨大的 JSON 文件。我正在寻找的是一种逐项处理它而不在内存中保留太多数据的方法。

我认为yajl-ruby gem 可以完成这项工作,但它消耗了我所有的记忆。我还查看了Yajl::FFI和 JSON:Stream gems,但其中明确说明:

对于较大的文档,我们可以使用 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 中做到这一点?

4

3 回答 3

5

问题

json = Yajl::Parser.parse(file_stream)

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

解决方案

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

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

或者假设您无权访问包含 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 对象是如何组成的,那么不可能给出比这更详细的解释;因此,您的里程可能会有所不同。但是,这至少应该让您指出正确的方向。

于 2015-09-29T03:52:13.953 回答
5

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

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

于 2016-05-29T00:16:02.997 回答
2

您的解决方案似乎是json-streamyajl-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,
  },
}

可以使用 yajl-ffi 对其进行流式解析,如下所示:

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
于 2015-09-29T22:24:26.430 回答