2013-11-08 5 views
1

Итак, я пытаюсь разобрать файл с размером строки 400k +, используя Nokogiri.Разбор большого XML с Nokogiri

Файл XML имеет этот базовый формат:

<?xml version="1.0" encoding="windows-1252"?> 
<JDBOR date="2013-09-01 04:12:31" version="1.0.20 [2012-12-14]" copyright="Orphanet (c) 2013"> 
<DisorderList count="6760"> 

    *** Repeated Many Times *** 
    <Disorder id="17601"> 
    <OrphaNumber>166024</OrphaNumber> 
    <Name lang="en">Multiple epiphyseal dysplasia, Al-Gazali type</Name> 
    <DisorderSignList count="18"> 
    <DisorderSign> 
     <ClinicalSign id="2040"> 
     <Name lang="en">Macrocephaly/macrocrania/megalocephaly/megacephaly</Name> 
     </ClinicalSign> 
     <SignFreq id="640"> 
     <Name lang="en">Very frequent</Name> 
     </SignFreq> 
    </DisorderSign> 
    </Disorder> 
    *** Repeated Many Times *** 

</DisorderList> 
</JDBOR> 

Вот код, который я создал, чтобы разобрать и вернуть каждый DisorderSign идентификатор и имя в базу данных:

require 'nokogiri' 

sympFile = File.open("Temp.xml") 
@doc = Nokogiri::XML(sympFile) 
sympFile.close() 
symptomsList = [] 

@doc.xpath("////DisorderSign").each do |x| 
    signId = x.at('ClinicalSign').attribute('id').text()  
    name = x.at('ClinicalSign').element_children().text() 
    symptomsList.push([signId, name]) 
end 

symptomsList.each do |x| 
    Symptom.where(:name => x[1], :signid => Integer(x[0])).first_or_create 
end 

Это работает идеально на тестовых файлах, которые я использовал, хотя они были намного меньше, около 10000 строк.

Когда я пытаюсь запустить это на большом XML-файле, он просто не заканчивается. Я оставил его в одночасье, и это казалось просто блокировкой. Есть ли какая-то фундаментальная причина того, что код, который я написал, сделает эту память интенсивной или неэффективной? Я понимаю, что я храню все возможные пары в списке, но это не должно быть достаточно большим, чтобы заполнить память.

Благодарим за помощь.

+2

Я не думаю, что '//// DisorderSign' делает то, что вы думаете, что он делает. –

+2

Возможно, вам удастся получить дополнительную информацию, если вы обнаружите *, где * она блокируется. Это во время создания DOM (т. Е. Строки «Nokogiri :: XML»)? Вы всегда можете попробовать интерфейсы SAX или Reader. Это во время разбора? Попробуйте исправить «//// DisorderSign» в «// DisorderSign» или еще лучше использовать полный путь к DisorderSign и полностью избавиться от «//». –

+0

У меня еще не было возможности проверить эти ответы, но я обязательно сделаю это, когда у меня появится такая возможность. Что касается вашего вопроса, я знаю, что он не блокируется во время создания DOM, через несколько секунд он проходит через это. Я попытаюсь использовать полный путь и отчитываться с любыми результатами, которые я получаю от этого, и с использованием ответов ниже. –

ответ

4

Я вижу несколько возможных проблем. Прежде всего, это:

@doc = Nokogiri::XML(sympFile) 

будет хлебать весь XML-файл в память в качестве своего рода структуры данных libxml2 и что, вероятно, будет больше, чем исходный файл XML.

Тогда вы делаете такие вещи, как это:

@doc.xpath(...).each 

Это не может быть достаточно умным, чтобы произвести перечислитель, который только поддерживает указатель на внутреннюю форму XML, он может производить копии все, когда он строит NodeSet, который возвращает xpath. Это даст вам еще одну копию большей части расширенной версии XML. Я не уверен, как много копирование и построение массива происходит здесь, но есть место для справедливого количества памяти и процессорных издержек, даже если он не копирует дубликаты всего.

Затем вы делаете вашу копию того, что вы заинтересованы в:

symptomsList.push([signId, name]) 

и, наконец, перебрать этот массив:

symptomsList.each do |x| 
    Symptom.where(:name => x[1], :signid => Integer(x[0])).first_or_create 
end 

я считаю, что SAX parsers лучше работать с большими наборами данных, но они являются более громоздкими для работы. Вы можете попробовать создать свой собственный SAX парсер что-то вроде этого:

class D < Nokogiri::XML::SAX::Document 
    def start_element(name, attrs = [ ]) 
    if(name == 'DisorderSign') 
     @data = { } 
    elsif(name == 'ClinicalSign') 
     @key  = :sign 
     @data[@key] = '' 
    elsif(name == 'SignFreq') 
     @key  = :freq 
     @data[@key] = '' 
    elsif(name == 'Name') 
     @in_name = true 
    end 
    end 

    def characters(str) 
    @data[@key] += str if(@key && @in_name) 
    end 

    def end_element(name, attrs = [ ]) 
    if(name == 'DisorderSign') 
     # Dump @data into the database here. 
     @data = nil 
    elsif(name == 'ClinicalSign') 
     @key = nil 
    elsif(name == 'SignFreq') 
     @key = nil 
    elsif(name == 'Name') 
     @in_name = false 
    end 
    end 
end 

Структура должна быть довольно ясно: вы следите за открытие элементов, которые вас интересуют и сделать немного бухгалтерии установить, когда do, затем кешируйте строки, если вы находитесь внутри элемента, который вам нужен, и, наконец, очистите и обработайте данные по мере закрытия элементов.Вы работаете в базе данных, замените

# Dump @data into the database here. 

комментарий.

Эта структура позволяет легко наблюдать за элементами <Disorder id="17601">, чтобы вы могли отслеживать, насколько далеко вы ушли. Таким образом, вы можете остановить и перезапустить импорт с небольшими изменениями в вашем скрипте.

+0

Когда я получу момент, я попытаюсь сделать это и отредактировать этот комментарий с результатами. –

+0

Отметьте мой ответ на мой оригинальный пост. Хотя это и решило мою проблему, я вижу, что синтаксический анализатор SAX имеет больше смысла для большего файла и может в конечном итоге изменить его на все, спасибо за ваши усилия, пытаясь помочь мне. –

+0

Ах, я пропустил дерево для леса. Вы можете преобразовать этот комментарий в ответ и принять свой собственный ответ (после 24-часовой задержки AFAIK). –

2

Скорее всего, вы исчерпали память, потому что symptomsList становится слишком большим в размере памяти. Почему бы не выполнить SQL в цикле xpath?

require 'nokogiri' 

sympFile = File.open("Temp.xml") 
@doc = Nokogiri::XML(sympFile) 
sympFile.close() 

@doc.xpath("////DisorderSign").each do |x| 
    signId = x.at('ClinicalSign').attribute('id').text()  
    name = x.at('ClinicalSign').element_children().text() 
    Symptom.where(:name => name, :signid => signId.to_i).first_or_create 
end 

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

+0

Хотя это не исправило проблему, определенно лучше выполнить SQL в цикле xpath, спасибо. –

3

SAX Parser определенно то, что вы хотите использовать. Если вы что-то вроде меня и не можете дживить с документацией Nokogiri, есть потрясающий драгоценный камень под названием Saxerator, который делает этот процесс очень простым.

Пример для того, что вы пытаетесь сделать -

require 'saxerator' 

parser = Saxerator.parser(Temp.xml) 


parser.for_tag(:DisorderSign).each do |sign| 
    signId = sign[:ClinicalSign][:id] 
    name = sign[:ClinicalSign][:name] 
    Symtom(:name => name, :id => signId).create! 
end 
0

Вы можете также использовать Nokogiri::XML::Reader. Это более интенсивный объем памяти, чем парсер Nokogiri::XML::SAX, но вы можете сохранить структуру XML, e.x.

class NodeHandler < Struct.new(:node) 
    def process 
    # Node processing logic 
    #e.x. 
    signId = node.at('ClinicalSign').attribute('id').text()  
    name = node.at('ClinicalSign').element_children().text() 

    end 
end 


Nokogiri::XML::Reader(File.open('./test/fixtures/example.xml')).each do |node| 
    if node.name == 'DisorderSign' && node.node_type == Nokogiri::XML::Reader::TYPE_ELEMENT 
    NodeHandler.new(
     Nokogiri::XML(node.outer_xml).at('./DisorderSign') 
    ).process 
    end 
end 

На основе этого blog

Смежные вопросы