Seite 1 von 1

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

Verfasst: Donnerstag 15. November 2007, 10:49
von Albert
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

Verfasst: Donnerstag 15. November 2007, 12:35
von windner
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.

Verfasst: Donnerstag 15. November 2007, 12:57
von 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.

Verfasst: Donnerstag 15. November 2007, 16:32
von 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

Verfasst: Freitag 16. November 2007, 11:26
von Albert
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

Verfasst: Freitag 16. November 2007, 11:44
von 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.

Verfasst: Montag 19. November 2007, 10:15
von Albert
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 ? :(

Verfasst: Montag 19. November 2007, 12:00
von 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.

Verfasst: Montag 19. November 2007, 14:21
von Y0Gi
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.

Verfasst: Montag 19. November 2007, 15:17
von Albert
Hi BlackJack,

es hat geklappt! Danke für deine Hilfe.

Grüße

Verfasst: Donnerstag 22. November 2007, 08:16
von Albert
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

Verfasst: Donnerstag 22. November 2007, 10:12
von 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.

Verfasst: Donnerstag 22. November 2007, 13:01
von Albert
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

Verfasst: Donnerstag 22. November 2007, 13:58
von 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.