基于流的解析和编写JSON

时间:2013-09-19 18:31:45

标签: ruby json parsing memory io

我从1,000个批次的服务器中获取大约20,000个数据集。每个数据集都是 JSON对象。坚持这使得大约350 MB的未压缩明文。

我的内存限制为 1GB 。因此,我将每个1,000个JSON对象作为数组写入追加模式的原始JSON 文件中。

结果是一个包含20个JSON数组的文件,需要进行聚合。无论如何我需要触摸它们,因为我想添加元数据。通常情况下Ruby Yajl Parser可以这样做:

raw_file = File.new(path_to_raw_file, 'r')
json_file = File.new(path_to_json_file, 'w')

datasets = []
parser = Yajl::Parser.new
parser.on_parse_complete = Proc.new { |o| datasets += o }

parser.parse(datasets)

hash = { date: Time.now, datasets: datasets }
Yajl::Encoder.encode(hash, json_file)

此解决方案的问题在哪里?问题是仍然将整个JSON解析为内存,我必须避免这种情况。

基本上我需要的是一个解决方案,从IO对象解析JSON 同时将它们编码为另一个IO对象

我认为Yajl提供了这个,但我还没找到方法,它的API也没有提供任何提示,所以我猜不是。是否有支持此功能的JSON Parser库?还有其他解决方案吗?


我能想到的唯一解决方案是使用IO.seek功能。一个接一个地写出所有数据集数组[...][...][...],在每个数组之后,我回到起点并用][覆盖,,有效地手动连接数组。

3 个答案:

答案 0 :(得分:5)

为什么不能一次从数据库中检索单个记录,根据需要处理它,将其转换为JSON,然后使用尾随/分隔逗号发出它?

如果您开始使用仅包含[的文件,然后附加所有JSON字符串,那么,在最终条目中没有附加逗号,而是使用结束] ,你有一个JSON哈希数组,只需要一次处理一行值。

它会慢一点(也许),但不会影响你的系统。如果您使用阻塞/分页一次检索合理数量的记录,则DB I / O可以非常快。

例如,这里是一些Sequel示例代码和代码的组合,用于将行提取为JSON并构建更大的JSON结构:

require 'json'
require 'sequel'

DB = Sequel.sqlite # memory database

DB.create_table :items do
  primary_key :id
  String :name
  Float :price
end

items = DB[:items] # Create a dataset

# Populate the table
items.insert(:name => 'abc', :price => rand * 100)
items.insert(:name => 'def', :price => rand * 100)
items.insert(:name => 'ghi', :price => rand * 100)

add_comma = false

puts '['
items.order(:price).each do |item|
  puts ',' if add_comma
  add_comma ||= true
  print JSON[item]
end
puts "\n]"

哪个输出:

[
{"id":2,"name":"def","price":3.714714089426208},
{"id":3,"name":"ghi","price":27.0179624376119},
{"id":1,"name":"abc","price":52.51248221170203}
]

请注意,订单现在是"价格"。

验证很简单:

require 'json'
require 'pp'

pp JSON[<<EOT]
[
{"id":2,"name":"def","price":3.714714089426208},
{"id":3,"name":"ghi","price":27.0179624376119},
{"id":1,"name":"abc","price":52.51248221170203}
]
EOT

结果是:

[{"id"=>2, "name"=>"def", "price"=>3.714714089426208},
 {"id"=>3, "name"=>"ghi", "price"=>27.0179624376119},
 {"id"=>1, "name"=>"abc", "price"=>52.51248221170203}]

这会验证JSON并证明原始数据是可恢复的。从数据库中检索的每一行应该是最小的&#34; bitesized&#34;要构建的整体JSON结构的一部分。

在此基础上,这里有如何读取数据库中的传入JSON,对其进行操作,然后将其作为JSON文件发出:

require 'json'
require 'sequel'

DB = Sequel.sqlite # memory database

DB.create_table :items do
  primary_key :id
  String :json
end

items = DB[:items] # Create a dataset

# Populate the table
items.insert(:json => JSON[:name => 'abc', :price => rand * 100])
items.insert(:json => JSON[:name => 'def', :price => rand * 100])
items.insert(:json => JSON[:name => 'ghi', :price => rand * 100])
items.insert(:json => JSON[:name => 'jkl', :price => rand * 100])
items.insert(:json => JSON[:name => 'mno', :price => rand * 100])
items.insert(:json => JSON[:name => 'pqr', :price => rand * 100])
items.insert(:json => JSON[:name => 'stu', :price => rand * 100])
items.insert(:json => JSON[:name => 'vwx', :price => rand * 100])
items.insert(:json => JSON[:name => 'yz_', :price => rand * 100])

add_comma = false

puts '['
items.each do |item|
  puts ',' if add_comma
  add_comma ||= true
  print JSON[
    JSON[
      item[:json]
    ].merge('foo' => 'bar', 'time' => Time.now.to_f)
  ]
end
puts "\n]"

生成:

[
{"name":"abc","price":3.268814929005337,"foo":"bar","time":1379688093.124606},
{"name":"def","price":13.871147312377719,"foo":"bar","time":1379688093.124664},
{"name":"ghi","price":52.720984131655676,"foo":"bar","time":1379688093.124702},
{"name":"jkl","price":53.21477190840114,"foo":"bar","time":1379688093.124732},
{"name":"mno","price":40.99364022416619,"foo":"bar","time":1379688093.124758},
{"name":"pqr","price":5.918738444452265,"foo":"bar","time":1379688093.124803},
{"name":"stu","price":45.09391752439902,"foo":"bar","time":1379688093.124831},
{"name":"vwx","price":63.08947792357426,"foo":"bar","time":1379688093.124862},
{"name":"yz_","price":94.04921035056373,"foo":"bar","time":1379688093.124894}
]

我添加了时间戳,以便您可以看到每一行都是单独处理的, AND 可以让您了解行的处理速度。当然,这是一个微小的内存数据库,它没有内容的网络I / O,但通过切换到合理数据库主机上的数据库的正常网络连接也应该非常快。告诉ORM以块的形式读取数据库可以加快处理速度,因为DBM将能够返回更大的块以更有效地填充数据包。您必须尝试确定所需的块大小,因为它会因您的网络,主机和记录大小而异。

在处理企业级数据库时,您的原始设计并不好,尤其是当您的硬件资源有限时。多年来,我们已经学会了如何解析BIG数据库,这使得20,000个行表显得微不足道。 VM切片现在很常见,我们将它们用于运算,因此它们通常是过去的PC:具有小内存占用和小驱动器的单CPU。我们无法击败它们或者它们会成为瓶颈,因此我们必须将数据分解为最小的原子碎片。

强调数据库设计:将JSON存储在数据库中是一个值得怀疑的做法。 DBM现在可以显示行的JSON,YAML和XML表示,但强制DBM在存储的JSON,YAML或XML字符串内搜索是处理速度的主要因素,因此除非您还具有等效的查找数据,否则不惜一切代价避免它在单独的字段中编制索引,以便您的搜索速度达到最高速度。如果数据在单独的字段中可用,那么做好的&#39;数据库查询,在DBM中调整或您选择的脚本语言,以及发送按摩数据变得更加容易。

答案 1 :(得分:0)

可以通过JSON::StreamYajl::FFI宝石。你必须编写自己的回调。有关如何执行此操作的一些提示可以在herehere找到。

面对类似的问题,我创建了json-streamer gem,这将使您无需创建自己的回调。之后,它会逐个将每个对象从内存中删除。然后,您可以按预期将这些传递给另一个IO对象。

如果您设法尝试,请告诉我。

答案 2 :(得分:0)

有一个名为oj的库正是这样做的。它可以进行解析和生成。例如,对于解析,您可以使用Oj::Doc

Oj::Doc.open('[3,[2,1]]') do |doc|
    result = {}
    doc.each_leaf() do |d|
        result[d.where?] = d.fetch()
    end
    result
end #=> ["/1" => 3, "/2/1" => 2, "/2/2" => 1]

您甚至可以使用doc.move(path)回溯文件。似乎很灵活。

要编写文档,可以使用Oj::StreamWriter

require 'oj'

doc = Oj::StreamWriter.new($stdout)

def write_item(doc, item)
  doc.push_object

  doc.push_key "type"
  doc.push_value "item"

  doc.push_key "value"
  doc.push_value item

  doc.pop
end

def write_array(doc, array)
  doc.push_object

  doc.push_key "type"
  doc.push_value "array"

  doc.push_key "value"
  doc.push_array
  array.each do |item|
    write_item(doc, item)
  end
  doc.pop

  doc.pop
end

write_array(doc, [{a: 1}, {a: 2}]) #=> {"type":"array","value":[{"type":"item","value":{":a":1}},{"type":"item","value":{":a":2}}]}