6

我分 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功能。一个接一个地写入所有数据集数组[...][...][...],在每个数组之后,我回到开头并用 覆盖][,有效地手动连接数组。

4

3 回答 3

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 并证明原始数据是可恢复的。从数据库中检索到的每一行都应该是您要构建的整个 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}
]

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

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

喋喋不休地谈论数据库设计:将 JSON 存储在数据库中是一种有问题的做法。现在的 DBM 可以输出 JSON、YAML 和 XML 表示的行,但是强制 DBM 在存储的 JSON、YAML 或 XML 字符串中搜索是处理速度的主要影响,因此除非您也有等效的查找数据,否则不惜一切代价避免它索引在单独的字段中,因此您的搜索速度尽可能快。如果数据在单独的字段中可用,那么进行良好的数据库查询、调整 DBM 或您选择的脚本语言以及发出经过处理的数据会变得容易得多。

于 2013-09-19T19:35:08.100 回答
1

可以通过JSON::StreamYajl::FFI gems。不过,您将不得不编写自己的回调。可以在此处此处找到有关如何执行此操作的一些提示。

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

于 2016-05-29T00:39:19.723 回答
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}}]}

于 2020-05-25T10:30:15.273 回答