[英]Parsing Large XML files w/ Ruby & Nokogiri
我有一个大型 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 中减去该数字以得到一个输出,内容为“Interest_Category 计数:n,所有其他计数: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>
具有七个类别之一。 首先,输出:
创建 XML 文件:1.7s
通过 SAX 计数:12.9s
创建DOM:1.6s
通过 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 使用情况。
我的机器上有足够的内存来处理 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 的计数器看起来像这样:
# 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
首先,让我们关注这段代码:
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 的 Hash,以及一些可以对其调用的方法。 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"]
对于这么大的文件,我建议使用 SAX 解析器而不是 DOM 解析器。 Nokogiri 内置了一个不错的 SAX 解析器: http ://nokogiri.org/Nokogiri/XML/SAX.html
SAX 处理大文件的方式非常适合大文件,因为它没有构建巨大的 DOM 树,这在您的情况下是多余的; 您可以在事件触发时建立自己的结构(例如,用于计算节点)。
您可以通过将代码更改为以下内容来显着减少执行时间。 只需将“99”更改为您要检查的任何类别。:
require 'rubygems'
require 'nokogiri'
require 'open-uri'
icount = 0
xmlfeed = Nokogiri::XML(open("test.xml"))
items = xmlfeed.xpath("//item")
items.each do |item|
text = item.children.children.first.text
if ( text =~ /99/ )
icount += 1
end
end
othercount = xmlfeed.xpath("//totalcount").inner_text.to_i - icount
puts icount
puts othercount
这在我的机器上花费了大约三秒钟。 我认为您犯的一个关键错误是您选择了“项目”迭代而不是创建“项目”节点的集合。 这使您的迭代代码既笨拙又缓慢。
你可能想试试这个 - https://github.com/amolpujari/reading-huge-xml
HugeXML.read xml, elements_lookup do |element|
# => element{ :name, :value, :attributes}
end
我也尝试过使用ox 。
查看 Greg Weber 的 Paul Dix 萨克斯机 gem 版本: http : //blog.gregweber.info/posts/2011-06-03-high-performance-rb-part1
使用 SaxMachine 解析大文件似乎正在将整个文件加载到内存中
sax-machine 使代码更简单; Greg 的变体使它成为流。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.