Analisi XML con spazio dei nomi in Python tramite 'ElementTree'


163

Ho il seguente XML che voglio analizzare usando Python ElementTree:

<rdf:RDF xml:base="http://dbpedia.org/ontology/"
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:owl="http://www.w3.org/2002/07/owl#"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema#"
    xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
    xmlns="http://dbpedia.org/ontology/">

    <owl:Class rdf:about="http://dbpedia.org/ontology/BasketballLeague">
        <rdfs:label xml:lang="en">basketball league</rdfs:label>
        <rdfs:comment xml:lang="en">
          a group of sports teams that compete against each other
          in Basketball
        </rdfs:comment>
    </owl:Class>

</rdf:RDF>

Voglio trovare tutti i owl:Classtag e quindi estrarre il valore di tutte le rdfs:labelistanze al loro interno. Sto usando il seguente codice:

tree = ET.parse("filename")
root = tree.getroot()
root.findall('owl:Class')

A causa dello spazio dei nomi, visualizzo il seguente errore.

SyntaxError: prefix 'owl' not found in prefix map

Ho provato a leggere il documento su http://effbot.org/zone/element-namespaces.htm ma non riesco ancora a farlo funzionare poiché l'XML sopra ha più spazi dei nomi nidificati.

Fammi sapere come modificare il codice per trovare tutti i owl:Classtag.

Risposte:


226

ElementTree non è troppo intelligente riguardo agli spazi dei nomi. Devi fornire a .find(), findall()e ai iterfind()metodi un dizionario esplicito dello spazio dei nomi. Questo non è documentato molto bene:

namespaces = {'owl': 'http://www.w3.org/2002/07/owl#'} # add more as needed

root.findall('owl:Class', namespaces)

I prefissi vengono cercati solo nel namespacesparametro passato. Ciò significa che è possibile utilizzare qualsiasi prefisso dello spazio dei nomi desiderato; l'API divide la owl:parte, cerca l'URL dello spazio dei nomi corrispondente nel namespacesdizionario, quindi modifica la ricerca per cercare {http://www.w3.org/2002/07/owl}Classinvece l' espressione XPath . Ovviamente puoi usare la stessa sintassi anche tu:

root.findall('{http://www.w3.org/2002/07/owl#}Class')

Se riesci a passare alla lxmllibreria, le cose vanno meglio; quella libreria supporta la stessa API ElementTree, ma raccoglie spazi dei nomi per te in un .nsmapattributo sugli elementi.


7
Grazie. Qualche idea su come posso ottenere lo spazio dei nomi direttamente da XML, senza codificarlo? O come posso ignorarlo? Ho provato findall ('{*} Class') ma nel mio caso non funzionerà.
Kostanos,

7
Dovresti scansionare l'albero per gli xmlnsattributi da solo; come indicato nella risposta, lxmlfa questo per te, il xml.etree.ElementTreemodulo no. Ma se stai cercando di abbinare un elemento specifico (già codificato), allora stai anche cercando di abbinare un elemento specifico in uno spazio dei nomi specifico. Lo spazio dei nomi non cambierà più tra i documenti di quanto non lo sia il nome dell'elemento. Puoi anche hardcode che con il nome dell'elemento.
Martijn Pieters

14
@Jon: register_namespaceinfluenza solo la serializzazione, non la ricerca.
Martijn Pieters

5
Piccola aggiunta che può essere utile: quando si usa cElementTreeinvece di ElementTree, findallnon prenderà gli spazi dei nomi come argomento di parole chiave, ma semplicemente come un argomento normale, cioè uso ctree.findall('owl:Class', namespaces).
egpbos,

2
@Bludwarf: I documenti lo menzionano (ora, se non quando l'hai scritto), ma devi leggerli attentamente. Vedi la sezione XML di analisi con spazi dei nomi : c'è un esempio che contrappone l'uso di findallsenza e quindi con l' namespaceargomento, ma l'argomento non è menzionato come uno degli argomenti del metodo metodo nella sezione Oggetto elemento .
Wilson F,

57

Ecco come farlo con lxml senza dover codificare gli spazi dei nomi o scansionare il testo per loro (come menziona Martijn Pieters):

from lxml import etree
tree = etree.parse("filename")
root = tree.getroot()
root.findall('owl:Class', root.nsmap)

AGGIORNAMENTO :

5 anni dopo mi imbatto ancora in variazioni di questo problema. lxml aiuta come ho mostrato sopra, ma non in tutti i casi. I commentatori possono avere un punto valido riguardo a questa tecnica quando si tratta di unire documenti, ma penso che la maggior parte delle persone abbia difficoltà a cercare semplicemente documenti.

Ecco un altro caso e come l'ho gestito:

<?xml version="1.0" ?><Tag1 xmlns="http://www.mynamespace.com/prefix">
<Tag2>content</Tag2></Tag1>

xmlns senza prefisso significa che i tag non prefissati ottengono questo spazio dei nomi predefinito. Ciò significa che quando si cerca Tag2, è necessario includere lo spazio dei nomi per trovarlo. Tuttavia, lxml crea una voce nsmap con Nessuno come chiave e non sono riuscito a trovare un modo per cercarla. Quindi, ho creato un nuovo dizionario dello spazio dei nomi come questo

namespaces = {}
# response uses a default namespace, and tags don't mention it
# create a new ns map using an identifier of our choice
for k,v in root.nsmap.iteritems():
    if not k:
        namespaces['myprefix'] = v
e = root.find('myprefix:Tag2', namespaces)

3
L'URL completo dello spazio dei nomi è l'identificatore dello spazio dei nomi che dovresti codificare. Il prefisso locale ( owl) può cambiare da file a file. Pertanto fare ciò che suggerisce questa risposta è una pessima idea.
Matti Virkkunen,

1
@MattiVirkkunen esattamente se la definizione di gufo può cambiare da file a file, non dovremmo usare la definizione definita in ciascun file invece di codificarla?
Loïc Faure-Lacroix,

@ LoïcFaure-Lacroix: di solito le librerie XML ti permettono di astrarre quella parte. Non è nemmeno necessario conoscere o preoccuparsi del prefisso utilizzato nel file stesso, è sufficiente definire il proprio prefisso allo scopo di analizzare o semplicemente utilizzare il nome completo dello spazio dei nomi.
Matti Virkkunen,

questa risposta mi ha aiutato almeno ad essere in grado di utilizzare la funzione find. Non è necessario creare il proprio prefisso. Ho appena fatto key = list (root.nsmap.keys ()) [0] e poi ho aggiunto la chiave come prefisso: root.find (f '{key}: Tag2', root.nsmap)
Eelco van Vliet

30

Nota : questa è una risposta utile per la libreria standard ElementTree di Python senza usare spazi dei nomi codificati.

Per estrarre i prefissi e l'URI dello spazio dei nomi dai dati XML è possibile utilizzare la ElementTree.iterparsefunzione, analizzando solo gli eventi di inizio dello spazio dei nomi ( start-ns ):

>>> from io import StringIO
>>> from xml.etree import ElementTree
>>> my_schema = u'''<rdf:RDF xml:base="http://dbpedia.org/ontology/"
...     xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
...     xmlns:owl="http://www.w3.org/2002/07/owl#"
...     xmlns:xsd="http://www.w3.org/2001/XMLSchema#"
...     xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
...     xmlns="http://dbpedia.org/ontology/">
... 
...     <owl:Class rdf:about="http://dbpedia.org/ontology/BasketballLeague">
...         <rdfs:label xml:lang="en">basketball league</rdfs:label>
...         <rdfs:comment xml:lang="en">
...           a group of sports teams that compete against each other
...           in Basketball
...         </rdfs:comment>
...     </owl:Class>
... 
... </rdf:RDF>'''
>>> my_namespaces = dict([
...     node for _, node in ElementTree.iterparse(
...         StringIO(my_schema), events=['start-ns']
...     )
... ])
>>> from pprint import pprint
>>> pprint(my_namespaces)
{'': 'http://dbpedia.org/ontology/',
 'owl': 'http://www.w3.org/2002/07/owl#',
 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#',
 'xsd': 'http://www.w3.org/2001/XMLSchema#'}

Quindi il dizionario può essere passato come argomento alle funzioni di ricerca:

root.findall('owl:Class', my_namespaces)

1
Ciò è utile per quelli di noi che non hanno accesso a lxml e non vogliono codificare lo spazio dei nomi.
delrocco,

1
Ho ricevuto l'errore: ValueError: write to closedper questa riga filemy_namespaces = dict([node for _, node in ET.iterparse(StringIO(my_schema), events=['start-ns'])]). Qualche idea vuole sbagliare?
Yuli,

Probabilmente l'errore è correlato alla classe io.StringIO, che rifiuta le stringhe ASCII. Avevo testato la mia ricetta con Python3. Aggiungendo il prefisso della stringa unicode 'u' alla stringa di esempio funziona anche con Python 2 (2.7).
Davide Brunato,

Invece di dict([...])te puoi anche usare la comprensione del dict.
Arminius,

Invece di StringIO(my_schema)te puoi anche mettere il nome del file XML.
JustAC0der

6

Ho usato un codice simile a questo e ho scoperto che vale sempre la pena leggere la documentazione ... come al solito!

findall () troverà solo elementi che sono figli diretti del tag corrente . Quindi, non proprio TUTTI.

Potrebbe valere la pena tentare di far funzionare il codice con quanto segue, soprattutto se si ha a che fare con file XML grandi e complessi in modo da includere anche i sotto-elementi secondari (ecc.). Se conosci te stesso dove sono gli elementi nel tuo XML, allora suppongo che andrà bene! Ho pensato che valesse la pena ricordare.

root.iter()

ref: https://docs.python.org/3/library/xml.etree.elementtree.html#finding-interesting-elements "Element.findall () trova solo elementi con un tag che sono figli diretti dell'elemento corrente. Element.find () trova il primo figlio con un tag particolare ed Element.text accede al contenuto del testo dell'elemento. Element.get () accede agli attributi dell'elemento: "


6

Per ottenere lo spazio dei nomi nel suo formato dello spazio dei nomi, ad esempio {myNameSpace}, è possibile effettuare le seguenti operazioni:

root = tree.getroot()
ns = re.match(r'{.*}', root.tag).group(0)

In questo modo, è possibile utilizzarlo in seguito nel codice per trovare nodi, ad esempio utilizzando l'interpolazione di stringhe (Python 3).

link = root.find(f"{ns}link")

0

La mia soluzione si basa sul commento di @Martijn Pieters:

register_namespace influenza solo la serializzazione, non la ricerca.

Quindi il trucco qui è usare dizionari diversi per la serializzazione e per la ricerca.

namespaces = {
    '': 'http://www.example.com/default-schema',
    'spec': 'http://www.example.com/specialized-schema',
}

Ora, registra tutti gli spazi dei nomi per l'analisi e la scrittura:

for name, value in namespaces.iteritems():
    ET.register_namespace(name, value)

Per la ricerca ( find(), findall(), iterfind()) abbiamo bisogno di un prefisso non vuoto. Passa a queste funzioni un dizionario modificato (qui modifico il dizionario originale, ma questo deve essere fatto solo dopo aver registrato gli spazi dei nomi).

self.namespaces['default'] = self.namespaces['']

Ora, le funzioni della find()famiglia possono essere utilizzate con il defaultprefisso:

print root.find('default:myelem', namespaces)

ma

tree.write(destination)

non utilizza alcun prefisso per gli elementi nello spazio dei nomi predefinito.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.