我的Rails应用程序中有一个Rake任务,它会查找XML文件的文件夹,对其进行解析,并将其保存到数据库中。代码工作正常,但我有大约2100个文件,总计1.5GB,处理速度非常慢,7小时内大约有400个文件。每个XML文件中大约有600-650个契约,每个契约可以有0到n个附件。我没有粘贴所有的价值,但每份合同都有25个价值。
为了加快这个过程,我使用了Activerecord的Importgem,所以我为每个文件构建一个数组,并在解析整个文件时构建。我大量导入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
代码中存在许多问题。以下是一些改进方法:
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
将按您的预期工作。
另请参阅"如何避免在抓取时连接来自节点的所有文本"。
有很多复制可能是干的。取而代之的是:
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 }