Разбор больших 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