0

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

2 回答 2

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

这里的小问题是,如果您只有一个孩子,那么实际上无法区分该孩子应该是一个数组还是一个简单的值。因此,在阅读时,当您想将值视为数组时,请使用此处的答案之一。例如,如果你不反对猴子补丁,

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"]]
于 2017-05-09T07:55:27.550 回答
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}}]}
于 2017-05-10T22:08:33.280 回答