【问题标题】:How to build a node from a hash using Nokogiri如何使用 Nokogiri 从哈希构建节点
【发布时间】:2015-01-11 01:19:25
【问题描述】:

很多人通常想做相反的转换,但我想从 Ruby 哈希(包含许多嵌套哈希甚至数组)构建一个节点:

my_hash = {
  "name" => "Something",
  "property_1" => "Something"
  "nested_array_items" => [{ "name" => "Nasty nested array item", 
                  "advice" => "Use recursive function" },
                 { "name" => "yes this is an array",
                  "notice" => "not necessarily the same keys"}],
  "nested_many_levels" => { "additional_items" => { "ok_stop_here" => true } },
}

我有一个 Nokogiri 节点,它应该包含所有这些。我如何定义函数来做到这一点?

每个子节点都应该以密钥的名称命名,最终将“_”替换为“-”。对于数组,使用每个项目的键名称的单数,假设它是常规复数(以“s”结尾,否则会引发错误)。

例如上面给出的哈希应该变成:

...
<name>something></name>
<property_1>Something</property_1>
<nested_array_items>
  <nested_array_item>
    <name>Nasty nested array item</name>
    <advice>Use recursive function</advice>
  </nested_array_item>
  <nested_array_item>
    <name>yes this is an array</name>
    <notice>not necessarily the same keys</notice>
  </nested_array_item>
</nested_array_items>
<nested_many_levels>
  <additional_items>
      <ok_stop_here>true</ok_stop_here>
  </additional_items>
</nested_many_levels>
...

【问题讨论】:

  • 或者 Nokogiri 已经提供了一个帮手来做这件事?
  • 我试图回答这个问题,但我投了反对票,因为您的要求不明确。您提供了一些示例输入,但没有与之配套的示例输出。然后,您根据不完整的输入提供示例输出。 nested_many_levels 会发生什么?如果您编辑问题以解决这些问题,我将很乐意改变我的投票。
  • 你说得对,这不是很清楚。我已经重写了输出样本,使其与输入哈希样本匹配。希望现在好多了

标签: ruby xml recursion hash nokogiri


【解决方案1】:

好的,所以我意识到从散列构建节点并不是我的最佳选择(在我的情况下,我希望拥有完整的 XML 结构,即使某些节点由于缺少散列内容而为空)。

因此,我使用的 XML 模板节点已经包含我想要的完整结构,只有长度为 1 的数组。因此,与其构建新节点,我将根据需要多次复制现有节点(预处理),然后我替换内容。

因为这只对数组来说很痛苦,所以假设我的 contents 变量在第一次调用时只有一个包含数组的散列(但这些数组的项可以是值、散列......)

复制custom_xml的模板节点

contents.map do |content, items|
    tmp = custom_xml.search("#{content.to_s}")      # Should be unique !
    if tmp.count > 1 then raise "ERROR : multiple nodes match XPATH //#{content.to_s}" end
    if tmp.count == 0 then raise "ERROR : No node matches \"search #{content.to_s}\" DEBUG : #{custom_xml.serialize}" end
    array_node = tmp.first # <array><item>...</item></array>
    template_node = array_node.first_element_child  
    # Okay, we have the customXML node corresponding to the first item of the content
    # We need to duplicate it as many times as needed
    items.each_with_index do |item, item_index|
        next if item_index == 0 # Skip the first one.
        array_node << template_node.dup
    end
end

然后在完成此预处理后,可以通过调用 replace_node_vars_recursively(array_node, items) 来实际替换 array_node(s) 的变量

请注意,对于第一次调用,我们确实有一个 array_nodeitems,但递归函数也需要处理散列和值。所以让我们用content这个词来指代这个东西,而node

使用“内容”递归更改节点文本

def  replace_node_vars_recursively(node, content)
    if content.nil?
        puts "WARNING : nil content trying to be assigned to node #{node.name}"
    elsif content.is_a?(Hash)
        # Every key in content SHOULD have a matching child node !
        content.each do |key, val|
            candidates = node.search("#{key.to_s}")     # Should be unique !
            if candidates.count > 1 
                puts "WARNING : multiple child_nodes match -->#{key.to_s}<--, skipping"
                next
            elsif candidates.count == 0 
                puts "WARNING : No child node matches \"#{key.to_s}\" "
                next
            end
            replace_node_vars_recursively(candidates.first, val)
        end
    # Array recursion (rq : array contains either a Hash or a value.)
    elsif content.is_a?(Array)
        # Let's rename the variables !
        array_items = content 
        array_node = node
        if array_items.count != array_node.element_children.count # /!\ using just "children" will return empty nodes !!!
            raise "ERROR : array length (#{array_items.count}) != number of nodes of #{array_node.name} (#{array_node.element_children.count}) !"
        end
        array_node.element_children.each_with_index do |child_node, index|  # Assume item is another content_hash. Wouldn't make sense (for me) to have just a value there...
            replace_node_vars_recursively(child_node, content[index])
        end
    # Value terminaison
    elsif content.is_a?(String) or content.is_a?(Integer) or content.is_a?(Float) or content.is_a?(Symbol) or content.is_a?(Date) or content.is_a?(Datetime)
        node.content = content.to_s
        puts "Replacing variable #{node.name} by #{content.to_s}"
    else 
        puts content
        raise "ERROR: unknown variable type for variable replacement !"
    end
end

【讨论】:

    【解决方案2】:

    好的,这是我的代码,似乎可以工作!

    def  build_node_recursively(node, content_hash, xml_document)
        content_hash.each do |key, val|
            new_node = Nokogiri::XML::Node.new "#{key.to_s}", xml_document
            # Val can be : a value | an array of hashes | another hash
            if val.is_a?(Array)
                item_label = key.to_s.singularize
                val.each do |item|  # Assume item is another content_hash. Wouldn't make sense (for me) to have just a value...
                    item_node = Nokogiri::XML::Node.new item_label, xml_document
                    new_node << build_node_recursively(item_node, item)
                end
            elsif val.is_a?(Hash)
                build_node_recursively(new_node, val)
            else
                new_node.content = val.to_s
            end
            node << new_node
        end
    end
    

    【讨论】:

    • 我对你的答案投了反对票,因为代码实际上并没有运行。除了需要 require 'active_support/core_ext/string/inflections' 之外,您无法将 xml_document 作为第三个参数递归地传递给您的方法,但即使进行了更改,代码也无法使用您自己的示例输入运行。
    • 啊,你在很多方面都是对的。首先,我从 Rails 应用程序运行此代码,因此已经加载了变形器 w。看来我还没有发布我的代码的最新版本......不幸的是我没有保留,因为我意识到我不需要构建一个节点,而只是替换已经存在的节点的内容......
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-10-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-15
    • 2017-05-27
    • 1970-01-01
    相关资源
    最近更新 更多