使用Nokogiri解析大型XML
所以我试图使用Nokogiri解析400k +行XML文件。
XML文件具有以下基本格式:
*** Repeated Many Times *** 166024 Multiple epiphyseal dysplasia, Al-Gazali type Macrocephaly/macrocrania/megalocephaly/megacephaly Very frequent *** Repeated Many Times ***
这是我创建的代码,用于解析每个DisorderSign id和名称并将其返回到数据库中:
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文件上运行它时,它根本就没有完成。 我把它留在了一夜之间,它似乎只是锁定。 我编写的代码是否会导致内存密集或低效? 我意识到我将所有可能的对存储在列表中,但这不应该足以填满内存。
感谢您的任何帮助。
我看到一些可能的问题。 首先,这个:
@doc = Nokogiri::XML(sympFile)
将整个XML文件作为某种libxml2数据结构粘贴到内存中,并且可能比原始XML文件大。
然后你做这样的事情:
@doc.xpath(...).each
这可能不够智能,无法生成只维护指向XML内部forms的指针的枚举器,它可能在构建xpath
返回的NodeSet
时生成所有内容的副本。 这将为您提供XML的大部分扩展内存版本的另一个副本。 我不确定这里发生了多少复制和数组构造,但即使它没有复制所有内容,也有足够的空间来获得相当大的内存和CPU开销。
然后你复制你感兴趣的内容:
symptomsList.push([signId, name])
最后遍历该数组:
symptomsList.each do |x| Symptom.where(:name => x[1], :signid => Integer(x[0])).first_or_create end
我发现SAX解析器可以更好地处理大型数据集,但使用它们更加麻烦。 您可以尝试创建自己的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.
评论。
这种结构使得观察
元素变得非常容易,这样您就可以跟踪自己走了多远。 这样,您可以通过对脚本进行一些小的修改来停止并重新启动导入。
SAX Parser绝对是您想要使用的。 如果你和我一样,并且不能使用Nokogiri文档,那么有一个名为Saxerator的精彩gem可以让这个过程变得非常简单。
你想要做的一个例子 –
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
你可能因内存大小过大而导致内存不足。 为什么不在xpath循环中执行SQL?
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
文件可能太大而无法处理缓冲区。 在这种情况下,您可以将其切割成较小的临时文件并单独处理它们。
您还可以使用Nokogiri::XML::Reader
。 Nokogiri::XML::SAX
解析器的内存密集程度更高,但是你可以保留XML结构,例如
class NodeHandler < Struct.new(:node) def process # Node processing logic #ex 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
基于这个博客