Komisches Verhalten von Element.remove() in xml.etree

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Dingels
User
Beiträge: 61
Registriert: Dienstag 23. Dezember 2008, 19:50

Schönen guten Abend,

ich beschäftige mich zum ersten Mal mit dem Parsen von XML mittels xml.etree.ElementTree und habe dabei ein für mich merkwürdiges Verhalten der Methode Element.remove() festgestellt. Ich illustriere das mal schnell an einem kleinen Beispiel (Python 2.6):

Code: Alles auswählen

>>> import xml.etree.ElementTree as ET
>>> xml_string = """
... <target>
...   Hier steht Text. <head>Text</head> Noch mehr Text.
... </target>"""
>>> target = ET.XML(xml_string)
>>> target
<Element target at 3c3620>
>>> head = target.find('head')
>>> head
<Element head at 3c3800>
>>> # An dieser Stelle sollen nur die head-Tags samt Inhalt entfernt werden, aber es wird auch noch der Text dahinter entfernt!?
>>> target.remove(head)
>>> ET.dump(target)
<target>
  Hier steht Text. </target>
>>>
1. Warum entfernt die Methode remove() auch noch den Text hinter dem schließenden </head>-Tag?
2. Wie lässt sich dieses Verhalten vermeiden und wie kann ich nur die head-Tags samt Inhalt entfernen und den Rest erhalten?

Herzlichen Dank! :)

PS: Falls ihr mir einen anderen xml-Parser empfehlen wollt -> Ich kann und darf nur mit der Standardbibliothek arbeiten.
deets

Das liegt an der Modellierung von whitespace in ElementTree. Statt explizite Textknoten zu nehmen, gibt es die Eigenschaften .text und .tail. Erstere ist der *erste* Textknoten eines Elementes (bzw. wahrscheinlich eine Verknuepfung aller Textknoten bis zum ersten nicht-text-element oder dem Ende).

Die .tail-Eigenschaft ist der Text *nach* einem Element, der aber strukturell an das darueber gehoert.

Korrektes entfernen ist dadurch natuerlich etwas schwieriger. Ich hab da mal was gebastelt:

Code: Alles auswählen

import xml.etree.ElementTree as ET
xml_string = """
<target>
  Hier steht Text. <head>Text</head> Noch mehr Text.
</target>"""
target = ET.XML(xml_string)
head = target.find('head')

def good_remove(parent, child):
    tail = child.tail
    if not tail:
        target.remove(child)
    else:
        previous = None
        for sibling in parent:
            if sibling is child:
                break
            previous = sibling
        if previous is not None:
            previous.tail = tail if previous.tail is None else previous.tail + tail
        else:
            parent.text = tail if parent.text is None else parent.text + tail
        target.remove(child)


xml_string = """
<target>
  Hier steht Text. <foo>text</foo>foo tail <head>Text</head> Noch mehr Text.
</target>"""

target = ET.XML(xml_string)
head = target.find('head')

good_remove(target, head)        
ET.dump(target)
Dingels
User
Beiträge: 61
Registriert: Dienstag 23. Dezember 2008, 19:50

Verflixt, ich hab ja ganz vergessen, hier zu antworten. Entschuldigung. :oops:

Vielen Dank, deets, für deine Erklärung und deinen Lösungsvorschlag. Es war mir entgangen, dass es nicht nur das Attribut .text, sondern auch .tail gibt. Merkwürdig, dass das so komisch implementiert wurde...
Mir ging es übrigens nicht so sehr darum, explizit Elemente zu entfernen, sondern ich wollte eigentlich nur auf den Text vor und hinter den <head>-Tags zugreifen. Das geht ja dann tatsächlich sehr einfach:

Code: Alles auswählen

>>> target = ET.XML(xml_string)
>>> head = target.find('head')
>>> text = target.text + head.tail
Deinen Beispielcode habe ich jetzt nicht getestet, aber ich werde ihn mir mal als Snippet abspeichern. Denn solch eine Funktionalität werde ich sicherlich später noch mal brauchen. Also, vielen Dank nochmals und nichts für ungut. :)
Antworten