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

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

Я думал, что гем yajl-ruby выполнит эту работу, но он потребляет всю мою память. Я также просмотрел Yajl::FFI и JSON:Stream gems, но там явно заявил:

Для больших документов мы можем использовать объект ввода-вывода для потоковой передачи в парсер. Нам по-прежнему нужно место для анализируемого объекта, но сам документ никогда полностью не считывается в память.

Вот что я сделал с 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 через любую библиотеку?


person thisismydesign    schedule 25.08.2015    source источник
comment
Какая версия рубина? Я спрашиваю, потому что звучит так, будто ГК не отпускает.   -  person Anthony    schedule 25.08.2015
comment
Возможный дубликат stackoverflow.com/ вопросы/20978518/   -  person bumpy    schedule 25.08.2015
comment
@Энтони: рубин-2.2.1   -  person thisismydesign    schedule 26.08.2015
comment
@bumpy: Я ищу способ легко сделать это с библиотекой, я не хочу сам изобретать колесо, как это было сделано в этом примере. Однако, если ничего не подходит для моей проблемы, то это был бы хороший способ начать пробовать, спасибо.   -  person thisismydesign    schedule 26.08.2015


Ответы (3)


Проблема

json = Yajl::Parser.parse(file_stream)

Когда вы вызываете Yajl::Parser таким образом, весь поток загружается в память для создания вашей структуры данных. Не делай этого.

Решение

Yajl предоставляет Parser#parse_chunk, Parser#on_parse_complete и другие связанные методы, которые позволяют вы можете инициировать события синтаксического анализа в потоке, не требуя одновременного анализа всего потока ввода-вывода. README содержит пример использования фрагментации .

Пример, приведенный в README:

Или, скажем, у вас не было доступа к объекту ввода-вывода, содержащему данные JSON, а вместо этого был доступ только к его частям за раз. Без проблем!

(Предположим, что мы находимся в экземпляре 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, невозможно дать более подробное объяснение; в результате ваш пробег может отличаться. Тем не менее, это должно, по крайней мере, указать вам правильное направление.

person Todd A. Jacobs    schedule 29.09.2015
comment
Мне нравится эта идея использования EventMachine, но я не знаю, как отправить файл в EM. Не могли бы вы показать полный пример? Интересно, что JSON представляет собой массив объектов, не уверен, что это вызовет какие-либо проблемы: [ {ключ:значение}, {ключ:значение} ... ] - person thisismydesign; 30.09.2015

И @CodeGnome, и @A. Ответ Рейджера помог мне понять решение.

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

person thisismydesign    schedule 29.05.2016

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

Можно было бы разобрать его с помощью 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
person A. Rager    schedule 29.09.2015