ruby Conversion de XML en Hash: Nori supprime les attributs des éléments XML les plus profonds



(1)

Résumé

J'utilise Ruby ( ruby 2.1.2p95 (2014-05-08) [x86_64-linux-gnu] sur ma machine, ruby 1.9.3p484 (2013-11-22 revision 43786) [x86_64-linux] dans un environnement de production) et Nori pour convertir un document XML (initialement traité avec Nokogiri pour une validation) en un Ruby Hash, mais j'ai découvert plus tard que Nori abandonnait les attributs des éléments XML les plus profonds.

Détails du problème et reproduction

Pour ce faire, j'utilise un code similaire à celui-ci:

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

Le code fonctionne généralement comme prévu, sauf pour un cas. Chaque fois que Nori analyse le texte XML, il supprime les attributs des éléments feuilles (c.-à-d. Les éléments sans éléments enfants).

Par exemple, le document suivant:

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

... est converti au hachage attendu (certaines sorties sont omises pour des raisons de brièveté):

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

Le problème apparaît lorsque les attributs d'élément sont utilisés sur des éléments sans enfants. Par exemple, le document suivant n'est pas converti comme prévu:

<?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>

Le même Nori.new.parse(xml.to_s) , affiché par awesome_print , montre que les attributs des éléments <field> les plus profonds sont absents :

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

The Hash n'a que ses valeurs en tant que liste, ce qui n'est pas ce que je voulais. Je m'attendais à ce que les éléments <field> conservent leurs attributs comme leurs éléments parents (par exemple, voir @id="1" pour <object> ), pas pour que leurs attributs soient coupés.

Même si le document est modifié pour ressembler à ceci, il ne fonctionne toujours pas comme prévu:

<?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>

Il produit le hachage suivant:

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

Qui manque le type="whatever" attributs pour chaque entrée de champ.

La recherche m'a finalement amené au numéro 59 avec le dernier message (à partir d'août 2015) indiquant qu'il ne pouvait pas "trouver le bogue dans le code de Nori".

Conclusion

Donc, ma question est la suivante: est-ce que certains d'entre vous savent comment contourner le problème de Nori (par exemple, un paramètre) qui me permettrait d'utiliser mon schéma d'origine (celui avec les attributs des éléments sans enfants)? Si oui, pouvez-vous partager un extrait de code qui gérera cela correctement?

J'ai dû repenser mon schéma XML et changer de code environ trois fois pour que cela fonctionne, alors si un moyen de faire en sorte que Nori se comporte, et que je ne le sois tout simplement pas au courant, j'aimerais savoir ce qu'il en est. est.

Je voudrais éviter d' installer plus de bibliothèques autant que possible pour que cela fonctionne correctement avec la structure de schéma que je voulais utiliser à l'origine, mais je suis ouvert à la possibilité si cela fonctionne. (Je devrais refaire le code une fois de plus ...) Les cadres sont vraiment exorbitants pour cela, alors s'il-vous-plaît: ne suggérez pas Ruby on Rails ou des solutions empilées similaires.

Veuillez noter que ma solution actuelle, basée sur un schéma redessiné (à contrecœur), fonctionne, mais elle est plus compliquée à générer et à traiter que la version originale, et je voudrais revenir au schéma plus simple.


Nori ne laisse pas tomber les attributs, ils ne sont tout simplement pas imprimés.

Si vous exécutez le script 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}"

Vous devriez obtenir la sortie

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

Ce qui montre clairement que l'attribut est là, mais ne sont pas affichés par la méthode puts x.inspect (la fonction p(x) étant la même que puts x.inspect ).

Vous remarquerez que puts field_list.inspect sorties puts field_list.inspect ["The name", "A description"] . mais field_list[0].attributes imprime la clé d'attribut et les données.

Si vous souhaitez que pp affiche ceci, vous pouvez surcharger la méthode Nori::StringWithAttributes dans Nori::StringWithAttributes .

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

Ou, si vous souhaitez modifier la sortie, vous pouvez surcharger la méthode self.new pour qu'elle renvoie une structure de données différente.

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

Et accéder aux données sous forme de tuple

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

Cela ferait en sorte que vous pourriez avoir les données comme JSON ou YAML car les éléments de texte ressembleraient à des tableaux avec 2 éléments. pp fonctionne également.

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

Cela devrait faire ce que vous voulez.

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

comme le résultat est:

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




hash