这里有一个例子,比较了使用SAX解析器和基于DOM的解析器进行计数的结果,对500,000个具有七个类别之一的<item>
进行计数。首先是输出:
创建XML文件:1.7秒
通过SAX进行计数:12.9秒
创建DOM:1.6秒
通过DOM进行计数:2.5秒
两种技术都生成相同的哈希值,以计算出每个类别的数量:
{"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.9s)在238MB RAM峰值处;DOM(4.1s)峰值为1.0GB。
- 对于1,000,000个项目,SAX(25.5s)在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
test_sax
sleep 2; GC.start
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
Nokogiri::HTML::SAX::Parser.new(counter).parse_file('tmp.xml')
counter.category_counts
end
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
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 的计数器看起来像这样:
doc = File.open('tmp.xml','r'){ |f| Nokogiri.XML(f) }
counts = Hash.new(0)
doc.xpath('//cat').each do |cat|
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解析器运行文档时,它将调用这些方法。
要在Nokogiri的SAX解析器中使用我们的自定义对象,请执行以下操作:
counter = CategoryCounter.new
Nokogiri::HTML::SAX::Parser.new(counter).parse_file('tmp.xml')
counts = counter.category_counts
p counts["Pigs"]