使用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::ReaderNokogiri::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 

基于这个博客