【问题标题】:How to read multiple XML files then output to multiple CSV files with the same XML filenames如何读取多个 XML 文件,然后输出到具有相同 XML 文件名的多个 CSV 文件
【发布时间】:2019-06-11 19:31:07
【问题描述】:

我正在尝试解析多个 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) 和其他错误。

【问题讨论】:

  • 您可以发布一个 yaml 文件的小样本作为示例吗?
  • 对不起,我不知道如何制作。关于如何编码的任何快速指导?还是您想要 xml 数据的样本?
  • 哦,是的,我的意思是 xml
  • 请看上面的xml示例
  • 没有必要,甚至不希望警告我们您没有编写代码的经验。相反,请完成“How to Ask”及其链接页面以及“mcve”中描述的作业。花点时间完成这些步骤,然后向我们提供所需的信息,我们会尽力为您提供帮助。

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


【解决方案1】:

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

而不是写:

input_folder = "H:/input"

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

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

input_folder[-1] # => "t"

这简化了您的逻辑,使其更具可读性,因为它缺少不必要的视觉噪音:

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

请参阅字符串文档中的 [][]=


这在我看来是个错误:

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"

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

【讨论】:

    【解决方案2】:

    您应该使用 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) }
    

    【讨论】:

    • 谢谢先生!逻辑有效!我只是将您的一些代码应用到我的代码中,使其适用于不同类型的 XML。
    • 小心删除这样的命名空间。使用它们是有原因的,以分离和区分名称相似的标签。删除它们可能会导致标签冲突或破坏数据,这可能会真正弄乱所需的结果。相反,学习使用命名空间。它们很痛苦,但在复杂的 XML 中它们是必要的痛苦。此外,请小心使用gsub 更改扩展名。文件名中的匹配将导致问题。而是将搜索限制为最后三个字符。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多