ruby-on-rails - 如何解决 Nokogiri 解析缓慢的问题

标签 ruby-on-rails ruby xml nokogiri

我的 Rails 应用程序中有一个 Rake 任务,它会在文件夹中查找 XML 文件,对其进行解析,然后将其保存到数据库中。代码工作正常,但我有大约 2100 个文件,总计 1.5GB,处理速度非常慢,7 小时内处理大约 400 个文件。每个 XML 文件中大约有 600-650 个契约(Contract),每个契约(Contract)可以有 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 href="https://stackoverflow.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="72135c110008111d1c06001311062d1b164f32111d1c5c1b16" rel="noreferrer noopener nofollow">[email protected]</a>
                    @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

最佳答案

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

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 ”。

有很多复制可以进行 DRY。而不是这个:

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 }

关于ruby-on-rails - 如何解决 Nokogiri 解析缓慢的问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41402938/

相关文章:

javascript - 使用 javascript 优雅地修改现有链接的参数

ruby-on-rails - 消除 Rails 3 中的弃用警告

ruby-on-rails - 有没有更好的方法来取消当前请求并重定向到Rails中的错误页面?

java - 从 2000 行文件中提取数据的 Java XML 解析器(在移动设备上)

php - 如何通过单击按钮从服务器下载 XML 文件?

ruby-on-rails - Rspec Net::SMTPServerBusy: 454 4.7.1: 中继访问被拒绝

ruby-on-rails - 改变值(value)观的问题

ruby-on-rails - Rails 迁移 - Mysql2::错误:指定的 key 太长;最大 key 长度为 767 字节

ruby-on-rails - 处理 Rails 3 中的未知操作

jquery - 使用 jquery 将 XML 属性转为 HTML 选择值