我的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
答案 0 :(得分:1)
代码存在许多问题。以下是一些改进方法:
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”。
有很多复制可能会被干掉。而不是:
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 }