【问题标题】:XML to Hash conversion: Nori drops the attributes of the deepest XML elementsXML 到 Hash 的转换:Nori 丢弃了最深的 XML 元素的属性
【发布时间】:2016-06-14 14:06:18
【问题描述】:

总结

我正在使用 Ruby(在我的机器上使用ruby 2.1.2p95 (2014-05-08) [x86_64-linux-gnu],在生产环境中使用ruby 1.9.3p484 (2013-11-22 revision 43786) [x86_64-linux])和 Nori 将 XML 文档(最初使用 Nokogiri 处理以进行一些验证)转换为 Ruby 哈希,但后来我发现 Nori正在删除最深的 XML 元素的属性。

问题详情及复现

为此,我使用类似于以下的代码:

xml  = Nokogiri::XML(File.open('file.xml')) { |config| config.strict.noblanks }
hash = Nori.new.parse xml.to_s

代码通常按预期工作,除了一种情况。每当 Nori 解析 XML 文本时,它都会从叶元素(即没有子元素的元素)中删除元素属性。

例如以下文档:

<?xml version="1.0"?>
<root>
  <objects>
    <object>
      <fields>
        <id>1</id>
        <name>The name</name>
        <description>A description</description>
      </fields>
    </object>
  </objects>
</root>

...被转换为预期的哈希(为简洁起见省略了一些输出):

irb(main):066:0> xml = Nokogiri::XML(txt) { |config| config.strict.noblanks }
irb(main):071:0> ap Nori.new.parse(xml.to_s), :indent => -2
{
  "root" => {
    "objects" => {
      "object" => {
        "fields" => {
          "id"   => "1",
          "name" => "The name"
          "description" => "A description"
        }
      }
    }
  }
}

当元素属性用于没有子元素的元素时,问题就会出现。例如,以下文档没有按预期转换:

<?xml version="1.0"?>
<root>
  <objects>
    <object id="1">
      <fields>
        <field name="Name">The name</field>
        <field name="Description">A description</field>
      </fields>
    </object>
  </objects>
</root>

同样的Nori.new.parse(xml.to_s),和awesome_print显示的一样,表示最深的&lt;field&gt;元素的属性不存在

irb(main):131:0> ap Nori.new.parse(xml.to_s), :indent => -2
{
  "root" => {
    "objects" => {
      "object" => {
        "fields" => {
          "field" => [
            [0] "The name",
            [1] "A description"
          ]
        },
        "@id"    => "1"
      }
    }
  }
}

哈希仅将它们的值作为一个列表,这不是我想要的。我希望 &lt;field&gt; 元素像它们的父元素一样保留它们的属性(例如,对于 &lt;object&gt;,请参阅 @id="1"),而不是因为它们的属性被截断。

即使将文档修改为如下所示,它仍然无法按预期工作:

<?xml version="1.0"?>
<root>
  <objects>
    <object id="1">
      <fields>
        <Name type="string">The name</Name>
        <Description type="string">A description</Description>
      </fields>
    </object>
  </objects>
</root>

它产生以下哈希:

{
  "root" => {
    "objects" => {
      "object" => {
        "fields" => {
          "Name"        => "The name",
          "Description" => "A description"
        },
        "@id"    => "1"
      }
    }
  }
}

缺少每个字段条目的type="whatever" 属性。

搜索最终将我带到Issue #59,最后一篇帖子(从 2015 年 8 月开始)说他无法“找到 Nori 代码中的错误”。

结论

所以,我的问题是:你们中是否有人知道一种解决 Nori 问题的方法(例如,可能是一种设置),它允许我使用我的原始模式(即与没有子元素的元素中的属性)?如果是这样,您能否分享一个可以正确处理此问题的代码 sn-p?

我不得不重新设计我的 XML 架构并更改代码大约 3 次才能使其正常工作,所以如果有办法让 Nori 正常工作,而我根本不知道,我想知道它是什么。

我想避免尽可能多地安装更多库,只是为了让它与我最初想要使用的架构结构正常工作,但如果它被证明,我愿意接受去工作。 (我不得不再次重构代码...)为此,框架肯定是矫枉过正,所以请:建议Ruby on Rails 或类似的全栈解决方案。

请注意,我当前的解决方案基于(不情愿地)重新设计的架构,正在运行,但它的生成和处理比原来的更复杂,我想回到更简单/更浅的架构。

【问题讨论】:

  • 我建议创建自己的递归方法“xml to json”。你可以用 nokogiri 做到这一点。
  • @andoke:如果您能详细说明这一点,我将不胜感激,也许可以提供包含概念验证代码的答案。如果我要在工作中花更多时间在这方面,并重构 XML 文档,我需要知道它确实可以工作,而不是死胡同。
  • Nori 中有一个针对这个错误的 GitHub 问题:issue #59 “It ignores attributes when a child is a text node”
  • @RoryO'Kane:谢谢,但在发布问题之前我已经遇到过这个问题,我什至在原帖中也提到过。对于这篇文章,我想看看是否有人知道该问题的解决方法。

标签: ruby xml hash


【解决方案1】:

Nori 实际上并没有删除属性,它们只是没有被打印出来。

如果你运行 ruby​​ 脚本:

require 'nori'

data = Nori.new(empty_tag_value: true).parse(<<XML)
<?xml version="1.0"?>
<root>
  <objects>
    <object>
      <fields>
        <field name="Name">The name</field>
        <field name="Description">A description</field>
      </fields>
    </object>
  </objects>
</root>
XML

field_list = data['root']['objects']['object']['fields']['field']

puts "text: '#{field_list[0]}' data: #{field_list[0].attributes}"
puts "text: '#{field_list[1]}' data: #{field_list[1].attributes}"

你应该得到输出

["The name", "A description"]
text: 'The name' data: {"name"=>"Name"}
text: 'A description' data: {"name"=>"Description"}

这清楚地表明属性存在,但inspect方法不显示(p(x)函数与puts x.inspect相同)。

您会注意到puts field_list.inspect 输出["The name", "A description"]。但field_list[0].attributes 打印属性键和数据。

如果您想让pp 显示此内容,您可以重载Nori::StringWithAttributes 中的inspect 方法。

class Nori
  class StringWithAttributes < String
    def inspect
      [attributes, String.new(self)].inspect
    end
  end
end

或者,如果您想更改输出,您可以重载 self.new 方法,让它返回不同的数据结构。

class Nori
  class MyText < Array
    def attributes=(data)
      self[1] = data
    end
    attr_accessor :text
    def initialize(text)
      self[0] = text
      self[1] = {}
    end
  end
  class StringWithAttributes < String
    def self.new(x)
      MyText.new(x)
    end
  end
end

并以元组的形式访问数据

puts "text: '#{data['root']['objects']['object']['fields']['field'][0].first}' data: #{ data['root']['objects']['object']['fields']['field'][0].last}"

这将使您可以将数据保存为 JSON 或 YAML,因为文本项看起来像具有 2 个元素的数组。 pp 也可以。

{"root"=>
  {"objects"=>
    {"object"=>
      {"fields"=>
        {"field"=>
          [["The name", {"name"=>"Name"}],
           ["A description", {"name"=>"Description"}]]},
       "bob"=>[{"@id"=>"id1"}, {"@id"=>"id2"}],
       "bill"=>
        [{"p"=>["one", {}], "@id"=>"bid1"}, {"p"=>["two", {}], "@id"=>"bid2"}],
       "@id"=>"1"}}}}

这应该做你想做的。

require 'awesome_print'
require 'nori'

# Copyright (c) 2016 G. Allen Morris III
#
# Awesome Print is freely distributable under the terms of MIT license.
# See LICENSE file or http://www.opensource.org/licenses/mit-license.php
#------------------------------------------------------------------------------
module AwesomePrint
  module Nori

    def self.included(base)
      base.send :alias_method, :cast_without_nori, :cast
      base.send :alias_method, :cast, :cast_with_nori
    end

    # Add Nori XML Node and NodeSet names to the dispatcher pipeline.
    #-------------------------------------------------------------------
    def cast_with_nori(object, type)
      cast = cast_without_nori(object, type)
      if defined?(::Nori::StringWithAttributes) && object.is_a?(::Nori::StringWithAttributes)
        cast = :nori_xml_node
      end
      cast
    end

    #-------------------------------------------------------------------
    def awesome_nori_xml_node(object)
      return %Q|["#{object}", #{object.attributes}]|
    end
  end
end

AwesomePrint::Formatter.send(:include, AwesomePrint::Nori)

data = Nori.new(empty_tag_value: true).parse(<<XML)
<?xml version="1.0"?>
<root>
  <objects>
    <object>
      <fields>
        <field name="Name">The name</field>
        <field name="Description">A description</field>
      </fields>
    </object>
  </objects>
</root>
XML

ap data

因为输出是:

{
    "root" => {
        "objects" => {
            "object" => {
                "fields" => {
                    "field" => [
                        [0] ["The name", {"name"=>"Name"}],
                        [1] ["A description", {"name"=>"Description"}]
                    ]
                }
            }
        }
    }
}

【讨论】:

  • 顶部的 2 行示例,您建议叶属性没有被打印,实际上与 TypeError: no implicit conversion of String into Integer 一起崩溃。此外,如果它们只是没有被打印,为什么需要重载其他类/方法,您似乎暗示这是不必要的?
  • 您更新的帖子有效,尽管ap 仍然无法打印属性。既然属性在那里,有没有办法让它们自动打印(例如ap data, :indent =&gt; -2)? (注意我使用的是awesome_print gem。)此外,我在我的 OP 中链接的错误报告表明这里存在实际问题。是否已解决此问题并且问题报告未更新?
  • 给了你胜利。事情发生了变化,我不再需要继续解决这个问题,但鉴于您似乎已经测试了代码(例如提供的输出)并且似乎是一个足够接近的解决方法,我给您点数。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-08-18
  • 2017-06-17
  • 1970-01-01
  • 1970-01-01
  • 2010-10-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多