【问题标题】:How to add duplicate keys from XML to a hash如何将 XML 中的重复键添加到哈希中
【发布时间】:2023-04-05 03:44:01
【问题描述】:

我有一个项目,它需要大量 XML 数据并将其传递给 Nokogiri,最终将每个元素添加到输出到 YAML 文件的哈希中。

这在 XML 数据集包含重复键之前有效。

示例数据:

<document>
    <form xmlns="">
        <title>
            <main-title>Foo</main-title>
        </title>
        <homes>
            <home>
                <home-name>home 1</home-name>
                <home-price>10</home-price>
            </home>
            <home>
                <home-name>home 2</home-name>
                <home-price>20</home-price>
            </home>
        </homes>
    </form>
</document>

homes 元素中我可以有多个家,但是每个home 总是包含不同的内容。

这些数据最终应该输出这样的结构:

title:
  main-title: Foo
homes:
  home:
    home-name: home 1
    home-price: 10
  home:
    home-name: home 2
    home-price: 20

然而我得到的只是homes中的最后一个元素

title:
      main-title: Foo
    homes:
      home:
        home-name: home 2
        home-price: 20

我相信这是因为,在将每个元素添加到散列时,如果它已经存在,它将简单地覆盖键,因此总是给我最后一个键。

这是用于将元素附加到哈希的代码:

def map_content(nodes, content_hash)
        nodes.map do |element|
          case element
          when Nokogiri::XML::Element
            child_content = map_content(element.children, {})
            content_hash[element.name] = child_content unless child_content.empty?
          when Nokogiri::XML::Text
            return element.content
          end
        end
        content_hash
      end

我相信

content_hash[element.name] = child_content

是罪魁祸首,但是此代码创建了具有这些类型的重复键的类似 YAML 文件,并且我想保留该功能,因此我不想简单地向数据哈希添加唯一键,因为这意味着我需要修改许多方法并更新它们从 YAML 文件中提取数据的方式。

我阅读了有关 compare_by_identity 的信息,但不确定我将如何实现它。


我尝试使用compare_by_identity,但它只会生成一个空的 YAML 文件,所以它可能正在生成哈希但无法将其写入 YAML 文件?

def map_content(nodes, content_hash)
        content_hash = content_hash.compare_by_identity

        nodes.map do |element|
          case element
          when Nokogiri::XML::Element
            child_content = map_content(element.children, {})
            content_hash[element.name] = child_content unless child_content.empty?
          when Nokogiri::XML::Text
            return element.content
          end
        end
        content_hash
      end
    end

【问题讨论】:

  • 拥有一个“homes”键并将其下方的所有内容放在一个数组中怎么样?您建议的结构将不起作用,因为在将其加载回您的应用程序时,它会给您相同的结果(只有最后一个 :home 键)。
  • 嗯我试图不修改当前的结构,但这可能是唯一的方法。
  • 您想要的 YAML 输出是不可能的。 YAML 在解析时会产生带有重复键的哈希,您已经发现这是不可能的。您必须使用哈希数组。

标签: ruby xml nokogiri ruby-hash


【解决方案1】:

compare_by_identity 原则上很简单:

hash = {}.compare_by_identity
hash[String.new("home")] = { "home-name" => "home 1", "home-price" => "10" }
hash[String.new("home")] = { "home-name" => "home 2", "home-price" => "20" }
hash
# => {"home"=>{"home-name"=>"home 1", "home-price"=>"10"}, "home"=>{"home-name"=>"home 2", "home-price"=>"20"}} 

(我使用String.new 将源代码中的文字字符串强制为不同的对象。你不需要这个,因为Nokogiri 会动态构造字符串对象,它们会有不同的object_id。)

即从字面上看,您需要做的就是在您制作的每个Hash 上调用.compare_by_identity。然而,这并非没有代价:访问停止工作。

hash["home"]
# => nil

您现在需要明确检查每个元素的相等性:

hash.to_a.select { |k, v| k == "home" }.map { |k, v| v }
# => [{"home-name"=>"home 1", "home-price"=>"10"}, {"home-name"=>"home 2", "home-price"=>"20"}]

正如 Severin 所说,如果您将其放入 YAML 或 JSON 中,也会产生可怕的影响,因为您将无法再次正确加载它。

您可以采用的另一种方法,也是一种更受欢迎的方法,是将 XML 特性留给 XML,并将结构转换为更 JSON-y 的结构(因此可以直接由 HashArray 对象表示)。例如,

class MultiValueHash < Hash
  def add(key, value)
    if !has_key?(key)
      self[key] = value
    elsif Array === self[key]
      self[key] << value
    else
      self[key] = [self[key], value]
    end
  end
end

hash = MultiValueHash.new
hash.add("home", { "home-name" => "home 1", "home-price" => "10" })
hash.add("home", { "home-name" => "home 2", "home-price" => "20" })
hash.add("work", { "work-name" => "work 1", "work-price" => "30" })
hash["home"]
# => [{"home-name"=>"home 1", "home-price"=>"10"}, {"home-name"=>"home 2", "home-price"=>"20"}]
hash["work"]
# => {"work-name"=>"work 1", "work-price"=>"30"}

这里的小问题是,如果你有一个孩子,实际上无法区分该孩子应该是一个数组还是一个简单的值。所以在阅读的时候,当你想把值当作一个数组来处理时,使用here的答案之一。例如,如果你不反对猴子补丁,

hash["home"].ensure_array
# => [{"home-name"=>"home 1", "home-price"=>"10"}, {"home-name"=>"home 2", "home-price"=>"20"}] 
hash["work"].ensure_array
# => [["work-name", "work 1"], ["work-price", "30"]]

【讨论】:

【解决方案2】:

我会这样做:

require 'nokogiri'

doc = Nokogiri::XML(<<EOT)
<document>
  <form xmlns="">
    <title>
      <main-title>Foo</main-title>
    </title>
    <homes>
      <home>
        <home-name>home 1</home-name>
        <home-price>10</home-price>
      </home>
      <home>
        <home-name>home 2</home-name>
        <home-price>20</home-price>
      </home>
    </homes>
  </form>
</document>
EOT

title = doc.at('main-title').text
homes = doc.search('home').map { |home|
  {
    'home' => {
      'home-name' => home.at('home-name').text,
      'home-price' => home.at('home-price').text.to_i
    }
  }
}

hash = {
  'title' => {'main-title' => title},
  'homes' => homes
}

当转换为 YAML 时,结果是:

require 'yaml'
puts hash.to_yaml

# >> ---
# >> title:
# >>   main-title: Foo
# >> homes:
# >> - home:
# >>     home-name: home 1
# >>     home-price: 10
# >> - home:
# >>     home-name: home 2
# >>     home-price: 20

你不能创造:

homes:
  home:
    home-name: home 1
    home-price: 10
  home:
    home-name: home 2
    home-price: 20

因为home: 元素是homes 哈希中的键。不可能有多个同名的键;第二个将覆盖第一个。相反,它们必须是指定为- home 的哈希数组,如上面的输出所示。

考虑这些:

require 'yaml'

foo = YAML.load(<<EOT)
title:
  main-title: Foo
homes:
  home:
    home-name: home 1
    home-price: 10
  home:
    home-name: home 2
    home-price: 20
EOT

foo
# => {"title"=>{"main-title"=>"Foo"},
#     "homes"=>{"home"=>{"home-name"=>"home 2", "home-price"=>20}}}

对比:

foo = YAML.load(<<EOT)
title:
  main-title: Foo
homes:
- home:
    home-name: home 1
    home-price: 10
- home:
    home-name: home 2
    home-price: 20
EOT

foo
# => {"title"=>{"main-title"=>"Foo"},
#     "homes"=>
#      [{"home"=>{"home-name"=>"home 1", "home-price"=>10}},
#       {"home"=>{"home-name"=>"home 2", "home-price"=>20}}]}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-05-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多