Ruby 使用Nokogiri解析大型XML

Ruby 使用Nokogiri解析大型XML,ruby,xml,ruby-on-rails-4,nokogiri,Ruby,Xml,Ruby On Rails 4,Nokogiri,所以我尝试使用Nokogiri解析一个400k+行的XML文件 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

所以我尝试使用Nokogiri解析一个400k+行的XML文件

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>
这在我使用的测试文件上非常有效,尽管它们小得多,大约10000行

当我尝试在大型XML文件上运行此操作时,它根本没有完成。我把它开了一夜,它好像就锁起来了。我写的代码会导致内存密集或效率低下,有什么根本原因吗?我意识到我将每一对都存储在一个列表中,但这个列表不应该太大,无法填满内存


谢谢您的帮助。

您可能内存不足,因为症状列表的内存太大。为什么不在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

文件可能太大,缓冲区无法处理。在这种情况下,您可以将其拆分为较小的临时文件,并单独处理它们。

您可能会耗尽内存,因为症状列表的内存太大。为什么不在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

文件可能太大,缓冲区无法处理。在这种情况下,您可以将其拆分为较小的临时文件,然后分别进行处理。

我发现了一些可能的问题。首先,这是:

@doc = Nokogiri::XML(sympFile)
将整个XML文件作为某种libxml2数据结构读入内存,这可能比原始XML文件大

然后你会这样做:

@doc.xpath(...).each
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
这可能不够聪明,无法生成一个只维护指向XML内部形式的指针的枚举器,它可能在构建xpath返回的节点集时生成了所有内容的副本。这将为您提供大部分扩展内存版本XML的另一个副本。我不确定这里会发生多少复制和数组构造,但即使没有复制所有内容,也有相当大的内存和CPU开销空间

然后你复制你感兴趣的东西:

symptomsList.push([signId, name])
最后迭代该数组:

symptomsList.each do |x|
    Symptom.where(:name => x[1], :signid => Integer(x[0])).first_or_create
end
我发现这种方法在处理大型数据集时效果更好,但处理起来更麻烦。您可以尝试创建自己的SAX解析器,如下所示:

@doc.xpath(...).each
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
评论


这种结构使得观察元素变得非常容易,这样你就可以跟踪你已经走了多远。这样,您就可以停止并重新启动导入,只需对脚本进行一些小的修改。

我发现了一些可能的问题。首先,这是:

@doc = Nokogiri::XML(sympFile)
将整个XML文件作为某种libxml2数据结构读入内存,这可能比原始XML文件大

然后你会这样做:

@doc.xpath(...).each
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
这可能不够聪明,无法生成一个只维护指向XML内部形式的指针的枚举器,它可能在构建xpath返回的节点集时生成了所有内容的副本。这将为您提供大部分扩展内存版本XML的另一个副本。我不确定这里会发生多少复制和数组构造,但即使没有复制所有内容,也有相当大的内存和CPU开销空间

然后你复制你感兴趣的东西:

symptomsList.push([signId, name])
最后迭代该数组:

symptomsList.each do |x|
    Symptom.where(:name => x[1], :signid => Integer(x[0])).first_or_create
end
我发现这种方法在处理大型数据集时效果更好,但处理起来更麻烦。您可以尝试创建自己的SAX解析器,如下所示:

@doc.xpath(...).each
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
评论


这种结构使得观察元素变得非常容易,这样你就可以跟踪你已经走了多远。这样,您就可以停止并重新启动导入,只需对脚本进行一些小的修改。

SAX解析器正是您想要使用的。如果你和我一样,不能使用Nokogiri文档,那么有一个很棒的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

SAX解析器正是您想要使用的。如果你和我一样,不能使用Nokogiri文档,那么有一个很棒的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
您还可以使用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
基于此,您还可以使用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

基于此

我不认为/////DisordSign做了你认为它做的事。如果你找到它锁在哪里,你可能会得到更多信息。是在DOM构建期间,即Nokogiri::XML行吗?您始终可以尝试SAX或读卡器接口。是在解析过程中吗?尝试将////DisorderSign改为//DisorderSign,或者更好的方法是使用DisorderSign的完整路径并将//全部删除。我还没有机会查看这些答案,但我一定会这样做
等我有机会的时候。至于你的问题,我知道它在DOM构建过程中没有被锁定,它会在几秒钟内通过。我将尝试使用完整路径,并报告从该路径以及使用下面的答案得到的任何结果。将/////disordsign更改为///disordsign完全解决了我的问题。我试图遵循nokogiri提供的示例中的用法,但我一定是误解了什么。你能试着澄清一下为什么这解决了我的问题吗?另外,你是否可以在另一个帖子中写下你的回复,这样我就可以接受你的回答?我不认为/////DisordSign做了你认为它做的事情。如果你找到它锁在哪里,你可能会得到更多的信息。是在DOM构建期间,即Nokogiri::XML行吗?您始终可以尝试SAX或读卡器接口。是在解析过程中吗?尝试将////disordesign改为//disordesign,或者更好的方法是使用完整的路径来修改//disordesign,然后将其全部删除。我还没有机会查看这些答案,但我会在有机会时确保这样做。至于你的问题,我知道它在DOM构建过程中没有被锁定,它会在几秒钟内通过。我将尝试使用完整路径,并报告从该路径以及使用下面的答案得到的任何结果。将/////disordsign更改为///disordsign完全解决了我的问题。我试图遵循nokogiri提供的示例中的用法,但我一定是误解了什么。你能试着澄清一下为什么这解决了我的问题吗?还有,你是否可以在另一篇文章中写下你的回答,这样我就可以接受你的回答?虽然这没有解决问题,但在xpath循环中执行SQL肯定更好,谢谢。虽然这没有解决问题,但在xpath循环中执行SQL肯定更好,谢谢。等我有时间,我将尝试此操作,并用结果编辑此评论。检查我对原始帖子的回复。虽然这解决了我的问题,但我确实看到SAX解析器对于更大的文件更有意义,并且可能最终会将其更改为该文件,谢谢您的帮助。啊,我错过了森林之树。您可以将该注释转换为答案,并在24小时延迟后接受自己的答案。对于来自internet的文件,您将如何调用SAX解析器?还有,如果用case语句替换掉那些if呢?@cyriduchon Doris:The可以使用IO句柄,这样你就可以直接把它连接到你正在下载的文件上。你可以使用任何适合你的控制结构。当我有时间的时候,我会尝试这个,并用结果编辑这个评论。检查我对我的原始帖子的回复。虽然这解决了我的问题,但我确实看到SAX解析器对于更大的文件更有意义,并且可能最终会将其更改为该文件,谢谢您的帮助。啊,我错过了森林之树。您可以将该注释转换为答案,并在24小时延迟后接受自己的答案。对于来自internet的文件,您将如何调用SAX解析器?还有,如果用case语句替换掉那些if呢?@cyriduchon Doris:The可以使用IO句柄,这样你就可以直接把它连接到你正在下载的文件上。您可以使用任何适合您的控制结构。