Blog JSystems - uwalniamy wiedzę!

Szukaj
XML - dane w strukturze drzewa
XML to zagnieżdżone znaczniki: korzeń zawiera elementy, te zawierają kolejne

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")
Szkolenie Python od podstaw, przez analizę danych, do machine learning w JSystems

Ten kurs daje Ci solidne podstawy Pythona. Jeśli chcesz pójść dalej i pod okiem trenera dojść od pierwszej linijki kodu do własnego modelu uczenia maszynowego, sprawdź nasze pięciodniowe szkolenie Python od podstaw, przez analizę danych, do machine learning. Pierwsze trzy dni to Python w praktyce: typy i kolekcje, instrukcje warunkowe i pętle, funkcje, moduły i pakiety, pliki tekstowe, usługi sieciowe oraz praca z bazą PostgreSQL. Czwarty dzień to analiza danych w pandas (wczytywanie CSV, TSV i XLS, filtrowanie, agregacje, statystyki) i wizualizacja w Matplotlib, Seaborn oraz Plotly. Piąty dzień to uczenie maszynowe w scikit-learn: regresja liniowa i logistyczna, drzewa decyzyjne, las losowy, XGBoost, walidacja predykcji, dobór hiperparametrów i przeciwdziałanie przeuczeniu. Szkolenie ma terminy gwarantowane, czyli odbywa się niezależnie od wielkości grupy.

Szkolenie Python od podstaw, przez analizę danych, do machine learning

To szkolenie może być dofinansowane dla Ciebie z KFS lub BUR.

★★★★★Średnia ocena naszych szkoleń w Google: 5/5

Najczęściej zadawane pytania

Której biblioteki użyto do przetwarzania XML w tym artykule?
W przykładach wykorzystywana jest biblioteka xml.etree.ElementTree, dostępna natywnie w Pythonie. Plik XML wczytujesz funkcją parse, która odczytuje jego zawartość w całości. Autor zaznacza, że modułów do XML jest wiele i pokazuje ten, z którego sam korzysta.
Czym jest korzeń w dokumencie XML i jak uzyskać do niego dostęp?
Korzeń to jeden element, w którym zawarte są wszystkie pozostałe, a format XML wymaga jego obecności. Po wczytaniu pliku funkcją parse uzyskujesz do niego dostęp funkcją getroot, która zwraca uchwyt do korzenia. Od tego miejsca możesz poruszać się po całym drzewie XML.
Dlaczego find nie zwraca od razu wartości elementu?
Funkcja find zwraca obiekt klasy Element, a nie samą wartość tekstową, ponieważ każdy element może mieć jeszcze podelementy. Aby pobrać właściwą zawartość tekstową, sięgasz po atrybut text tego obiektu. Dopiero on zwraca oczekiwany ciąg znaków jako typ str.
Jak odczytać wszystkie elementy o tym samym tagu?
Do odczytania wielu elementów o tej samej nazwie służy funkcja findall, będąca odpowiednikiem find dla wszystkich wystąpień. Zwraca ona zwykłą listę, po której możesz iterować. Ponieważ to lista, możesz też odwołać się do konkretnego wystąpienia po jego indeksie.
Jak odwołać się do elementu XML po pozycji?
Poza wyszukiwaniem po nazwie możesz sięgać po elementy według pozycji, indeksując obiekt jak listę, licząc od zera. Elementy zagnieżdżone traktujesz jak listę w liście, więc do podelementu docierasz podając kolejno indeks elementu nadrzędnego i indeks podelementu.

Komentarze (0)

Musisz być zalogowany by móc dodać komentarz. Zaloguj się przez Google

Brak komentarzy...