Blog JSystems - z miłości do programowania

Przetwarzanie danych XML



Modułów służących do przetwarzania XML w Pythonie jest bardzo wiele, prezentuję tutaj taki z jakiego sam korzystam. Nawet jeśli zamierzasz korzystać z innego, możesz potraktować ten rozdział jako przykład.


W tym samym katalogu co kod który będę uruchamiać w ramach przykładów umieszczam plik o nazwie "dane.xml", a oto jego zawartość:


<?xml version="1.0" encoding="UTF-8"?>
<dane atrybut="jakaś wartość">
<imie>Andrzej</imie>
<nazwisko param="wartość przykładowa" param2="kolejna wartość przykładowa">Klusiewicz</nazwisko>
<wzrost>176cm</wzrost>
<adres>
     <miasto>Warszawa</miasto>
     <kod>02-019</kod>
</adres>
<jezyki>polski</jezyki>
<jezyki>angielski</jezyki>
<jezyki>Java</jezyki>
<jezyki>R</jezyki>
<jezyki>Python</jezyki>
<jezyki>PL/SQL</jezyki>
</dane>


Są to te same dane których używałem w rozdziale o przetwarzaniu JSON - tyle że tu w formacie XML.


 


Odczyt danych z pliku XML i sięganie do elementu po nazwie


Zaczniemy od zaimportowania odpowiedniej biblioteki i parsowania pliku XML.


import xml.etree.ElementTree as et
drzewo=et.parse('dane.xml')


Funkcja parse powoduje odczyt wskazanego pliku w całości. Tą samą funkcją można też przeładować dane, gdyby na przykład zostały zmodyfikowane a chcielibyśmy widzieć zmiany. Zmienna drzewo jest obiektem specjalnej klasy opakowującej, tym razem nie bedzie to żadna z omawianych dotychczas struktur danych, zapewne dla tego że w Pythonie nie ma właściwego odpowiednika strukturalnego. Możesz sprawdzić jaki to typ wywołując:


print(type(drzewo))


Na konsoli zostanie wyrzucony typ:


<class 'xml.etree.ElementTree.ElementTree'>


Jest to klasa zdefiniowana w tej samej bibliotece co narzędzia których bedziemy używać. W związku z tym będziemy musieli używać specjalnych funkcji (również wbudowanych w tę samą bibliotekę) służących do przetwarzania obiektu XML.


Format XML wymaga by struktura danych posiadała korzeń - a więc jeden element w którym zawarte są wszystkie pozostałe.  Taki element oczywiście w rzeczonym przykładzie występuje, a jest to element który w pliku jest reprezentowany parą tagów:


<dane atrybut="jakaś wartość">


....


</dane>


Aby uzyskać do niego dostęp wykorzystamy funkcję "getroot" która zwraca nam handler do korzenia:


root=drzewo.getroot()


W tej chwili możemy już poruszać się po drzewie XML. Dla przykładu jeśli zechcemy odczytać zawartość pola imię które w pliku znajduje się tu:


<?xml version="1.0" encoding="UTF-8"?>
<dane atrybut="jakaś wartość">
<imie>Andrzej</imie>


zastosujemy konstrukcję jak poniżej:


imie=root.find('imie')


Do zmiennej "imie" nie zostanie jednak przypisana wartość z elementu, czego pewnie można by się spodziewać, a obiekt klasy Element. Jest tak, ponieważ każdy z elementów może mieć jeszcze podelementy do których także będziemy uzyskiwać dostęp. Możemy to sprawdzić drukując samą zmienną "imie", oraz jej typ:


print(imie)
print(type(imie))


Na konsoli zobaczymy:


<Element 'imie' at 0x000002BBEC6CAA98>


<class 'xml.etree.ElementTree.Element'>


W związku z tym, będziemy potrzebowali wykorzystać atrybut "text" tej klasy do pobrania właściwej wartości:


print(imie.text)
print(type(imie.text))


co dopiero zwróci nam oczekiwane przez nas dane. Zrzut z konsoli jak zwykle:


Andrzej


<class 'str'>


Ok, mamy rozebrany proces na części pierwsze. Złóżmy teraz pacjenta do kupy. Całość dotychczasowego kodu, od otwarcia pliku do wydłubania danych z elementu "imie":


import xml.etree.ElementTree as et
drzewo=et.parse('dane.xml')
root=drzewo.getroot()
imie=root.find('imie').text
print(imie)


 


Sięganie po podelementy


Korzystając z funkcji "find" wydobywaliśmy obiekt "imie" klasy "Element" z obiektu "root". Tak się składa, że obiekt "root" także jest klasy "Element", a z tego płynie wniosek że wydobycie podelementów wyglądać będzie tak samo dla dowolnego obiektu tej klasy. Sprawdźmy:


import xml.etree.ElementTree as et
tree=et.parse('dane.xml')
root=tree.getroot()
adres=root.find('adres')
miasto=adres.find('miasto')
print(miasto.text)


Zróć uwagę że z obiektu "adres" za pomocą funkcji "find" wydobywam podelement "miasto" w taki sam sposób w jaki wcześniej wydobywałem obiekt "imie" z obiektu "root". Po uruchomieniu tego kodu, na konsoli zobaczymy "Warszawa".


 


Sięganie do elementu po pozycji


Podobnie jak mogę odnajdywać elementy po nazwie, tak mogę i po pozycji.


import xml.etree.ElementTree as et
drzewko=et.parse('dane.xml')
korzonek=drzewko.getroot()
drugi=korzonek[1].text
print(drugi)


W ten sposób odwołam się do elementu "nazwisko" będącego drugim elementem w pliku (czyli mającemu indeks 1 - liczenie od zera). Co jednak jeśli zechcielibyśmy odwołać się do elementu "kod" zagnieżdżonego w elemencie "adres"? Adres ma indeks 3 (czwarta pozycja), a "kod" ma indeks 1 (druga pozycja) wewnątrz elementu "adres", przypomina więc to listę zagnieżdżoną w liście... Skoro tak, to możemy do "kodu" dobrać się tak:


import xml.etree.ElementTree as et
drzewko=et.parse('dane.xml')
korzonek=drzewko.getroot()
zag=korzonek[3][1].text
print(zag)


 


Listy wartości w XML i odwoływanie się do "n-tego" wystąpienia tagu


W naszym pliku XML mamy jeszcze takie wartości:


...


...


</adres>
<jezyki>polski</jezyki>
<jezyki>angielski</jezyki>
<jezyki>Java</jezyki>
<jezyki>R</jezyki>
<jezyki>Python</jezyki>
<jezyki>PL/SQL</jezyki>
</dane>


Chcielibyśmy odnaleźć wszystkie elementy znajdujące się pomiędzy tagami <jezyki> i </jezyki>. Nic prostszego:


import xml.etree.ElementTree as et
d=et.parse("dane.xml")
root=d.getroot()
for e in root.findall('jezyki'): # tylko po elementach "jezyki"
    print(e.text)


Analogicznie do funkcji "find", jest też funkcja "findall" odnajdująca wszystkie elementy o określonym tagu. Funkcja ta zwraca nam zwyczajną listę, po której możemy iterować. Skoro jest to lista, to rozwiązuje nam to również problem typu "a jak się dobrać do 2 wystąpienia elementu xyz?"


import xml.etree.ElementTree as et
d=et.parse("dane.xml")
root=d.getroot()
print (root.findall('jezyki')[1].text)


 


Atrybuty


Atrybuty występują w naszym pliku z danymi w dwóch miejscach. Podświetliłem je na żółto w przykładzie poniżej.


<?xml version="1.0" encoding="UTF-8"?>
<dane atrybut="jakaś wartość">
<imie>Andrzej</imie>
<nazwisko param="wartość przykładowa" param2="kolejna wartość przykładowa">Klusiewicz</nazwisko>
<wzrost>176cm</wzrost>
<adres>
     <miasto>Warszawa</miasto>
     <kod>02-019</kod>
</adres>
<jezyki>polski</jezyki>
<jezyki>angielski</jezyki>
<jezyki>Java</jezyki>
<jezyki>R</jezyki>
<jezyki>Python</jezyki>
<jezyki>PL/SQL</jezyki>
</dane>


Chciałbym się teraz do nich dobrać.  Podobnie jak mogę na elemencie wywołać "text" by dostać jego zawartość, tak mogę wywołać również "attrib" dostając w zamian wszystkie atrybuty w postaci słownika:


import xml.etree.ElementTree as et
tree=et.parse("dane.xml")
root=tree.getroot()
nazwisko=root.find('nazwisko')
print(nazwisko.attrib)


Wynik działania:


{'param': 'wartość przykładowa', 'param2': 'kolejna wartość przykładowa'}


Skoro to słownik, to chcąc wybrać zawartość atrybutu "param", mogę posłużyć się notacją znaną nam już ze słowników i wykorzystać nazwę atrybutu jako klucz słownika (bo tak to jest przechowywane jak widać powyżej):


import xml.etree.ElementTree as et
tree=et.parse("dane.xml")
root=tree.getroot()
nazwisko=root.find('nazwisko')
print(nazwisko.attrib['param'])


Wynik działania:


wartość przykładowa


Ewentualnie to samo ale w krótszym zapisie:


import xml.etree.ElementTree as et
print(et.parse("dane.xml").getroot().find("nazwisko").attrib['param'])


 


Użyteczne "sztuczki"


Odczytywanie XML jako zwykły tekst


Gdybyśmy zechcieli odczytać zawartość pliku jako zwykły tekst, moglibyśmy oczywiście odczytać plik XML jako zwykły plik tekstowy. Nie zawsze jednak dane XML będą pochodzić z pliku, mogą być np. pobrane z jakiejś usługi sieciowej.  Wydrukowanie xml na konsolę w ten sposób:


import xml.etree.ElementTree as et
drzewko= et.parse("dane.xml")
korzen = drzewko.getroot()
print(korzen)


wyświetli nam informacje o obiekcie, a nie zawartość tekstową:


<Element 'dane' at 0x000002A21895A9A8>


Moduł ElementTree posiada metodę tostring która pozwoli nam na odczyt XML jako tekst:


import xml.etree.ElementTree as et
drzewko= et.parse("dane.xml")
korzen = drzewko.getroot()
print(et.tostring(korzen))


 


Sprawdzanie nazwy elementu


Poniżej pokazuję przykład wyświetlenia nazwy, atrybutów i zawartości wszystkich elementów na pierwszym poziomie zagnieżdżenia. Chodzi mi zasadniczo o wywołanie "e.tag" które jest tu nowością, a służy do pobierania nazwy elementu właśnie. Przykład jednak uznałem za użyteczny i dlatego zamieszczam go w takiej formie:


import xml.etree.ElementTree as et
r=et.parse("dane.xml").getroot()
for e in r:
    print(e.tag, e.attrib, e.text)


Wynik działania:


imie {} Andrzej


nazwisko {'param': 'wartość przykładowa', 'param2': 'kolejna wartość przykładowa'} Klusiewicz


wzrost {} 176cm


adres {}


jezyki {} polski


jezyki {} angielski


jezyki {} Java


jezyki {} R


jezyki {} Python


jezyki {} PL/SQL


 


Modyfikowanie drzewa XML


Modyfikowanie zawartości element


Podobnie jak używaliśmy funkcji find do odnalezienia elementu w celu jego odczytania, tak możemy jej użyć w celu zmiany zawartości. Tym razem zamiast wykorzystywać "text" do odczytu zawartości elementu, używamy go do podstawienia nowej wartości:


import xml.etree.ElementTree as et

r=et.parse("dane.xml").getroot()
for e in r:
    print(e.tag,e.text)


 


r.find('nazwisko').text="Klusiewicz po zmianie"

print("-------------------")
for e in r:
    print(e.tag,e.text)


 


Dodawanie i modyfikowanie atrybutów element


Dodawanie i modyfikowanie atrybutów elementów odbywa się za pomocą "attrib" tak samo jak jego pobieranie. Jeśli atrybut o podanej w nawiasach kwadratowych już istnieje dla podanego elementu, zostanie nadpisany, jeśli nie to zostanie dodany:


import xml.etree.ElementTree as et

r=et.parse("dane.xml").getroot()
for e in r:
    print(e.tag,e.text,e.attrib)

r.find('nazwisko').attrib['encoding']="utf-8"

print("-------------------")
for e in r:
    print(e.tag,e.text,e.attrib)


 


Tworzenie nowych elementów


Do tworzenia podelementów używamy "SubElement" z modułu ElementTree. Jako jego parametry podajemy element który ma się stać rodzicem nowo dodawanego elementu (w tym przypadku korzeń drzewa - czyli nowy element będzie równoległy do elementów nazwisko, imię etc), oraz nazwę element. Później dodajemy wartość elementu, tak samo jak robiliśmy to w istniejących elementach:


import xml.etree.ElementTree as et

r=et.parse("dane.xml").getroot()
for e in r:
    print(e.tag,e.text,e.attrib)

nowy=et.SubElement(r,"masa")
nowy.text=78

print("-------------------")
for e in r:
    print(e.tag,e.text,e.attrib)


Taki nowy podelement zostanie dodany na końcu. Gdybyśmy zechcieli dodać go w wybranym przez nas miejscu, użyjemy funkcji "insert":


import xml.etree.ElementTree as et

r=et.parse("dane.xml").getroot()
for e in r:
    print(e.tag,e.text,e.attrib)

nowy=et.Element("samochod")
nowy.text="Renault"
r.insert(0,nowy)

print("-------------------")
for e in r:
    print(e.tag,e.text,e.attrib)


Funkcja "insert" jest wywoływana na rzecz tego elementu w którym chcemy umieścić nowy element jako podelement. Przyjmuje ona dwa parametry. Pierwszy to pozycja (w tym przypadku pierwsza), drugi to element który ma zostać dodany.


 


Usuwanie elementów


Kasować elementy z drzewa XML możemy po pozycji:


import xml.etree.ElementTree as et

r=et.parse("dane.xml").getroot()
for e in r:
    print(e.tag,e.text)

del r[0]
print("-------------------")
for e in r:
    print(e.tag,e.text)


lub po nazwie :


import xml.etree.ElementTree as et

r=et.parse("dane.xml").getroot()
for e in r:
    print(e.tag,e.text)

naz =r.find('nazwisko')
r.remove(naz)

print("-------------------")
for e in r:
    print(e.tag,e.text)


 


Zapis drzewa XML do pliku


Zapis do pliku odbywa się w podobny sposób jak zapis do pliku tekstowego. Tym razem skorzystamy z  funkcji write wbudowanej w element. Zadbamy też o to by zapis wykorzystał właściwe kodowanie. Poniższy przykład odczytuje drzewo XML z pliku "dane.xml", a następnie bez jego modyfikowania zapisuje je do pliku "dane2.xml":


import xml.etree.ElementTree as et
d = et.parse("dane.xml")
d.write("dane2.xml",encoding="utf-8")


 

Przyjdź do nas na szkolenie z języka Python! Mamy szereg szkoleń w ofercie, od podstawowych po aplikacje webowe z użyciem Django, analizę danych, tesowanie, machine learning i wiele innych.
Sprawdź dostępne szkolenia Python
Zapisz się do newslettera aby otrzymywać najnowsze świeżynki pojawiające się na blogu!