9

次の形式の大きな 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 を使用して個々のノードのそれぞれを解析し、1 つのカテゴリ内のアイテムの量をカウントすることです。次に、その数値を 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分以上しゃべってます。これを行うより良い方法はありますか?私は最適とは言えない方法で何かをしていますか?

4

5 に答える 5

26

以下は、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"]
于 2012-05-15T02:48:53.727 に答える
4

このサイズのファイルには、DOM パーサーではなく SAX パーサーを使用することをお勧めします。Nokogiri には素晴らしい SAX パーサーが組み込まれています: http://nokogiri.org/Nokogiri/XML/SAX.html

SAXのやり方は、巨大なDOMツリーを構築しないという理由だけで、大きなファイルに適しています。これはあなたの場合はやり過ぎです。イベントが発生したときに独自の構造を構築できます (たとえば、ノードをカウントするため)。

于 2012-05-14T19:10:01.470 に答える
3

コードを次のように変更すると、実行時間を大幅に短縮できます。「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

私のマシンでは、これに約 3 秒かかりました。あなたが犯した主な誤りは、「アイテム」ノードのコレクションを作成する代わりに「アイテム」の反復を選択したことだと思います。これにより、反復コードが厄介で遅くなりました。

于 2012-05-14T19:20:10.017 に答える
0

Paul Dix の sax-machine gem の Greg Weber バージョンをチェックしてください: http://blog.gregweber.info/posts/2011-06-03-high-performance-rb-part1

SaxMachine で大きなファイルを解析すると、ファイル全体がメモリに読み込まれているようです

sax-machine はコードをはるかに単純にします。グレッグのバリアントはそれをストリーミングします。

于 2013-08-03T13:23:09.950 に答える