Разобрать большой хэш JSON с помощью ruby-yajl?

У меня есть большой файл (> 50 МБ), который содержит хэш JSON. Что-то типа:

{ 
  "obj1": {
    "key1": "val1",
    "key2": "val2"
  },
  "obj2": {
    "key1": "val1",
    "key2": "val2"
  }
  ...
}

Вместо того, чтобы анализировать весь файл и брать, скажем, первые десять элементов, я хотел бы проанализировать каждый элемент в хэше. Меня на самом деле не волнует ключ, то есть obj1.

Если я преобразую приведенное выше в это:

  {
    "key1": "val1",
    "key2": "val2"
  }
  "obj2": {
    "key1": "val1",
    "key2": "val2"
  }

Я могу легко добиться того, чего хочу, используя потоковую передачу Yajl:

io = File.open(path_to_file)
count = 10
Yajl::Parser.parse(io) do |obj|
  puts "Parsed: #{obj}"
  count -= 1
  break if count == 0
end
io.close

Есть ли способ сделать это без изменения файла? Может быть, какой-то обратный вызов в Yajl?


person rainkinz    schedule 07.01.2014    source источник
comment
Yajl должен поддерживать синтаксический анализатор типа SAX, который позволял бы вам читать файл, и по мере его чтения выборочно обрабатывать объекты, однако, как и вы, я не вижу примеров делая это с интерфейсом Ruby. Потоковая передача не поможет вам, если весь документ должен быть прочитан в память до того, как будет проанализирован JSON и возвращен объект. Ваш код приводит только к частично прочитанной структуре Ruby для больших файлов. Yajl не увидит закрывающие фигурные скобки и квадратные скобки, необходимые для того, чтобы точно знать, где закрывать объекты, поэтому я думаю, что ваша идея не сработает.   -  person the Tin Man    schedule 07.01.2014
comment
Из моих тестов obj в вашем блоке не будет доступен, пока файл не будет прочитан полностью. Возможно, разработчики драгоценного камня Ruby смогут пролить на это больше света?   -  person the Tin Man    schedule 07.01.2014
comment
@theTinMan спасибо. Да, кажется, что в Yajl есть поддержка разбора типа SAX, но не в рубиновой обертке вокруг него. Правильно, что объект в моем блоке не будет доступен, пока я не прочитаю весь файл. Не желательно. Я нашел другое решение и вставил ниже ответ, о котором мне было бы интересно узнать ваше мнение.   -  person rainkinz    schedule 08.01.2014


Ответы (2)


В итоге я решил это, используя JSON::Stream, который имеет обратные вызовы для start_document, start_object и т. д.

Я дал своему «парсеру» метод to_enum, который испускает все объекты «Ресурс» по мере их анализа. Обратите внимание, что ResourcesCollectionNode на самом деле никогда не используется, если вы полностью не анализируете поток JSON, а ResourceNode является подклассом ObjectNode только для целей именования, хотя я мог бы просто избавиться от него:

class Parser
  METHODS = %w[start_document end_document start_object end_object start_array end_array key value]

  attr_reader :result

  def initialize(io, chunk_size = 1024)
    @io = io
    @chunk_size = chunk_size
    @parser = JSON::Stream::Parser.new

    # register callback methods
    METHODS.each do |name|
      @parser.send(name, &method(name))
    end 
  end

  def to_enum
    Enumerator.new do |yielder|
      @yielder = yielder
      begin
        while [email protected]?
          # puts "READING CHUNK"
          chunk = @io.read(@chunk_size)
          @parser << chunk
        end
      ensure
        @yielder = nil
      end
    end
  end

  def start_document
    @stack = []
    @result = nil
  end

  def end_document
    # @result = @stack.pop.obj
  end

  def start_object
    if @stack.size == 0
      @stack.push(ResourceCollectionNode.new)
    elsif @stack.size == 1
      @stack.push(ResourceNode.new)
    else
      @stack.push(ObjectNode.new)
    end
  end

  def end_object
    if @stack.size == 2
      node = @stack.pop
      #puts "Stack depth: #{@stack.size}. Node: #{node.class}"
      @stack[-1] << node.obj

      # puts "Parsed complete resource: #{node.obj}"
      @yielder << node.obj

    elsif @stack.size == 1
      # puts "Parsed all resources"
      @result = @stack.pop.obj
    else
      node = @stack.pop
      # puts "Stack depth: #{@stack.size}. Node: #{node.class}"
      @stack[-1] << node.obj
    end
  end

  def end_array
    node = @stack.pop
    @stack[-1] << node.obj
  end

  def start_array
    @stack.push(ArrayNode.new)
  end

  def key(key)
    # puts "Stack depth: #{@stack.size} KEY: #{key}"
    @stack[-1] << key
  end

  def value(value)
    node = @stack[-1]
    node << value
  end

  class ObjectNode
    attr_reader :obj

    def initialize
      @obj, @key = {}, nil
    end

    def <<(node)
      if @key
        @obj[@key] = node
        @key = nil
      else
        @key = node
      end
      self
    end
  end

  class ResourceNode < ObjectNode
  end

  # Node that contains all the resources - a Hash keyed by url
  class ResourceCollectionNode < ObjectNode
    def <<(node)
      if @key
        @obj[@key] = node
        # puts "Completed Resource: #{@key} => #{node}"
        @key = nil
      else
        @key = node
      end
      self
    end
  end

  class ArrayNode
    attr_reader :obj

    def initialize
      @obj = []
    end

    def <<(node)
      @obj << node
      self
    end
  end

end

и пример использования:

def json
  <<-EOJ
  {
    "1": {
      "url": "url_1",
      "title": "title_1",
      "http_req": {
        "status": 200,
        "time": 10
      }
    },
    "2": {
      "url": "url_2",
      "title": "title_2",
      "http_req": {
        "status": 404,
        "time": -1
      }
    },
    "3": {
      "url": "url_1",
      "title": "title_1",
      "http_req": {
        "status": 200,
        "time": 10
      }
    },
    "4": {
      "url": "url_2",
      "title": "title_2",
      "http_req": {
        "status": 404,
        "time": -1
      }
    },
    "5": {
      "url": "url_1",
      "title": "title_1",
      "http_req": {
        "status": 200,
        "time": 10
      }
    },
    "6": {
      "url": "url_2",
      "title": "title_2",
      "http_req": {
        "status": 404,
        "time": -1
      }
    }          

  }
  EOJ
end


io = StringIO.new(json)
resource_parser = ResourceParser.new(io, 100)

count = 0
resource_parser.to_enum.each do |resource|
  count += 1
  puts "READ: #{count}"
  pp resource
  break
end

io.close

Выход:

READ: 1
{"url"=>"url_1", "title"=>"title_1", "http_req"=>{"status"=>200, "time"=>10}}
person rainkinz    schedule 08.01.2014
comment
Это похоже на лучший путь. Жаль, что Yajl-Ruby не предоставляет SAX-подобный интерфейс, потому что они претендуют на большое ускорение. - person the Tin Man; 08.01.2014

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

Использование в вашем случае будет (v 0.4.0):

io = File.open(path_to_file)
streamer = Json::Streamer::JsonStreamer.new(io)
streamer.get(nesting_level:1).each do |object|
  p oject
end
io.close

Применяя его к вашему примеру, вы получите объекты без ключей obj:

{
  "key1": "val1",
  "key2": "val2"
}
person thisismydesign    schedule 29.05.2016