Analysieren von XML mit Namespace in Python über 'ElementTree'

163

Ich habe das folgende XML, das ich mit Pythons analysieren möchte 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>

Ich möchte alle owl:ClassTags finden und dann den Wert aller darin enthaltenen rdfs:labelInstanzen extrahieren . Ich verwende den folgenden Code:

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

Aufgrund des Namespace wird der folgende Fehler angezeigt.

SyntaxError: prefix 'owl' not found in prefix map

Ich habe versucht, das Dokument unter http://effbot.org/zone/element-namespaces.htm zu lesen, aber ich kann dies immer noch nicht zum Laufen bringen, da das obige XML mehrere verschachtelte Namespaces hat.

Bitte lassen Sie mich wissen, wie Sie den Code ändern können, um alle owl:ClassTags zu finden .

Sudar
quelle

Antworten:

226

ElementTree ist in Bezug auf Namespaces nicht besonders schlau. Sie müssen das geben .find(), findall()und iterfind()Methoden einen expliziten Namespace - Wörterbuch. Dies ist nicht sehr gut dokumentiert:

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

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

Präfixe werden nur in dem namespacesParameter nachgeschlagen, den Sie übergeben. Dies bedeutet, dass Sie ein beliebiges Namespace-Präfix verwenden können. Die API teilt das owl:Teil auf, sucht die entsprechende Namespace-URL im namespacesWörterbuch und ändert dann die Suche, um {http://www.w3.org/2002/07/owl}Classstattdessen nach dem XPath-Ausdruck zu suchen . Sie können die gleiche Syntax natürlich auch selbst verwenden:

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

Wenn Sie zur lxmlBibliothek wechseln können, sind die Dinge besser. Diese Bibliothek unterstützt dieselbe ElementTree-API, sammelt jedoch Namespaces für Sie in einem .nsmapAttribut für Elemente.

Martijn Pieters
quelle
7
Danke dir. Irgendeine Idee, wie ich den Namespace direkt aus XML abrufen kann, ohne ihn fest zu codieren? Oder wie kann ich das ignorieren? Ich habe findall ('{*} Class') ausprobiert, aber in meinem Fall funktioniert es nicht.
Kostanos
7
Sie müssten den Baum selbst nach xmlnsAttributen durchsuchen. Wie in der Antwort angegeben, lxmlerledigt dies für Sie, das xml.etree.ElementTreeModul nicht. Wenn Sie jedoch versuchen, ein bestimmtes (bereits fest codiertes) Element abzugleichen, versuchen Sie auch, ein bestimmtes Element in einem bestimmten Namespace abzugleichen. Dieser Namespace ändert sich zwischen Dokumenten nicht mehr als der Elementname. Sie können dies auch mit dem Elementnamen fest codieren.
Martijn Pieters
14
@ Jon: register_namespaceBeeinflusst nur die Serialisierung, nicht die Suche.
Martijn Pieters
5
Kleine Ergänzung, die nützlich sein kann: Wenn Sie cElementTreeanstelle von verwenden ElementTree, findallwerden Namespaces nicht als Schlüsselwortargument verwendet, sondern einfach als normales Argument, dh als Verwendung ctree.findall('owl:Class', namespaces).
egpbos
2
@Bludwarf: Die Dokumente erwähnen es zwar (jetzt, wenn nicht, als Sie das geschrieben haben), aber Sie müssen sie sehr sorgfältig lesen. Weitere Informationen finden Sie im Abschnitt XML mit Namespaces analysieren : Es gibt ein Beispiel, das die Verwendung von findallwithout und dann mit dem namespaceArgument kontrastiert. Das Argument wird jedoch nicht als eines der Argumente für die Methodenmethode im Abschnitt Element object erwähnt .
Wilson F
57

So geht's mit lxml, ohne die Namespaces fest codieren oder den Text nach ihnen durchsuchen zu müssen (wie Martijn Pieters erwähnt):

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

UPDATE :

5 Jahre später stoße ich immer noch auf Variationen dieses Problems. lxml hilft wie oben gezeigt, aber nicht in jedem Fall. Die Kommentatoren mögen einen gültigen Punkt in Bezug auf diese Technik haben, wenn es darum geht, Dokumente zusammenzuführen, aber ich denke, die meisten Leute haben Schwierigkeiten, einfach Dokumente zu suchen.

Hier ist ein weiterer Fall und wie ich damit umgegangen bin:

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

xmlns ohne Präfix bedeutet, dass nicht vorfixierte Tags diesen Standard-Namespace erhalten. Dies bedeutet, dass Sie bei der Suche nach Tag2 den Namespace angeben müssen, um ihn zu finden. Lxml erstellt jedoch einen nsmap-Eintrag mit None als Schlüssel, und ich konnte keine Möglichkeit finden, danach zu suchen. Also habe ich ein neues Namespace-Wörterbuch wie dieses erstellt

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)
Brad Dre
quelle
3
Die vollständige Namespace-URL ist die Namespace-ID, die Sie fest codieren sollen. Das lokale Präfix ( owl) kann sich von Datei zu Datei ändern. Daher ist es eine wirklich schlechte Idee, das zu tun, was diese Antwort nahelegt.
Matti Virkkunen
1
@MattiVirkkunen genau wenn die Eulendefinition von Datei zu Datei geändert werden kann, sollten wir nicht die in jeder Datei definierte Definition verwenden, anstatt sie fest zu codieren?
Loïc Faure-Lacroix
@ LoïcFaure-Lacroix: In XML-Bibliotheken können Sie diesen Teil normalerweise abstrahieren. Sie müssen nicht einmal das in der Datei selbst verwendete Präfix kennen oder sich darum kümmern, sondern definieren lediglich Ihr eigenes Präfix zum Parsen oder verwenden einfach den vollständigen Namespace-Namen.
Matti Virkkunen
Diese Antwort hat mir geholfen, zumindest die Suchfunktion verwenden zu können. Sie müssen kein eigenes Präfix erstellen. Ich habe gerade key = list (root.nsmap.keys ()) [0] gemacht und dann den Schlüssel als Präfix hinzugefügt: root.find (f '{key}: Tag2', root.nsmap)
Eelco van Vliet
30

Hinweis : Dies ist eine nützliche Antwort für die ElementTree-Standardbibliothek von Python, ohne fest codierte Namespaces zu verwenden.

Um die Präfixe und URI des Namespace aus XML-Daten zu extrahieren, können Sie die ElementTree.iterparseFunktion verwenden und nur Namespace -Startereignisse ( Start-ns ) analysieren :

>>> 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#'}

Dann kann das Wörterbuch als Argument an die Suchfunktionen übergeben werden:

root.findall('owl:Class', my_namespaces)
Davide Brunato
quelle
1
Dies ist nützlich für diejenigen von uns, die keinen Zugriff auf lxml haben und keinen Namespace fest codieren möchten.
Delrocco
1
Ich habe den Fehler bekommen: ValueError: write to closedfür diese Zeile filemy_namespaces = dict([node for _, node in ET.iterparse(StringIO(my_schema), events=['start-ns'])]). Irgendeine Idee will falsch?
Yuli
Wahrscheinlich hängt der Fehler mit der Klasse io.StringIO zusammen, die ASCII-Zeichenfolgen ablehnt. Ich hatte mein Rezept mit Python3 getestet. Das Hinzufügen des Unicode-Zeichenfolgenpräfixes 'u' zur Beispielzeichenfolge funktioniert auch mit Python 2 (2.7).
Davide Brunato
Stattdessen dict([...])können Sie auch das Diktatverständnis verwenden.
Arminius
Stattdessen StringIO(my_schema)können Sie auch den Dateinamen der XML-Datei eingeben.
JustAC0der
6

Ich habe einen ähnlichen Code verwendet und festgestellt, dass es sich immer lohnt, die Dokumentation zu lesen ... wie immer!

findall () findet nur Elemente, die direkte untergeordnete Elemente des aktuellen Tags sind . Also nicht wirklich ALLE.

Es kann sich lohnen, zu versuchen, Ihren Code mit den folgenden Funktionen zum Laufen zu bringen, insbesondere wenn Sie mit großen und komplexen XML-Dateien arbeiten, sodass auch Unter-Unterelemente (usw.) enthalten sind. Wenn Sie selbst wissen, wo sich Elemente in Ihrer XML befinden, ist das wahrscheinlich in Ordnung! Ich dachte nur, das wäre es wert, in Erinnerung zu bleiben.

root.iter()

ref: https://docs.python.org/3/library/xml.etree.elementtree.html#finding-interesting-elements "Element.findall () findet nur Elemente mit einem Tag, die direkte untergeordnete Elemente des aktuellen Elements sind. Element.find () findet das erste untergeordnete Element mit einem bestimmten Tag und Element.text greift auf den Textinhalt des Elements zu. Element.get () greift auf die Attribute des Elements zu: "

MJM
quelle
6

Um den Namespace in seinem Namespace-Format zu erhalten, {myNameSpace}können Sie beispielsweise Folgendes tun:

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

Auf diese Weise können Sie es später in Ihrem Code verwenden, um Knoten zu finden, z. B. mithilfe der Zeichenfolgeninterpolation (Python 3).

link = root.find(f"{ns}link")
Bram Vanroy
quelle
0

Meine Lösung basiert auf dem Kommentar von @Martijn Pieters:

register_namespace Beeinflusst nur die Serialisierung, nicht die Suche.

Der Trick hier besteht also darin, verschiedene Wörterbücher für die Serialisierung und für die Suche zu verwenden.

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

Registrieren Sie jetzt alle Namespaces zum Parsen und Schreiben:

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

Für die Suche ( find(), findall(), iterfind()) brauchen wir einen nicht leeren Präfix. Übergeben Sie diesen Funktionen ein geändertes Wörterbuch (hier ändere ich das ursprüngliche Wörterbuch, dies muss jedoch erst erfolgen, nachdem die Namespaces registriert wurden).

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

Jetzt können die Funktionen aus der find()Familie mit dem defaultPräfix verwendet werden:

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

aber

tree.write(destination)

verwendet keine Präfixe für Elemente im Standard-Namespace.

peter.slizik
quelle