如何修复缓慢的Nokogiri解析

时间:2016-12-30 21:32:18

标签: ruby-on-rails ruby xml nokogiri

我的Rails应用程序中有一个Rake任务,它查找XML文件的文件夹,解析它并将其保存到数据库中。代码工作正常,但我有大约2100GB的文件总共1.5GB,处理速度很慢,7小时内大约有400个文件。每个XML文件中大约有600-650个合同,每个合同可以有0到n个附件。我没有粘贴所有值,但每个合约都有25个值。

为了加快这个过程,我使用了Activerecord的Import gem,所以我正在为每个文件构建一个数组,并解析整个文件。我对Postgres大量进口。只有在找到记录时才会直接更新和/或插入新附件,但这类似于100000条记录中的1条。这有点帮助,而不是每个合同做新的记录,但现在我看到缓慢的部分是XML解析。在解析时我能不能看看我做错了什么?

当我尝试打印我正在构建的数组时,缓慢的部分直到它加载/解析整个文件并开始按数组打印数组。这就是为什么我认为探测器的速度是解析,因为Nokogiri在开始之前加载整个XML。

require 'nokogiri'
require 'pp'
require "activerecord-import/base"

ActiveRecord::Import.require_adapter('postgresql')
namespace :loadcrz2 do
  desc "this task load contracts from crz xml files to DB"
  task contracts: :environment do
    actual_dir = File.dirname(__FILE__).to_s
    Dir.foreach(actual_dir+'/../../crzfiles') do |xmlfile|

        next if xmlfile == '.' or xmlfile == '..' or xmlfile == 'archive'

         page = Nokogiri::XML(open(actual_dir+"/../../crzfiles/"+xmlfile))
         puts xmlfile
         cons = page.xpath('//contracts/*')
         contractsarr = []
         @c =[]
         cons.each do |contract|
            name = contract.xpath("name").text
            crzid = contract.xpath("ID").text
            procname = contract.xpath("procname").text
            conname = contract.xpath("contractorname").text
            subject = contract.xpath("subject").text
            dateeff = contract.xpath("dateefficient").text
            valuecontract = contract.xpath("value").text

            attachments = contract.xpath('attachments/*')
            attacharray = []
            attachments.each do |attachment|
                attachid = attachment.xpath("ID").text
                attachname = attachment.xpath("name").text
                doc = attachment.xpath("document").text
                size = attachment.xpath("size").text

                arr = [attachid,attachname,doc,size]
                attacharray.push arr
            end
            @con = Crzcontract.find_by_crzid(crzid)
            if @con.nil?
                @c=Crzcontract.new(:crzname => name,:crzid => crzid,:crzprocname=>procname,:crzconname=>conname,:crzsubject=>subject,:dateeff=>dateeff,:valuecontract=>valuecontract)
            else
                @con.crzname = name
                @con.crzid = crzid
                @con.crzprocname=procname
                @con.crzconname=conname
                @con.crzsubject=subject
                @con.dateeff=dateeff
                @con.valuecontract=valuecontract
                @con.save!
            end
            attacharray.each do |attar|
            attachid=attar[0]
            attachname=attar[1]
            doc=attar[2]
            size=attar[3]

                @at = Crzattachment.find_by_attachid(attachid)
                if @at.nil?

                if @con.nil?
                    @c.crzattachments.build(:attachid=>attachid,:attachname=>attachname,:doc=>doc,:size=>size)
                else
                    @a=Crzattachment.new
                    @a.attachid = attachid
                    @a.attachname = attachname
                    @a.doc = doc
                    @a.size = size
                    @a.crzcontract_id=@con.id
                    @a.save!
                end
                end

         end
            if @c.present?
            contractsarr.push @c
            end
            #p @c

         end
         #p contractsarr

         puts "done"
         if contractsarr.present?
         Crzcontract.import contractsarr, recursive: true
         end
         FileUtils.mv(actual_dir+"/../../crzfiles/"+xmlfile, actual_dir+"/../../crzfiles/archive/"+xmlfile)


        end
  end
end

1 个答案:

答案 0 :(得分:1)

代码存在许多问题。以下是一些改进方法:

actual_dir = File.dirname(__FILE__).to_s

请勿使用to_sdirname已经返回一个字符串。

actual_dir+'/../../crzfiles',重复使用带有和不带尾随路径分隔符的情况。不要让Ruby一遍又一遍地重建连接字符串。而是定义一次,但利用Ruby建立完整路径的能力:

File.absolute_path('../../bar', '/path/to/foo') # => "/path/bar"

所以使用:

actual_dir = File.absolute_path('../../crzfiles', __FILE__)

然后仅参考actual_dir

Dir.foreach(actual_dir)

这很笨重:

next if xmlfile == '.' or xmlfile == '..' or xmlfile == 'archive'

我会这样做:

next if (xmlfile[0] == '.' || xmlfile == 'archive')

甚至:

next if xmlfile[/^(?:\.|archive)/]

比较这些:

'.hidden'[/^(?:\.|archive)/] # => "."
'.'[/^(?:\.|archive)/] # => "."
'..'[/^(?:\.|archive)/] # => "."
'archive'[/^(?:\.|archive)/] # => "archive"
'notarchive'[/^(?:\.|archive)/] # => nil
'foo.xml'[/^(?:\.|archive)/] # => nil

如果模式以'.'开头或等于'archive',则该模式将返回真值。它不是可读的,但它很紧凑。我建议使用复合条件测试。

在某些地方,你正在连接xmlfile,所以再次让Ruby做一次:

xml_filepath = File.join(actual_dir,xmlfile)

将支持您运行的任何操作系统的文件路径分隔符。然后使用xml_filepath而不是连接名称:

xml_filepath = File.join(actual_dir, xmlfile)))
page = Nokogiri::XML(open(xml_filepath))

[...]

FileUtils.mv(xml_filepath, File.join(actual_dir, "archive", xmlfile)

join是一个很好的工具,所以要充分利用它。它不仅仅是连接字符串的另一个名称,因为它还知道正确的分隔符,用于运行代码的操作系统。

您使用了很多实例:

xpath("some_selector").text

不要那样做。 xpath,以及csssearch返回一个NodeSet,而text在NodeSet上使用时可能是邪恶的,会让你感到非常陡峭滑坡。考虑一下:

require 'nokogiri'

doc = Nokogiri::XML(<<EOT)
<root>
  <node>
    <data>foo</data>
  </node>
  <node>
    <data>bar</data>
  </node>
</root>
EOT

doc.search('//node/data').class # => Nokogiri::XML::NodeSet
doc.search('//node/data').text # => "foobar"

将文本连接到'foobar'不能轻易拆分,这是我们经常在问题中看到的问题。

如果由于使用searchxpathcss而希望返回NodeSet,请执行此操作:

doc.search('//node/data').map(&:text) # => ["foo", "bar"]

如果您在特定节点之后,最好使用atat_xpathat_css,因为text可以按预期工作。

另请参阅“How to avoid joining all text from Nodes when scraping”。

有很多复制可能会被干掉。而不是:

name = contract.xpath("name").text
crzid = contract.xpath("ID").text
procname = contract.xpath("procname").text

您可以执行以下操作:

name, crzid, procname = [
  'name', 'ID', 'procname'
].map { |s| contract.at(s).text }