Navigazione verso i nodi usando xpath in struttura piatta
Domanda
Ho un file xml in una struttura piatta. Non controlliamo il formato di questo file xml, dobbiamo solo occuparcene. Ho rinominato i campi perché sono altamente specifici del dominio e non fanno davvero alcuna differenza al 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>
Aspetti chiave da notare: il file non è particolarmente gerarchico. I libri sono delimitati da un'occorrenza di un elemento di attributo con nome = 'Titolo'. Ma il nodo dell'attributo name = 'Author' è facoltativo.
Esiste una semplice istruzione xpath che posso usare per trovare gli autori del libro 'n'? È facile identificare il titolo del libro 'n', ma il valore degli autori è facoltativo. E non puoi semplicemente prendere il seguente autore perché nel caso del libro 2, questo darebbe all'autore per il libro 3.
Ho scritto una macchina a stati per analizzarlo come una serie di elementi, ma non posso fare a meno di pensare che ci sarebbe stato un modo per ottenere direttamente i risultati desiderati.
Soluzione
Vogliamo l'attributo " " elemento di @name 'Autore' che sta seguendo un " attributo " elemento di @name 'Title' con un valore di 'Book n', senza altri " attributo " elemento di @name 'Title' tra loro (perché se ci sono, allora l'autore ha creato un altro libro).
Detto diversamente, significa che vogliamo un autore di cui il primo titolo precedente (quello a cui " appartiene a ") è quello che stiamo cercando:
//attribute[@name='Author']
[preceding-sibling::attribute[@name='Title'][1][contains(.,'Book N')]]
N = C = > trova <attribute name="Author"><value>James Berry</value></attribute>
N = B = > non trova nulla
L'uso dei tasti e / o delle funzioni di raggruppamento disponibili in XSLT 2.0 renderebbe questo più semplice (e molto più veloce se il file è grande).
(il parser del codice SO sembra pensare che "//" sta per "commenti" ma in XPath non lo è !!! Sospiro.)
Altri suggerimenti
Bene, ho usato Elementtree per estrarre i dati dall'XML sopra. Ho salvato questo XML nel file denominato 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
Quando esegui questa funzione otterrai questo:
{'Book A': ['James Berry', 'John Smith'], 'Book C': ['James Berry']}
Spero che questo sia quello che stai cercando. In caso contrario, basta specificare un po 'di più. :)
Come bambax ha notato nella sua risposta, una soluzione che utilizza chiavi XSLT è più efficiente , specialmente per documenti XML di grandi dimensioni:
<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>
Quando la trasformazione sopra è applicata su questo 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>
viene prodotto l'output corretto:
Book C Author:
<attribute name="Author">
<value>James Berry</value>
</attribute>
====================
Book B Author:
Si noti che l'uso dell'abbreviazione "//"
XPath dovrebbe essere evitato il più possibile , poiché di solito provoca la scansione dell'intero documento XML su ogni valutazione dell'espressione XPath.
Seleziona tutti i titoli e applica il modello
<xsl:template match="/">
<xsl:apply-templates select="//attribute[@name='Title']"/>
</xsl:template>
Nel titolo di output del modello, controlla se esiste il titolo successivo. In caso contrario, output dopo l'autore. Se esiste, controlla se il seguente nodo autore del seguente libro è uguale al seguente nodo autore del libro corrente. Se lo è, significa che il libro attuale non ha autore:
<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>
Non sono sicuro che tu voglia davvero andarci: il più semplice che ho trovato è stato di andare dall'autore, ottenere il titolo precedente, quindi controllare che il primo autore o il titolo seguente fosse effettivamente un titolo. Brutto!
/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]
(ho aggiunto il tag libri per avvolgere il file).
L'ho provato con libxml2 BTW, usando xml_grep2 , ma solo sui dati di esempio che hai fornito, quindi ulteriori test sono benvenuti).