Разбор больших XML-файлов с 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, чтобы получить вывод, который читает "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 минут для 10 000 деталей. Есть ли лучший способ сделать это? Я делаю что-то не лучшим образом?

5 ответов


вы можете значительно сократить время выполнения, изменив код на следующий. Просто измените " 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

это заняло около трех секунд на моей машине. Я думаю, что ключевая ошибка, которую вы сделали, заключалась в том, что вы выбрали итерацию "items" вместо создания коллекции узлов "item". Это сделало ваш итерационный код неудобным и медленным.


вот пример сравнения количества парсеров SAX с количеством на основе DOM, считая 500,000 <item>С одной из семи категорий. Во-первых, вывод:

создать XML-файл: 1.7 s
Счет через саксофон: 12.9 s
Создать DOM: 1.6 s
Счет через DOM: 2.5 s

оба метода производят один и тот же хэш, подсчитывая количество каждой категории:

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

версия SAX занимает 12.9 s, чтобы подсчитать и классифицировать, в то время как версия DOM занимает всего 1,6 С для создания элементов DOM и 2,5 С больше, чтобы найти и классифицировать все <cat> значения. Версия DOM примерно в 3 раза быстрее!

...но это еще не вся история. Мы также должны посмотреть на использование ОЗУ.

  • для 500,000 пунктов SAX (12.9 s) пики на 238MB ОЗУ; DOM (4.1 s) пики на 1.0 GB.
  • для 1,000,000 пунктов Sax (25.5 s) пики на 243MB ОЗУ; DOM (8.1 s) пики на 2.0 GB.
  • для 2,000,000 пунктов SAX (55.1 s) пики на 250 МБ ОЗУ; DOM (???) пики на 3,2 ГБ.

у меня было достаточно памяти на моей машине, чтобы обрабатывать 1 000 000 элементов, но в 2 000 000 я исчерпал ОЗУ и должен был начать использовать виртуальную память. Даже с SSD и быстрой машиной я позволил коду DOM работать почти десять минут, прежде чем, наконец, убить его.

очень вероятно, что долгое время вы сообщаете, потому что у вас заканчивается ОЗУ и вы нажимаете диск непрерывно как часть виртуальной памяти. Если вы можете поместить 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__ == 

как работает подсчет 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

как работает подсчет саксофона?

во-первых, давайте сосредоточимся на этом код:

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, мы делаем следующее:

# 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, которое в вашем случае излишне; вы можете создавать свои собственные структуры, когда события срабатывают (например, для подсчета узлов).


проверьте версию Грега Вебера sax-machine gem пола Дикса: http://blog.gregweber.info/posts/2011-06-03-high-performance-rb-part1

разбор большого файла с помощью SaxMachine, похоже, загружает весь файл в память

sax-машина делает код намного проще; вариант Грега делает его потоковым.


вы можете попробовать это - https://github.com/amolpujari/reading-huge-xml

HugeXML.read xml, elements_lookup do |element| # => element{ :name, :value, :attributes} end

Я также попытался с помощью ox