以下は、SAX パーサーのカウントと DOM ベースのカウントを比較する例で<item>
、7 つのカテゴリのいずれかで 500,000 秒をカウントしています。まず、出力:
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 バージョンでは DOM 要素の作成に 1.6 秒しかかからず、すべての<cat>
値の検索と分類にさらに 2.5 秒かかります。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.1 秒) は 250MB の RAM でピークに達します。DOM ( ??? ) は 3.2GB でピークに達します。
マシンには 1,000,000 アイテムを処理するのに十分なメモリがありましたが、2,000,000 で RAM が不足し、仮想メモリを使い始めなければなりませんでした。SSD と高速なマシンでも、DOM コードをほぼ 10 分間実行してから、最終的に終了させました。
報告されている時間が長いのは、RAM が不足していて、仮想メモリの一部として継続的にディスクにアクセスしているためである可能性が非常に高いです。DOM がメモリに収まる場合は、FAST であるため使用してください。ただし、それができない場合は、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 になる Hash を持つオブジェクトと、その上で呼び出すことができるいくつかのメソッドが取得されます。SAX パーサーは、ドキュメントを処理する際にこれらのメソッドを呼び出します。
SAX パーサーは新しい要素を検出するたびに、start_element
このクラスのメソッドを呼び出します。その場合、この要素の名前が「cat」かどうかに基づいてフラグを設定します (後でその名前を見つけることができるようにするため)。
SAX パーサーがテキストのチャンクを丸呑みするたびcharacters
に、オブジェクトのメソッドを呼び出します。その場合、最後に見た要素がカテゴリであったかどうか (つまり、@count
が に設定されているかどうか) を確認しtrue
ます。その場合、このテキスト ノードの値をカテゴリ名として使用し、カウンタに 1 つ追加します。
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"]