Как я могу обрабатывать огромные файлы JSON в виде потоков в Ruby, не потребляя всю память?

У меня возникли проблемы с обработкой огромного файла JSON в Ruby. То, что я ищу,-это способ обработать его запись за записью, не сохраняя слишком много данных в памяти.

Я думал, что yajl-Рубин джем сделал бы работу, но она поглощает всю мою память. Я также посмотрел на 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

использование памяти продолжает расти, пока процесс не будет убит.

Я не понимаю, почему Yajl хранит обработанные записи в памяти. Могу ли я как-то освободить их, или я просто неправильно понял возможности парсера Yajl?

Если это невозможно сделать с помощью Yajl: есть ли способ сделать это в Ruby через любой библиотека?

3 ответов


оба ответа @CodeGnome и @A. Rager помогли мне понять решение.

Я закончил создание драгоценного камня в JSON-стример это предлагает общий подход и избавляет от необходимости вручную определять обратные вызовы для каждого сценария.


ваши решения кажутся в JSON поток и 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,
  },
}

можно было бы поток разбирать его с 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