如何读取多个XML文件,然后输出到具有相同XML文件名的多个CSV文件

时间:2019-06-11 19:31:07

标签: ruby xml csv nokogiri export-to-csv

我试图解析多个XML文件,然后将它们输出到CSV文件中,以列出正确的行和列。

我能够通过定义文件名一次处理一个文件,并将其具体输出到定义的输出文件名来实现:

File.open('H:/output/xmloutput.csv','w')

我想写入多个文件并使它们的名称与XML文件名相同,而无需对其进行硬编码。我尝试过多种方式,但是到目前为止还没有运气。

示例XML:

<?xml version="1.0" encoding="UTF-8"?>
<record:root>
<record:Dataload_Request>
    <record:name>Bob Chuck</record:name>
    <record:Address_Data>
        <record:Street_Address>123 Main St</record:Street_Address>
        <record:Postal_Code>12345</record:Postal_Code>
    </record:Address_Data>
    <record:Age>45</record:Age>
</record:Dataload_Request>
</record:root>

这是我尝试过的:

require 'nokogiri'
require 'set'

files = ''
input_folder = "H:/input"
output_folder = "H:/output"

if input_folder[input_folder.length-1,1] == '/'
   input_folder = input_folder[0,input_folder.length-1]
end

if output_folder[output_folder.length-1,1] != '/'
   output_folder = output_folder + '/'
end


files   = Dir[input_folder + '/*.xml'].sort_by{ |f| File.mtime(f)}
file    = File.read(input_folder + '/' + files)
doc     = Nokogiri::XML(file)
record  = {} # hashes
keys    = Set.new
records = [] # array
csv     = ""

doc.traverse do |node| 
  value = node.text.gsub(/\n +/, '')
    if node.name != "text" # skip these nodes: if class isnt text then skip
      if value.length > 0 # skip empty nodes
        key = node.name.gsub(/wd:/,'').to_sym
        if key == :Dataload_Request && !record.empty?
          records << record
          record = {}
        elsif key[/^root$|^document$/]
          # neglect these keys
        else
          key = node.name.gsub(/wd:/,'').to_sym
          # in case our value is html instead of text
          record[key] = Nokogiri::HTML.parse(value).text
          # add to our key set only if not already in the set
          keys << key
        end
      end
    end
  end

# build our csv
File.open('H:/output/.*csv', 'w') do |file|
  file.puts %Q{"#{keys.to_a.join('","')}"}
  records.each do |record|
    keys.each do |key|
      file.write %Q{"#{record[key]}",}
    end
    file.write "\n"
  end
  print ''
  print 'output files ready!'
  print ''
end

我一直遇到'read memory': no implicit conversion of Array into String (TypeError)和其他错误。

2 个答案:

答案 0 :(得分:2)

这里是您的代码的快速同行评审,就像您在公司环境中一样...

代替写作:

input_folder = "H:/input"

input_folder[input_folder.length-1,1] == '/' # => false

考虑使用距字符串结尾的-1偏移量来访问字符:

input_folder[-1] # => "t"

这简化了您的逻辑,使其更具可读性,因为它没有不必要的视觉干扰:

input_folder[-1] == '/' # => false

请参见String文档中的[][]=


这对我来说似乎是个错误:

files   = Dir[input_folder + '/*.xml'].sort_by{ |f| File.mtime(f)}
file    = File.read(input_folder + '/' + files)

files是文件名数组。 input_folder + '/' + files正在将数组附加到字符串:

foo = ['1', '2'] # => ["1", "2"]
'/parent/' + foo # => 
# ~> -:9:in `+': no implicit conversion of Array into String (TypeError)
# ~>  from -:9:in `<main>'

您要如何处理它是程序员的一项练习。


doc.traverse do |node|

令人讨厌,因为它回避了Nokogiri能够使用访问器搜索特定标签的功能。我们很少需要逐个标签地遍历一个文档,通常仅在我们查看其结构和布局时才需要。 traverse较慢,因此只能作为最后的手段。


length很不错,但在检查字符串是否包含内容时不需要:

value = 'foo'
value.length > 0 # => true
value > '' # => true

value = ''
value.length > 0 # => false
value > '' # => false

来自Java的程序员喜欢使用访问器,但我喜欢懒惰,这可能是因为我的C和Perl背景。


请注意subgsub,因为它们没有按照您认为的去做。两者都希望有一个正则表达式,但是在开始扫描之前,它们都将使用一个经过escape处理的字符串。

您要传递正则表达式,在这种情况下可以这样做,但是如果您不记得所有模式匹配规则并且gsub会扫描到最后,则可能会导致意外问题。字符串:

foo = 'wd:barwd:' # => "wd:barwd:"
key = foo.gsub(/wd:/,'') # => "bar"

通常,我建议人们在使用正则表达式之前要三思而后行。我已经看到了相当高级的程序员在逻辑上打开了一些空白,因为他们不知道引擎将要做什么。它们功能强大,但需要通过外科手术使用,而不是作为通用解决方案。

字符串也会发生相同的情况,因为gsub不知道何时退出:

key = foo.gsub('wd:','') # => "bar"

因此,如果您只想更改第一个实例,请使用sub

key = foo.sub('wd:','') # => "barwd:"

我会做一些不同的事情。

foo = 'wd:bar'

我可以检查一下前三个字符是什么

foo[0,3] # => "wd:"

或者我可以使用字符串索引将它们替换为其他内容:

foo[0,3] = '' 
foo # => "bar"

还有更多,但我认为现在已经足够了。

答案 1 :(得分:1)

您应该使用Ruby的CSV类。另外,您不需要执行任何字符串匹配或正则表达式的操作。使用Nokogiri定位元素。如果您知道XML中的节点名称是一致的,则应该非常简单。我不确定这是否是您想要的输出,但这应该可以使您朝正确的方向前进:

require 'nokogiri'
require 'csv'

def xml_to_csv(filename)
  xml_str = File.read(filename)
  xml_str.gsub!('record:','') # remove the record: namespace
  doc = Nokogiri::XML xml_str
  csv_filename = filename.gsub('.xml', '.csv')

  CSV.open(csv_filename, 'wb' ) do |row|
    row << ['name', 'street_address', 'postal_code', 'age']
    row << [
      doc.xpath('//name').text,
      doc.xpath('//Street_Address').text,
      doc.xpath('//Postal_Code').text,
      doc.xpath('//Age').text,
    ]
  end
end

# iterate over all xml files
Dir.glob('*.xml').each { |filename| xml_to_csv(filename) }