如何使用 Nokogiri 将多个类似格式的 XML 文件组合成 CSV

How to use Nokogiri to combine multiple like-formatted XML files into CSV

我想将多个类似格式的 XML 文件解析为一个 CSV 文件。

我在 Google、nokogiri.org 和 SO 上进行了搜索,但未能找到答案。

我有十个 XML 文件,它们在 node/element 结构方面具有相同的格式,位于当前目录中。

将 XML 文件组合成一个 XML 文件后,我需要提取 advisory 节点的特定元素。我想将 linktitlelocationos -> language -> namereference -> name 数据输出到 CSV 文件。

我的代码只能解析单个 XML 文档,我希望它能考虑 1:many:

# Parse the XML file into a Nokogiri::XML::Document object
@doc = Nokogiri::XML(File.open("file.xml"))

# Gather the 5 specific XML elements out of the 'advisory' top-level node
data = @doc.search('advisory').map { |adv|
  [
    adv.at('link').content,
    adv.at('title').content,
    adv.at('location').content,
    adv.at('os > language > name').content,
    adv.at('reference > name').content
  ]
}

# Loop through each array element in the object and write out as CSV row
CSV.open('output_file.csv', 'wb') do |csv|
  # Explicitly set headers until you figure out how to get them programatically
  csv << ['Link', 'Title', 'Location', 'OS Name', 'Reference Name']
  data.each do |row|
    csv << row
  end
end

我尝试更改代码以支持多个 XML 文件并将它们放入 Nokogiri::XML::Document 个对象中:

xml_docs = []

Dir.glob("*.xml").each do |file|
  xml = Nokogiri::XML(File.new(file))
  xml_docs << Nokogiri::XML::Document.new(xml)
end

这成功创建了一个包含正确对象的数组 xml_docs,但我不知道如何将这六个对象转换为一个对象。

这是示例 XML。所有 XML 文件使用相同的 node/element 结构:

<advisories>
  <title> Not relevant </title>
  <customer> N/A </customer>
  <advisory id="12345">
    <link> https://www.google.com </link>
    <release_date>2016-04-07</release_date>
    <title> The Short Description Would Go Here </title>
    <location> Location Name Here </location>
    <os>
      <product>
        <id>98765</id>
        <name>Product Name</name>
      </product>
      <language>
        <id>123</id>
        <name>en</name>
      </language>
    </os>
    <reference>
      <id>00029</id>
      <name>Full</name>
      <area>Not Defined</area>
    </reference>
  </advisory>
  <advisory id="98765">
    <link> https://www.msn.com </link>
    <release_date>2016-04-08</release_date>
    <title> The Short Description Would Go Here </title>
    <location> Location Name Here </location>
    <os>
      <product>
        <id>12654</id>
        <name>Product Name</name>
      </product>
      <language>
        <id>126</id>
        <name>fr</name>
      </language>
    </os>
    <reference>
      <id>00052</id>
      <name>Partial</name>
      <area>Defined</area>
    </reference>
  </advisory>
</advisories>

该代码利用了 Nokogiri::XML::Document,但如果 Nokogiri::XML::Builder 对此效果更好,我非常愿意相应地调整我的代码。

我将处理解析一个 XML 文件的第一部分,如下所示:

require 'nokogiri'

doc = Nokogiri::XML(<<EOT)
<advisories>
  <advisory id="12345">
    <link> https://www.google.com </link>
    <title> The Short Description Would Go Here </title>
    <location> Location Name Here </location>
    <os>
      <language>
        <name>en</name>
      </language>
    </os>
    <reference>
      <name>Full</name>
    </reference>
  </advisory>
  <advisory id="98765">
    <link> https://www.msn.com </link>
    <release_date>2016-04-08</release_date>
    <title> The Short Description Would Go Here </title>
    <location> Location Name Here </location>
    <os>
      <language>
        <name>fr</name>
      </language>
    </os>
    <reference>
      <name>Partial</name>
    </reference>
  </advisory>
</advisories>
EOT

注意:这里删除了节点,因为它们对问题不重要。请在询问时去除绒毛,因为它会分散注意力。

这是代码的核心:

doc.search('advisory').map{ |advisory|
  link = advisory.at('link').text
  title = advisory.at('title').text
  location = advisory.at('location').text
  os_language_name = advisory.at('os > language > name').text
  reference_name = advisory.at('reference > name').text

  {
    link: link,
    title: title,
    location: location,
    os_language_name: os_language_name,
    reference_name: reference_name
  }
}

这可以是 DRY'd,但它是作为操作示例编写的。

运行 生成哈希数组,可以通过 CSV 轻松输出:

# => [
      {:link=>" https://www.google.com ", :title=>" The Short Description Would Go Here ", :location=>" Location Name Here ", :os_language_name=>"en", :reference_name=>"Full"}, 
      {:link=>" https://www.msn.com ", :title=>" The Short Description Would Go Here ", :location=>" Location Name Here ", :os_language_name=>"fr", :reference_name=>"Partial"}
     ]

一旦你开始工作,然后将它放入循环的修改版本中以输出 CSV 并读取 XML 文件。这是未经测试的,但看起来是正确的:

CSV.open('output_file.csv', 'w', 
  headers: ['Link', 'Title', 'Location', 'OS Name', 'Reference Name'],
  write_headers: true
) do |csv|
  Dir.glob("*.xml").each do |file|
    xml = Nokogiri::XML(File.read(file))
    # parse a file and get the array of hashes
  end

  # pass the array of hashes to CSV for output
end

请注意,您使用的文件模式为 'wb'。你很少需要 b 和 CSV,因为 CSV 应该是一种文本格式。如果你确定你会遇到二进制数据,那么也使用'b',但这可能会导致一条包含龙的路径。

另请注意,这是使用 readread 不可扩展,这意味着它不关心文件有多大,它会尝试将其读入内存,无论它是否适合。有很多理由可以避免这种情况,但最好的是它会让您的程序瘫痪。如果您的 XML 文件可能超过系统的可用空闲内存,那么您将需要使用 Nokogiri 支持的 SAX 解析器重写。如何做到这一点是另一个问题。


it was actually an Array of array of hashes. I'm not sure how I ended up there but I was easily able to use array.flatten

对此进行冥想:

foo = []    # => []
foo << [{}] # => [[{}]]
foo.flatten # => [{}]

您可能想这样做:

foo = []    # => []
foo += [{}] # => [{}]

每当我必须使用 flatten 时,我都会查看是否可以创建数组,而不是它是某物数组的数组。这并不是说它们天生就不好,因为有时它们非常有用,但你真的想要一个哈希数组,这样你就知道出了什么问题,flatten 是一个便宜的出路,但使用它也会花费更多 CPU次。最好找出问题并解决它并最终得到 faster/more 高效的代码。 (有些人会说这是浪费精力或过早优化,但编写高效代码是一个非常好的特征和目标。)