gpt4 book ai didi

ruby - 使用 Ruby 和 Nokogiri 解析大型 XML 文件

转载 作者:数据小太阳 更新时间:2023-10-29 01:43:50 25 4
gpt4 key购买 nike

我有一个很大的 XML 文件(大约 10K 行),我需要定期解析这种格式的文件:

<summarysection>
<totalcount>10000</totalcount>
</summarysection>
<items>
<item>
<cat>Category</cat>
<name>Name 1</name>
<value>Val 1</value>
</item>
...... 10,000 more times
</items>

我想做的是使用 nokogiri 解析每个单独的节点,以计算一个类别中的项目数量。然后,我想从 total_count 中减去该数字,得到一个输出“Count of Interest_Category: n, Count of All Else: z”。

现在这是我的代码:

#!/usr/bin/ruby

require 'rubygems'
require 'nokogiri'
require 'open-uri'

icount = 0
xmlfeed = Nokogiri::XML(open("/path/to/file/all.xml"))
all_items = xmlfeed.xpath("//items")

all_items.each do |adv|
if (adv.children.filter("cat").first.child.inner_text.include? "partofcatname")
icount = icount + 1
end
end

othercount = xmlfeed.xpath("//totalcount").inner_text.to_i - icount

puts icount
puts othercount

这似乎可行,但速度很慢!我说 10,000 件元素需要 10 多分钟。有一个更好的方法吗?我做某事的方式不是很理想吗?

最佳答案

这是一个比较 SAX 解析器计数与基于 DOM 的计数的示例,计数为 500,000 <item>具有七个类别之一。一、输出:

Create XML file: 1.7s
Count via SAX: 12.9s
Create DOM: 1.6s
Count via DOM: 2.5s

这两种技术都会产生相同的哈希值,计算每个类别的数量:

{"Cats"=>71423, "Llamas"=>71290, "Pigs"=>71730, "Sheep"=>71491, "Dogs"=>71331, "Cows"=>71536, "Hogs"=>71199}

SAX 版本需要 12.9 秒来计算和分类,而 DOM 版本只需要 1.6 秒来创建 DOM 元素,多 2.5 秒来查找和分类所有 <cat>值。 DOM 版本的速度大约是原来的 3 倍!

……但这还不是全部。我们还必须查看 RAM 使用情况。

  • 对于 500,000 个项目,SAX(12.9 秒)的峰值为 238MB RAM; DOM (4.1s) 峰值为 1.0GB。
  • 对于 1,000,000 个项目,SAX(25.5 秒)在 243MB RAM 时达到峰值; DOM (8.1s) 峰值为 2.0GB。
  • 对于 2,000,000 个项目,SAX (55.1s) 在 250MB RAM 时达到峰值; DOM (???) 峰值为 3.2GB。

我的机器上有足够的内存来处理 1,000,000 个项目,但在处理 2,000,000 个项目时我用完了 RAM,不得不开始使用虚拟内存。即使使用 SSD 和快速机器,我还是让 DOM 代码运行了将近十分钟,然后才最终将其终止。

您报告的时间很长很可能是因为您的 RAM 耗尽并且作为虚拟内存的一部分不断地访问磁盘。如果您可以将 DOM 装入内存,请使用它,因为它很快。但是,如果不能,那么您真的必须使用 SAX 版本。

测试代码如下:

require 'nokogiri'

CATEGORIES = %w[ Cats Dogs Hogs Cows Sheep Pigs Llamas ]
ITEM_COUNT = 500_000

def test!
create_xml
sleep 2; GC.start # Time to read memory before cleaning the slate
test_sax
sleep 2; GC.start # Time to read memory before cleaning the slate
test_dom
end

def time(label)
t1 = Time.now
yield.tap{ puts "%s: %.1fs" % [ label, Time.now-t1 ] }
end

def test_sax
item_counts = time("Count via SAX") do
counter = CategoryCounter.new
# Use parse_file so we can stream data from disk instead of flooding RAM
Nokogiri::HTML::SAX::Parser.new(counter).parse_file('tmp.xml')
counter.category_counts
end
# p item_counts
end

def test_dom
doc = time("Create DOM"){ File.open('tmp.xml','r'){ |f| Nokogiri.XML(f) } }
counts = time("Count via DOM") do
counts = Hash.new(0)
doc.xpath('//cat').each do |cat|
counts[cat.children[0].content] += 1
end
counts
end
# p counts
end

class CategoryCounter < Nokogiri::XML::SAX::Document
attr_reader :category_counts
def initialize
@category_counts = Hash.new(0)
end
def start_element(name,att=nil)
@count = name=='cat'
end
def characters(str)
if @count
@category_counts[str] += 1
@count = false
end
end
end

def create_xml
time("Create XML file") do
File.open('tmp.xml','w') do |f|
f << "<root>
<summarysection><totalcount>10000</totalcount></summarysection>
<items>
#{
ITEM_COUNT.times.map{ |i|
"<item>
<cat>#{CATEGORIES.sample}</cat>
<name>Name #{i}</name>
<name>Value #{i}</name>
</item>"
}.join("\n")
}
</items>
</root>"
end
end
end

test! if __FILE__ == $0

DOM 计数是如何工作的?

如果我们去掉一些测试结构,基于 DOM 的计数器看起来像这样:

# Open the file on disk and pass it to Nokogiri so that it can stream read;
# Better than doc = Nokogiri.XML(IO.read('tmp.xml'))
# which requires us to load a huge string into memory just to parse it
doc = File.open('tmp.xml','r'){ |f| Nokogiri.XML(f) }

# Create a hash with default '0' values for any 'missing' keys
counts = Hash.new(0)

# Find every `<cat>` element in the document (assumes one per <item>)
doc.xpath('//cat').each do |cat|
# Get the child text node's content and use it as the key to the hash
counts[cat.children[0].content] += 1
end

SAX 计数是如何工作的?

首先,让我们关注这段代码:

class CategoryCounter < Nokogiri::XML::SAX::Document
attr_reader :category_counts
def initialize
@category_counts = Hash.new(0)
end
def start_element(name,att=nil)
@count = name=='cat'
end
def characters(str)
if @count
@category_counts[str] += 1
@count = false
end
end
end

当我们创建此类的新实例时,我们会得到一个对象,该对象的所有值的哈希值默认为 0,并且有几个可以对其调用的方法。 SAX 解析器将在遍历文档时调用这些方法。

  • 每次 SAX 解析器看到一个新元素时,它都会调用 start_element这个类的方法。发生这种情况时,我们会根据该元素是否命名为“cat”设置一个标志(以便我们稍后可以找到它的名称)。

  • 每次 SAX 解析器处理一大块文本时,它都会调用 characters我们对象的方法。当发生这种情况时,我们检查我们看到的最后一个元素是否是一个类别(即 @count 是否设置为 true );如果是这样,我们使用这个文本节点的值作为类别名称,并向我们的计数器加一。

要将我们的自定义对象与 Nokogiri 的 SAX 解析器一起使用,我们这样做:

# Create a new instance, with its empty hash
counter = CategoryCounter.new

# Create a new parser that will call methods on our object, and then
# use `parse_file` so that it streams data from disk instead of flooding RAM
Nokogiri::HTML::SAX::Parser.new(counter).parse_file('tmp.xml')

# Once that's done, we can get the hash of category counts back from our object
counts = counter.category_counts
p counts["Pigs"]

关于ruby - 使用 Ruby 和 Nokogiri 解析大型 XML 文件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10588194/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com