我的 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_s
。 dirname
已经返回一个字符串。
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
以及 css
和 search
返回一个 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”无法轻易拆分,这是我们在问题中经常看到的问题。
如果您希望因使用 search
、xpath
或 css
恢复 NodeSet,请执行此操作:
doc.search('//node/data').map(&:text) # => ["foo", "bar"]
如果您在特定节点之后,最好使用 at
、at_xpath
或 at_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/