如何将重复键从XML添加到哈希

时间:2017-05-09 06:53:44

标签: ruby xml nokogiri ruby-hash

我有一个项目,它需要大量的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

2 个答案:

答案 0 :(得分: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。)

即。您需要做的就是在每个.compare_by_identity上拨打Hash。然而,这并非没有价格:访问停止工作。

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。例如,如果您不对monkeypatching不感兴趣,

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"]]

答案 1 :(得分:0)

我这样做:

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}}]}