Pregunta

Tengo un archivo xml en una estructura plana. No controlamos el formato de este archivo xml, solo tenemos que lidiar con él. Cambié el nombre de los campos porque son altamente específicos de dominio y realmente no hacen ninguna diferencia en el problema.

<attribute name="Title">Book A</attribute>
<attribute name="Code">1</attribute>
<attribute name="Author">
   <value>James Berry</value>
   <value>John Smith</value>
</attribute>
<attribute name="Title">Book B</attribute>
<attribute name="Code">2</attribute>
<attribute name="Title">Book C</attribute>
<attribute name="Code">3</attribute>
<attribute name="Author">
    <value>James Berry</value>
</attribute>

Aspectos clave a tener en cuenta: el archivo no es particularmente jerárquico. Los libros están delimitados por la aparición de un elemento de atributo con nombre = 'Título'. Pero el nodo de atributo name = 'Author' es opcional.

¿Hay una simple declaración xpath que pueda usar para encontrar a los autores del libro 'n'? Es fácil identificar el título del libro 'n', pero el valor de los autores es opcional. Y no puede simplemente tomar el siguiente autor porque en el caso del libro 2, esto le daría al autor el libro 3.

He escrito una máquina de estados para analizar esto como una serie de elementos, pero no puedo evitar pensar que habría habido una manera de obtener directamente los resultados que quiero.

¿Fue útil?

Solución

Queremos el " atributo " elemento de @nombre 'Autor' que sigue a " atributo " elemento de @nombre 'Título' con un valor de 'Libro n', sin ningún otro " atributo " elemento de @name 'Título' entre ellos (porque si los hay, entonces el autor es autor de otro libro).

Dicho de otra manera, significa que queremos un autor cuyo first título anterior (el que " pertenece a ") es el que estamos buscando:

//attribute[@name='Author']
[preceding-sibling::attribute[@name='Title'][1][contains(.,'Book N')]]

N = C = > encuentra <attribute name="Author"><value>James Berry</value></attribute>

N = B = > no encuentra nada

El uso de teclas y / o funciones de agrupación disponibles en XSLT 2.0 lo haría más fácil (y mucho más rápido si el archivo es grande).

(¡El analizador de código SO parece pensar que '//' significa 'comentarios' pero en XPath no lo es! Suspiro).

Otros consejos

Bueno, he usado Elementtree para extraer datos del XML anterior. He guardado este XML en un archivo llamado foo.xml

from xml.etree.ElementTree import fromstring

def extract_data():
    """Returns list of dict of book and
    its authors."""

    f = open('foo.xml', 'r+')
    xml = f.read()
    elem = fromstring(xml)
    attribute_list = elem.findall('attribute')
    dic = {}
    lst = []

    for attribute in attribute_list:
        if attribute.attrib['name'] == 'Title':
            key = attribute.text
        if attribute.attrib['name'] == 'Author':
            for v in attribute.findall('value'):
                lst.append(v.text)
            value = lst
            lst = []
            dic[key] = value
    return dic

Cuando ejecute esta función obtendrá esto:

{'Book A': ['James Berry', 'John Smith'], 'Book C': ['James Berry']}

Espero que esto sea lo que estás buscando. Si no, solo especifique un poco más. :)

Como bambax señaló en su respuesta, una solución que utiliza claves XSLT es más eficiente , especialmente para documentos XML grandes:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes"/>
 <!--                                            -->
 <xsl:key name="kAuthByTitle" 
  match="attribute[@name='Author']"
  use="preceding-sibling::attribute[@name='Title'][1]"/>
 <!--                                            -->
    <xsl:template match="/">
      Book C Author:
      <xsl:copy-of select=
         "key('kAuthByTitle', 'Book C')"/>
  <!--                                            -->
         ====================
      Book B Author:
      <xsl:copy-of select=
         "key('kAuthByTitle', 'Book B')"/>
    </xsl:template>
</xsl:stylesheet>

Cuando la transformación anterior se aplica en este documento XML:

<t>
    <attribute name="Title">Book A</attribute>
    <attribute name="Code">1</attribute>
    <attribute name="Author">
        <value>James Berry</value>
        <value>John Smith</value>
    </attribute>
    <attribute name="Title">Book B</attribute>
    <attribute name="Code">2</attribute>
    <attribute name="Title">Book C</attribute>
    <attribute name="Code">3</attribute>
    <attribute name="Author">
        <value>James Berry</value>
    </attribute>
</t>

se produce la salida correcta:

  Book C Author:
  <attribute name="Author">
    <value>James Berry</value>
</attribute>

     ====================
  Book B Author:

Tenga en cuenta que el uso de la "//" abreviatura XPath debe evitarse tanto como sea posible , ya que generalmente hace que se escanee todo el documento XML en cada evaluación de la expresión XPath.

Seleccione todos los títulos y aplique la plantilla

<xsl:template match="/">
  <xsl:apply-templates select="//attribute[@name='Title']"/>
</xsl:template>

En el título de salida de la plantilla, verifique si existe el siguiente título. Si no, salida siguiente autor. Si existe, verifique si el siguiente nodo de autor del siguiente libro es el mismo que el siguiente nodo de autor del libro actual. Si es así, significa que el libro actual no tiene autor:

<xsl:template match="*">
   <book>
     <title><xsl:value-of select="."/></title> 
   <author>
   <xsl:if test="not(following::attribute[@name='Title']) or following::attribute[@name='Author'] != following::attribute[@name='Title']/following::attribute[@name='Author']">
   <xsl:value-of select="following::attribute[@name='Author']"/>
   </xsl:if>
   </author>
   </book>
</xsl:template>

No estoy seguro de que realmente quiera ir allí: lo más simple que encontré fue ir del autor, obtener el título anterior y luego verificar que el primer autor o título siguiente fuera realmente un título. ¡Feo!

/books/attribute[@name="Author"]
  [preceding-sibling::attribute[@name="Title" and string()="Book B"]
                               [following-sibling::attribute[ @name="Author" 
                                                             or @name="Title"
                                                            ]
                                 [1]
                                 [@name="Author"]
                               ]
  ][1]

(agregué la etiqueta books para envolver el archivo).

Probé eso con libxml2 BTW, usando xml_grep2 , pero solo en los datos de muestra que proporcionó, por lo que más pruebas son bienvenidas).

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top