Python ElementTree-Modul: So ignorieren Sie den Namespace von XML-Dateien, um das passende Element zu finden, wenn Sie die Methode "find", "findall" verwenden.

136

Ich möchte die Methode "findall" verwenden, um einige Elemente der XML-Quelldatei im ElementTree-Modul zu finden.

Die XML-Quelldatei (test.xml) hat jedoch einen Namespace. Ich schneide einen Teil der XML-Datei als Beispiel ab:

<?xml version="1.0" encoding="iso-8859-1"?>
<XML_HEADER xmlns="http://www.test.com">
    <TYPE>Updates</TYPE>
    <DATE>9/26/2012 10:30:34 AM</DATE>
    <COPYRIGHT_NOTICE>All Rights Reserved.</COPYRIGHT_NOTICE>
    <LICENSE>newlicense.htm</LICENSE>
    <DEAL_LEVEL>
        <PAID_OFF>N</PAID_OFF>
        </DEAL_LEVEL>
</XML_HEADER>

Der Beispiel-Python-Code ist unten:

from xml.etree import ElementTree as ET
tree = ET.parse(r"test.xml")
el1 = tree.findall("DEAL_LEVEL/PAID_OFF") # Return None
el2 = tree.findall("{http://www.test.com}DEAL_LEVEL/{http://www.test.com}PAID_OFF") # Return <Element '{http://www.test.com}DEAL_LEVEL/PAID_OFF' at 0xb78b90>

Obwohl dies funktionieren kann, ist es sehr unpraktisch, vor jedem Tag einen Namespace hinzuzufügen, da ein Namespace "{http://www.test.com}" vorhanden ist.

Wie kann ich den Namespace ignorieren, wenn ich die Methoden "find", "findall" usw. verwende?

KevinLeng
quelle
18
Ist tree.findall("xmlns:DEAL_LEVEL/xmlns:PAID_OFF", namespaces={'xmlns': 'http://www.test.com'})bequem genug?
iMom0
Vielen Dank. Ich probiere deine Methode aus und sie kann funktionieren. Es ist bequemer als meins, aber es ist immer noch etwas umständlich. Wissen Sie, ob es im ElementTree-Modul keine andere geeignete Methode gibt, um dieses Problem zu lösen, oder ob es überhaupt keine solche Methode gibt?
KevinLeng
Oder versuchen Sie estree.findall("{0}DEAL_LEVEL/{0}PAID_OFF".format('{http://www.test.com}'))
Warf
In Python 3.8 kann ein Platzhalter für den Namespace verwendet werden. stackoverflow.com/a/62117710/407651
mzjn

Antworten:

62

Anstatt das XML-Dokument selbst zu ändern, ist es am besten, es zu analysieren und dann die Tags im Ergebnis zu ändern. Auf diese Weise können Sie mehrere Namespaces und Namespace-Aliase verarbeiten:

from io import StringIO  # for Python 2 import from StringIO instead
import xml.etree.ElementTree as ET

# instead of ET.fromstring(xml)
it = ET.iterparse(StringIO(xml))
for _, el in it:
    prefix, has_namespace, postfix = el.tag.partition('}')
    if has_namespace:
        el.tag = postfix  # strip all namespaces
root = it.root

Dies basiert auf der Diskussion hier: http://bugs.python.org/issue18304

Update: rpartition Statt partitionsicherzustellen, dass Sie den Tag-Namen postfixauch dann erhalten, wenn kein Namespace vorhanden ist. So könnte man es verdichten:

for _, el in it:
    _, _, el.tag = el.tag.rpartition('}') # strip ns
nonagon
quelle
2
Dies. Das das das. Mehrere Namensräume würden der Tod von mir sein.
Jess
8
OK, das ist schön und fortgeschrittener, aber es ist immer noch nicht so et.findall('{*}sometag'). Außerdem wird der Elementbaum selbst entstellt, nicht nur "die Suche wird durchgeführt, wobei Namespaces nur dieses Mal ignoriert werden, ohne das Dokument usw. erneut zu analysieren und die Namespace-Informationen beizubehalten". In diesem Fall müssen Sie den Baum beobachten und selbst sehen, ob der Knoten Ihren Wünschen entspricht, nachdem Sie den Namespace entfernt haben.
Tomasz Gandor
1
Dies funktioniert durch Entfernen der Zeichenfolge, aber wenn ich die XML-Datei mit write (...) speichere, verschwindet der Namespace vom Beginn des XML xmlns = " bla " verschwindet. Bitte beraten Sie
TraceKira
@TomaszGandor: Sie könnten den Namespace vielleicht einem separaten Attribut hinzufügen. Für einfache Tag-Containment-Tests ( enthält dieses Dokument diesen Tag-Namen? ) Ist diese Lösung hervorragend und kann kurzgeschlossen werden.
Martijn Pieters
@TraceKira: Diese Technik entfernt Namespaces aus dem analysierten Dokument, und Sie können damit keine neue XML-Zeichenfolge mit Namespaces erstellen. Speichern Sie die Namespace-Werte entweder in einem zusätzlichen Attribut (und fügen Sie den Namespace wieder ein, bevor Sie den XML-Baum wieder in eine Zeichenfolge umwandeln), oder analysieren Sie die ursprüngliche Quelle erneut, um Änderungen an der basierend auf dem entfernten Baum vorzunehmen.
Martijn Pieters
48

Wenn Sie das xmlns-Attribut vor dem Parsen aus der xml entfernen, wird nicht jedem Tag im Baum ein Namespace vorangestellt.

import re

xmlstring = re.sub(' xmlns="[^"]+"', '', xmlstring, count=1)
user2212280
quelle
5
Dies funktionierte in vielen Fällen für mich, aber dann stieß ich auf mehrere Namespaces und Namespace-Aliase. Siehe meine Antwort für einen anderen Ansatz, der diese Fälle behandelt.
Nonagon
47
-1 Das Manipulieren der XML über einen regulären Ausdruck vor dem Parsen ist einfach falsch. Obwohl dies in einigen Fällen funktionieren kann, sollte dies nicht die am häufigsten gewählte Antwort sein und nicht in einer professionellen Anwendung verwendet werden.
Mike
1
Abgesehen von der Tatsache, dass die Verwendung eines regulären Ausdrucks für einen XML-Parsing-Job von Natur aus nicht sinnvoll ist , funktioniert dies für viele XML-Dokumente nicht , da Namespace-Präfixe ignoriert werden und die XML-Syntax beliebige Leerzeichen vor Attributnamen zulässt (nicht nur) Leerzeichen) und um das =Gleichheitszeichen.
Martijn Pieters
Ja, es ist schnell und schmutzig, aber es ist definitiv die eleganteste Lösung für einfache Anwendungsfälle, danke!
Rimkashox
18

Die bisherigen Antworten haben den Namespace-Wert explizit in das Skript eingefügt. Für eine allgemeinere Lösung würde ich lieber den Namespace aus der XML extrahieren:

import re
def get_namespace(element):
  m = re.match('\{.*\}', element.tag)
  return m.group(0) if m else ''

Und verwenden Sie es in der Suchmethode:

namespace = get_namespace(tree.getroot())
print tree.find('./{0}parent/{0}version'.format(namespace)).text
wimous
quelle
15
Zu viel, um anzunehmen, dass es nur einen gibtnamespace
Kashyap
Dies berücksichtigt nicht, dass verschachtelte Tags unterschiedliche Namespaces verwenden können.
Martijn Pieters
15

Hier ist eine Erweiterung der Antwort von nonagon, mit der auch Namespaces von Attributen entfernt werden:

from StringIO import StringIO
import xml.etree.ElementTree as ET

# instead of ET.fromstring(xml)
it = ET.iterparse(StringIO(xml))
for _, el in it:
    if '}' in el.tag:
        el.tag = el.tag.split('}', 1)[1]  # strip all namespaces
    for at in list(el.attrib.keys()): # strip namespaces of attributes too
        if '}' in at:
            newat = at.split('}', 1)[1]
            el.attrib[newat] = el.attrib[at]
            del el.attrib[at]
root = it.root

UPDATE: hinzugefügt, list()damit der Iterator funktioniert (wird für Python 3 benötigt)

Scheune
quelle
14

Verbesserung der Antwort von ericspod:

Anstatt den Analysemodus global zu ändern, können wir dies in ein Objekt einschließen, das das with-Konstrukt unterstützt.

from xml.parsers import expat

class DisableXmlNamespaces:
    def __enter__(self):
            self.oldcreate = expat.ParserCreate
            expat.ParserCreate = lambda encoding, sep: self.oldcreate(encoding, None)
    def __exit__(self, type, value, traceback):
            expat.ParserCreate = self.oldcreate

Dies kann dann wie folgt verwendet werden

import xml.etree.ElementTree as ET
with DisableXmlNamespaces():
     tree = ET.parse("test.xml")

Das Schöne an dieser Art ist, dass sie kein Verhalten für nicht verwandten Code außerhalb des with-Blocks ändert. Ich habe dies erstellt, nachdem ich Fehler in nicht verwandten Bibliotheken erhalten hatte, nachdem ich die Version von ericspod verwendet hatte, die auch expat verwendete.

lijat
quelle
Das ist süß und gesund! Rettete meinen Tag! +1
AndreasT
In Python 3.8 (noch nicht mit anderen Versionen getestet) scheint dies bei mir nicht zu funktionieren. Wenn man sich die Quelle ansieht, sollte es funktionieren, aber es scheint, dass der Quellcode für xml.etree.ElementTree.XMLParserirgendwie optimiert ist und das Patchen von Affen expatabsolut keine Wirkung hat.
Reinderien
Oh ja. Siehe @ barnys Kommentar: stackoverflow.com/questions/13412496/…
Reinderien
5

Sie können auch das elegante Konstrukt zur Zeichenfolgenformatierung verwenden:

ns='http://www.test.com'
el2 = tree.findall("{%s}DEAL_LEVEL/{%s}PAID_OFF" %(ns,ns))

oder, wenn Sie sicher sind, dass PAID_OFF nur in einer Ebene im Baum angezeigt wird :

el2 = tree.findall(".//{%s}PAID_OFF" % ns)
tzp
quelle
2

Wenn Sie verwenden ElementTreeund nicht cElementTree, können Sie Expat zwingen, die Namespace-Verarbeitung zu ignorieren, indem Sie Folgendes ersetzen ParserCreate():

from xml.parsers import expat
oldcreate = expat.ParserCreate
expat.ParserCreate = lambda encoding, sep: oldcreate(encoding, None)

ElementTreeversucht, Expat durch Aufrufen zu verwenden ParserCreate(), bietet jedoch keine Option, keine Namespace-Trennzeichenfolge anzugeben. Der obige Code führt dazu, dass es ignoriert wird. Es wird jedoch gewarnt, dass dies andere Probleme verursachen kann.

Ericspod
quelle
Dies ist ein besserer Weg als andere aktuelle Antworten, da es nicht von der Zeichenfolgenverarbeitung abhängt
lijat
3
In Python 3.7.2 (und möglicherweise eariler) AFAICT es ist nicht mehr möglich , mit celementtree zu vermeiden, so dass diese Abhilfe nicht möglich sein kann :-(
barny
1
cElemTree ist veraltet, aber es gibt Schatten für Typen, die mit C-Beschleunigern ausgeführt werden . Der C-Code ruft nicht bei Expat auf, also ist diese Lösung defekt.
Ericspod
@barny es ist immer noch möglich, ElementTree.fromstring(s, parser=None)ich versuche Parser an ihn zu übergeben.
est
2

Ich könnte zu spät kommen, aber ich denke nicht, dass dies re.subeine gute Lösung ist.

Das Umschreiben xml.parsers.expatfunktioniert jedoch nicht für Python 3.x-Versionen.

Der Hauptschuldige ist das xml/etree/ElementTree.pysiehe unten im Quellcode

# Import the C accelerators
try:
    # Element is going to be shadowed by the C implementation. We need to keep
    # the Python version of it accessible for some "creative" by external code
    # (see tests)
    _Element_Py = Element

    # Element, SubElement, ParseError, TreeBuilder, XMLParser
    from _elementtree import *
except ImportError:
    pass

Welches ist ein bisschen traurig.

Die Lösung besteht darin, es zuerst loszuwerden.

import _elementtree
try:
    del _elementtree.XMLParser
except AttributeError:
    # in case deleted twice
    pass
else:
    from xml.parsers import expat  # NOQA: F811
    oldcreate = expat.ParserCreate
    expat.ParserCreate = lambda encoding, sep: oldcreate(encoding, None)

Getestet auf Python 3.6.

Die Try- tryAnweisung ist nützlich, wenn Sie irgendwo in Ihrem Code ein Modul zweimal neu laden oder importieren und dabei seltsame Fehler wie z

  • maximale Rekursionstiefe überschritten
  • AttributeError: XMLParser

Übrigens sieht der etree-Quellcode wirklich chaotisch aus.

Europäische Sommerzeit
quelle
1

Kombinieren wir die Antwort von nonagon mit der Antwort von mzjn auf eine verwandte Frage :

def parse_xml(xml_path: Path) -> Tuple[ET.Element, Dict[str, str]]:
    xml_iter = ET.iterparse(xml_path, events=["start-ns"])
    xml_namespaces = dict(prefix_namespace_pair for _, prefix_namespace_pair in xml_iter)
    return xml_iter.root, xml_namespaces

Mit dieser Funktion können wir:

  1. Erstellen Sie einen Iterator, um sowohl Namespaces als auch ein analysiertes Baumobjekt abzurufen .

  2. Iterate über die erstellte Iterator bekommen die Namensräume dict , dass wir später in jedem passieren können find()oder findall()Anruf als von iMom0 sugested .

  3. Gibt das Stammelementobjekt und die Namespaces des analysierten Baums zurück.

Ich denke, dies ist der beste Ansatz, da weder eine Quell-XML noch die daraus resultierende analysierte xml.etree.ElementTreeAusgabe manipuliert werden kann.

Ich möchte auch der Antwort von barny die Bereitstellung eines wesentlichen Teils dieses Puzzles zuschreiben (dass Sie die analysierte Wurzel vom Iterator erhalten können). Bis dahin habe ich den XML-Baum in meiner Anwendung tatsächlich zweimal durchlaufen (einmal, um Namespaces zu erhalten, zweitens für einen Stamm).

z33k
quelle
Ich habe herausgefunden, wie man es benutzt, aber es funktioniert nicht für mich. Ich sehe immer noch die Namespaces in der Ausgabe
Taiko
1
Schauen Sie sich den Kommentar von iMom0 zur Frage von OP an . Mit dieser Funktion erhalten Sie sowohl das analysierte Objekt als auch die Möglichkeit, es mit find()und abzufragen findall(). Sie füttern diese Methoden einfach mit dem Diktat der Namespaces parse_xml()und verwenden das Präfix des Namespaces in Ihren Abfragen. ZB:et_element.findall(".//some_ns_prefix:some_xml_tag", namespaces=xml_namespaces)
z33k