【问题标题】:Parse large JSON hash with ruby-yajl?用 ruby​​-yajl 解析大型 JSON 哈希?
【发布时间】:2014-01-07 17:56:13
【问题描述】:

我有一个包含 JSON 哈希的大文件 (>50Mb)。比如:

{ 
  "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 中的某种回调可能吗?

【问题讨论】:

  • Yajl supposed 支持 SAX 类型的解析器,它可以让您读取文件,并在读取文件时选择性地处理对象,但是,就像您一样,我没有看到任何使用 Ruby 界面执行此操作的示例。如果在解析 JSON 并返回对象之前必须将整个文档读入内存,则流式处理不会对您有所帮助。您的代码只会导致部分读取大文件的 Ruby 结构。 Yajl 不会看到它确切知道在哪里关闭对象所需的右大括号和方括号,所以我认为你的想法行不通。
  • 根据我的测试,在完整读取文件之前,您块中的 obj 将不可用。也许 Ruby gem 的开发人员可以对此有更多了解?
  • @theTinMan 谢谢。是的,在 Yajl 中似乎支持 SAX 类型解析,但在它周围的 ruby​​ 包装器中不支持。在我阅读整个文件之前,我的块中的那个 obj 是不可用的。不可取。我找到了另一个解决方案,并在下面粘贴了一个答案,我对您的反馈感兴趣。

标签: ruby yajl


【解决方案1】:

我最终使用JSON::Stream 解决了这个问题,它有start_documentstart_object 等的回调。

我给了我的“解析器”一个to_enum 方法,它在解析时发出所有“资源”对象。请注意,除非您完全解析 JSON 流,否则永远不会真正使用 ResourcesCollectionNode,并且 ResourceNodeObjectNode 的子类,仅用于命名目的,尽管我可能会摆脱它:

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 !@io.eof?
          # 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}}

【讨论】:

  • 这看起来是一条更好的路径。遗憾的是,Yajl-Ruby 没有公开类似 SAX 的接口,因为它们声称有很大的加速。
【解决方案2】:

我遇到了同样的问题并创建了 gem 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"
}

【讨论】:

    猜你喜欢
    • 2011-11-24
    • 1970-01-01
    • 1970-01-01
    • 2020-10-29
    • 2018-09-25
    • 2016-05-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多