XML-Datei verändern und ein Knoten löschen

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
Albert
User
Beiträge: 23
Registriert: Montag 29. Oktober 2007, 09:42

Hi,

ich möchte eine XML-Datei verändern. Das bestehende XML-File sieht so aus:

Code: Alles auswählen

<?xml version="1.0" encoding="ISO-8859-1"?>
<repositoryTree version="0.5">
	<printName>Haustier</printName>
	<version>2007-02-06</version>
	
	<repositoryFolder>
		<printName>Katze</printName>
		<repositoryFolder>
			<printName>Große Katze</printName>
			<repositoryFolder>
				<printName>Name</printName>
				<repositoryItem  name="Sisi" printName="Sisi"/>
				<repositoryItem  name="Muschi" printName="Muschi"/>
			</repositoryFolder>
		</repositoryFolder>
	</repositoryFolder>
	
	<repositoryFolder>
		<printName>Hund</printName>
		<repositoryFolder>
			<printName>Dackel</printName>
			<repositoryItem name="Lutz" printName="Lutz"/>
			<repositoryItem name="Bruno" printName="Bruno"/>
		</repositoryFolder>
		<repositoryFolder>
			<printName>Schäferhund</printName>
			<repositoryItem name="Heinz" printName="Heinz"/>
		</repositoryFolder>
	</repositoryFolder>

</repositoryTree>
Nun möchte ich gerne den Knoten Hund löschen und dafür die Kinderknoten an dieser Stelle haben. Also statt Hund sollen dann direkt Dackel und Schäferhund kommen.

Das XML-File sieht dann so aus:

Code: Alles auswählen

<?xml version="1.0" encoding="ISO-8859-1"?>
<repositoryTree version="0.5">
	<printName>Haustier</printName>
	<version>2007-02-06</version>
	
	<repositoryFolder>
		<printName>Katze</printName>
		<repositoryFolder>
			<printName>Große Katze</printName>
			<repositoryFolder>
				<printName>Name</printName>
				<repositoryItem  name="Sisi" printName="Sisi"/>
				<repositoryItem  name="Muschi" printName="Muschi"/>
			</repositoryFolder>
		</repositoryFolder>
	</repositoryFolder>
	
    <repositoryFolder>
	    <printName>Dackel</printName>
		<repositoryItem name="Lutz" printName="Lutz"/>
		<repositoryItem name="Bruno" printName="Bruno"/>
	</repositoryFolder>
	<repositoryFolder>
		<printName>Schäferhund</printName>
		<repositoryItem name="Heinz" printName="Heinz"/>
	</repositoryFolder>

</repositoryTree>

Um das zu realisieren, benutzte ich die Bibliothek „elementtree.ElementTree“. Nun schaffe ich es zwar, die einzelnen Knoten zu durchlaufen. Aber was ich nicht hinbekomme, ist den Knoten Hund durch die entsprechenden Kindknoten zu ersetzten.

Aber ich glaube, mein Code ist auch nicht ganz richtig.

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-

import sys
import elementtree.ElementTree as etree

def walk_tree(node):
    print 'node text', node.text
    
    if node.text == 'Hund':
        node.remove(node.items())
        #pass 
    print 'Element --->', node.tag
    
    for (name, value) in node.attrib.items():
        print 'Attrib --> Name: %s  Value: %s \n\n' % (name, value)   
    children = node.getchildren()
    
    for child in children:
        walk_tree(child)

def check(inFileName):
    doc = etree.parse(inFileName)
    root = doc.getroot()
    walk_tree(root)

def main():
    args = sys.argv[1:]
    if len(args) != 1:
        print 'usage infile.xml'
        sys.exit(-1)
    check(args[0])

if __name__ == '__main__':
    main()
Es wäre nett, wenn mir jemand zeigen könnte, wie ich das hinbekomme.

Danke schon mal im Voraus!!

Albert
windner
User
Beiträge: 76
Registriert: Freitag 19. Oktober 2007, 11:25

Um das zu realisieren, benutzte ich die Bibliothek „elementtree.ElementTree“.
Meintest du xml.etree.(c)ElementTree? Oder ist das was anderes?

Geht's um's Üben oder soll das eine produktive Anwendung werden? Das wäre im produktiven Bereich wahrscheinlich mit XSLT am bequemsten, da erspart man sich die Programmierung.

Du willst ja eigentlich nur die Zeilen 19, 20, 30 löschen. Soll das Programm nur das können? In dem Fall könnte man auch mit xml.parsers.expat alles durchlaufen (damit erspart man sich walk_tree), und einfach alles ausgeben, bis auf die nicht mehr erwünschten Zeilen.
BlackJack

Da würde ich etwas anderes als `ElementTree` nehmen. `BeautifulSoup` oder `lxml`. Bei beiden kann man von Knoten auch zu den Elternknoten navigieren und bei `lxml` auch XPath zum Selektieren benutzen. Das geht bei `ElementTree` nicht. `lxml.etree` bietet die gleiche API wie `ElementTree`.

Code: Alles auswählen

from lxml import etree

def main():
    root = etree.fromstring(source)
    dog_folder = root.xpath('//printName[text()="Hund"]/..')[0]
    sub_folders = dog_folder.findall('repositoryFolder')
    parent_folder = dog_folder.getparent()
    dog_folder_position = parent_folder.index(dog_folder)
    del parent_folder[dog_folder_position]
    parent_folder[dog_folder_position:dog_folder_position] = sub_folders
    etree.dump(root)
Definition von `source` habe ich hier weggelassen, das XML steht ja schon im ersten Beitrag.
BlackJack

@windner: XSLT ist turingvollständig, also letztlich kann man sich das Programmieren auch damit nicht sparen. ;-)

Für kleine Sachen ist `xmlstarlet` auch ganz brauchbar wenn man nicht gleich mit XSLT auf das Problem schlagen möchte. Beispiel:

Code: Alles auswählen

#!/bin/sh

HUND_FOLDER="//printName[text()='Hund']/.."

xmlstarlet edit --template \
    --move "$HUND_FOLDER/repositoryFolder" "$HUND_FOLDER/.." \
    --delete "$HUND_FOLDER" \
    test.xml
Albert
User
Beiträge: 23
Registriert: Montag 29. Oktober 2007, 09:42

Hi BlackJack,
erst mal vielen Dank für Deine Hilfe und sorry für die späte Antwort. Wenn
ich es nach deinem Erstvorschlag mache, habe ich das Problem, dass ich meine
XML-Datei nicht als String einlesen kann.

Code: Alles auswählen

>>> from lxml import etree
>>> file = 'C:\\test.xml'
>>> source = etree.parse(file)
>>> root = etree.fromstring(source)
Traceback (most recent call last):
File "<pyshell#89>", line 1, in <module>
root = etree.fromstring(source)
File "etree.pyx", line 1973, in etree.fromstring
File "parser.pxi", line 987, in etree._parseMemoryDocument
ValueError: can only parse strings
>>>
Wie kann ich die XM-Datei als String parsen?

Grüße
BlackJack

Zeichenketten parst man mit `fromstring()`, dazu muss man natürlich auch eine Zeichenkette übergeben. `source` ist bei Dir ja schon eine geparste XML-Datei.
Albert
User
Beiträge: 23
Registriert: Montag 29. Oktober 2007, 09:42

Ok, glaub jetzt hab ich es verstanden. Aber leider bekomme ich jetzt bei der Zeile „etree.dump(root)“ diese Fehlermeldung:

Code: Alles auswählen

>>> etree.dump(root)
Traceback (most recent call last):
  File "<pyshell#106>", line 1, in <module>
    etree.dump(root)
  File "etree.pyx", line 1981, in etree.dump
TypeError: Argument 'elem' has incorrect type (expected etree._Element, got etree._ElementTree)
Und was mache ich jetzt ? :(
BlackJack

Dem Hinweis in der Fehlermeldung folgen. Du hast einen Baum, die Funktion will aber ein Element. Die Bäume haben eine Methode um das Wurzelelement abzufragen, ich glaube `getroot()` war's.
Y0Gi
User
Beiträge: 1454
Registriert: Freitag 22. September 2006, 23:05
Wohnort: ja

windner hat geschrieben:
Um das zu realisieren, benutzte ich die Bibliothek „elementtree.ElementTree“.
Meintest du xml.etree.(c)ElementTree? Oder ist das was anderes?
ElementTree wurde erst mit Python 2.5 in die Stdlib aufgenommen, davor konnte man es nur als separates Paket `elementtree` nutzen.
Albert
User
Beiträge: 23
Registriert: Montag 29. Oktober 2007, 09:42

Hi BlackJack,

es hat geklappt! Danke für deine Hilfe.

Grüße
Albert
User
Beiträge: 23
Registriert: Montag 29. Oktober 2007, 09:42

Hi,
ich bin’s mal wieder. Ich möchte jetzt noch zum Schäferhund einen weiteres 'repositoryItem' hinzufügen z.B. Karl.

So sieht mein Code aus:

Code: Alles auswählen

file = 'C:\\test.xml'
source = etree.parse(file.encode('ISO-8859-1'))
root = source.getroot()

def main():
    
    ### Hinzufügen von Karl
    karl_folder = root.xpath('//printName[text()="Schäferhund"]/..')[0]
    sub_folders_Schäferhund = karl_folder.findall('repositoryItem')
    entry = etree.SubElement(karl_folder,'<repositoryItem name="Karl" printName="Karl"/>') 
    karl_folder.insert(len(sub_folders_Schäferhund)+1,entry)
    
    ### Löscht Hund
    root = etree.fromstring(source)
    dog_folder = root.xpath('//printName[text()="Hund"]/..')[0]
    sub_folders = dog_folder.findall('repositoryFolder')
    parent_folder = dog_folder.getparent()
    dog_folder_position = parent_folder.index(dog_folder)
    del parent_folder[dog_folder_position]
    parent_folder[dog_folder_position:dog_folder_position] = sub_folders
    etree.dump(root)
    
    #dump
    etree.dump(root)
    
    #neue XML-Datei
    source.write('C:\\test2.xml', 'ISO-8859-1')
    

if __name__ == '__main__':
    main()
Aber damit fügt er mir den neuen Eintrag an den „repositoryFolder“. Ich möchte, dass er es mir als Sub-Elemente von Schäferhund hinzufügt.

Die XML-Datei müsste doch dann so aussehen.

Code: Alles auswählen

        <repositoryFolder>
			<printName>Dackel</printName>
			<repositoryItem name="Lutz" printName="Lutz"/>
			<repositoryItem name="Bruno" printName="Bruno"/>
		</repositoryFolder>
		<repositoryFolder>
			<printName>Schäferhund</printName>
			<repositoryItem name="Heinz" printName="Heinz"/>
			<repositoryItem name="Karl" printName="Karl"/>
		</repositoryFolder>
Was mache ich falsch?

Danke schon mal im voraus
BlackJack

Im Grunde wird damit etwas ziemlich kaputtes erzeugt. Du benutzt `SubElement` falsch. Damit erzeugt man ein Element und fügt keinen Text ein. Das erste Argument ist ein Tagname und versuchst ein Tag mit dem *Namen* '<repositoryItem name="Karl" printName="Karl"/>' zu erzeugen. Der Name von dem neuen Element ist aber nur 'repositoryItem' und es gibt zwei Attribute. Das wird so erzeugt:

Code: Alles auswählen

    entry = etree.SubElement(karl_folder,
                             'repositoryItem',
                             name='Karl',
                             printName='Karl')
Als nächstes ist das `insert()` überflüssig weil `SubElement()`, wie der Name schon andeutet, ein Unterelement anlegt. Sonst bräuchte man als erstes Argument ja nicht angeben, wovon es ein Unterelement sein soll.

Umlaute in Bezeichnern geben bei mir übrigens einen `SyntaxError` und wenn Du Umlaute in XPath-Ausdrücken benutzt, musst Du eine Unicode-Zeichenkette verwenden. Generell ist bei XML Unicode angesagt.
Albert
User
Beiträge: 23
Registriert: Montag 29. Oktober 2007, 09:42

Dank für Deine Antwort. Ich glaube, so langsam habe ich es verstanden.

Nur, wenn ich es nach Deinem Vorschlag mache, fügt er mir leider immer noch den neuen Eintrag an den „repositoryFolder“.

Code: Alles auswählen

        <repositoryFolder>
            <printName>Schäferhund</printName>
            <repositoryItem name="Heinz" printName="Heinz"/>
        <repositoryItem printName="Karl" name="Karl"/>
        </repositoryFolder>
Und das Nächste, was mich wundert, ist, dass er zuerst ‚printname’ schreibt und dann ‚name’, obwohl in meinen Code erst ‚name’ angegeben wird und dann ‚printname’ Hä???

Code: Alles auswählen

karl_folder = root.xpath('//printName[text()="Schäferhund"]/..')[0]
etree.SubElement(karl_folder, 'repositoryItem',
                             name='Karl', printName='Karl'.decode('iso-8859-1'))
Grüße
BlackJack

Die Reihenfolge ist weder bei Dictionaries noch bei XML-Attributen fest. Das ist also egal.

Und die Position stimmt auch mit Deinem Beispiel überein. Der neue Hund ist *im* 'repositoryFolder' nach dem ersten eingefügt. Du musst nach Tags gehen und nicht nach der Einrückung.
Antworten