26. Z słownika do XML i odwrotnie
W tym miejscu zamierzamy zapoznać się z modułem [xml2dict], który umożliwia przekształcenie:
- ciąg znaków XML na słownik:
- słownik na ciąg znaków XML;
Przed pojawieniem się formatu jSON odpowiedzi serwisów internetowych często miały postać XML (eXtended Markup Language). Ponadto protokołem tych usług internetowych był często SOAP (Simple Object Process Protocol). SOAP to protokół oparty na protokole internetowym HTTP. Obecnie (2020 r.) usługi internetowe są raczej typu REST (Representational State Transfer). Badane przez nas usługi internetowe nie należą do żadnego z tych typów, ale zdecydowanie bardziej zbliżone są do REST niż do SOAP. Niemniej jednak wolę określić je jako typu „swobodnego” lub „nieznanego”, ponieważ nie spełniają one wszystkich zasad REST.
Pokażemy, jak łatwo przekształcić nasze architektury klient-serwer typu jSON w architektury klient-serwer typu XML. Wystarczy użyć modułu [xmltodict].
Zaczynamy od zainstalowania go w terminalu Python:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install xmltodict
Collecting xmltodict
Using cached xmltodict-0.12.0-py2.py3-none-any.whl (9.2 kB)
Installing collected packages: xmltodict
Successfully installed xmltodict-0.12.0
Gdy to zrobimy, na przykładzie przyjrzymy się, co można zrobić za pomocą tego modułu:

Skrypt [xml_01] wygląda następująco:
from collections import OrderedDict
import xmltodict
# xmltodict.parse w celu przejścia z XML do słownika. Słownik musi posiadać katalog główny
# wygenerowany słownik jest typu OrderedDict
# xmltodict.unparse, aby przejść ze słownika do XML
def ordereddict2dict(ordered_dictionary) -> dict:
…
def transform(message: str, dictionary: dict):
# logi
print(f"\n{message}-------")
print(f"dictionnaire={dictionary}")
# słownik -> xml
xml1 = xmltodict.unparse(dictionary)
print(f"xml={xml1}")
# xml -> OrderedDict
ordereddict_dictionary1 = xmltodict.parse(xml1)
print(f"ordereddict_dictionary1={ordereddict_dictionary1}")
# OrderedDict -> słownik
print(f"dict_dictionary1={ordereddict2dict(ordereddict_dictionary1)}")
# test 1
transform("test 1", {"nom": "séléné"})
# test 2
transform("test 2", {"famille": {"père": {"prénom": "andré"}, "mère": {"prénom": "angèle"}, "nom": "séléné"}})
# test 3
transform("test 3", {"famille": {"nom": "séléné", "père": {"prénom": "andré"}, "mère": {"prénom": "angèle"},
"hobbies": ["chant", "footing"]}})
# test 4
transform("test 4", {'réponse': {
'błędy': ['Wymagana metoda GET z parametrami wyłącznie [marié, enfants, salaire]', 'brakuje parametru [marié]',
'brakuje parametru [enfants]', 'brakuje parametru [salaire]']}})
# test 5
transform("test 5", {'réponse': {
'wynik': {'id': 0, 'stan cywilny': 'tak', 'dzieci': 2, 'wynagrodzenie': 50000, 'podatek': 1384, 'ulga podatkowa': 384, 'dopłata podatkowa': 0,
'ulga': 347, 'stawka': 0,14}}})
# test 6
transform("test 6", {"root": {'liste': ["un", "deux", "trois"]}})
# test 7
transform("test 7", {"root": {'liste': [{"un": [10, 11]}, {"deux": [20, 21]}, {"trois": [30, 31]}]}})
- wiersze 14–25: funkcja [transform] otrzymuje tekst do zapisania [message] oraz słownik [dictionary];
- wiersz 16: wyświetlenie komunikatu;
- wiersz 17: wyświetla się otrzymany słownik;
- wiersze 19–20: słownik ten jest przekształcany w ciąg znaków XML, a następnie wyświetlany. Metodą, która to wykonuje, jest [xmltodict.unparse];
- wiersze 21–23: poprzedni ciąg znaków XML jest przekształcany w słownik, a następnie wyświetlany. Metodą, która to wykonuje, jest [xmltodict.parse]. Metoda ta nie generuje słownika typu [dict], lecz typu [OrderedDict] (wiersz 1);
- wiersze 24–25: uzyskany typ [OrderedDict] przekształca się w typ [dict] za pomocą metody (jeszcze nie napisanej) [ordereddict2dict]. Metoda ta działa rekurencyjnie. Jeśli niektóre wartości w słowniku są typu [OrderedDict, list], wartości tych zbiorów są sprawdzane pod kątem tego, czy one również są typu [OrderedDict]. Jeśli tak jest, są one przekształcane na typ [dict]. Należy zauważyć, że metoda [xmltodict.parse] nie generuje żadnego słownika typu [dict];
Zanim przeanalizujemy brakujące funkcje, przyjrzyjmy się wynikom, aby sprawdzić, czego szukamy:
Test 1 (wiersze 28–29) daje następujące wyniki:
test 1-------
dictionnaire={'nom': 'séléné'}
xml=<?xml version="1.0" encoding="utf-8"?>
<nom>séléné</nom>
ordereddict_dictionary1=OrderedDict([('nom', 'séléné')])
dict_dictionary1={'nom': 'séléné'}
- wiersz 2: testowany słownik. Należy zwrócić uwagę na ważną kwestię: metoda [xml2dict.unparse] wymaga, aby słownik miał postać {„klucz”: wartość}, gdzie [valeur] może być następnie słownikiem, listą lub typem prostym;
- wiersze 3–4: ciąg znaków XML pochodzący ze słownika. Jest on poprzedzony nagłówkiem [<?xml version="1.0" encoding="utf-8"?>\n], który zazwyczaj stanowi pierwszy wiersz pliku XML;
- wiersz 5: typ [OrderedDict] uzyskany za pomocą metody [xml2dict.parse], której jako parametr przekazano poprzedni ciąg znaków XML;
- wiersz 6: słownik typu [dict] uzyskany poprzez zastosowanie metody [ordereddict2dict] do poprzedniego typu. Znajdujemy tu oryginalny słownik z wiersza 2;
Wszystkie pozostałe testy są skonstruowane według tego samego schematu i powinny pomóc w zrozumieniu, jak przejść od słownika do ciągu znaków XML, a następnie od tego ciągu znaków XML do pierwotnego słownika.
Pozostałe testy dają następujące wyniki:
test 2-------
dictionnaire={'famille': {'père': {'prénom': 'andré'}, 'mère': {'prénom': 'angèle'}, 'nom': 'séléné'}}
xml=<?xml version="1.0" encoding="utf-8"?>
<famille><père><prénom>andré</prénom></père><mère><prénom>angèle</prénom></mère><nom>séléné</nom></famille>
ordereddict_dictionary1=OrderedDict([('famille', OrderedDict([('père', OrderedDict([('prénom', 'andré')])), ('mère', OrderedDict([('prénom', 'angèle')])), ('nom', 'séléné')]))])
dict_dictionary1={'famille': {'père': {'prénom': 'andré'}, 'mère': {'prénom': 'angèle'}, 'nom': 'séléné'}}
test 3-------
dictionnaire={'famille': {'nom': 'séléné', 'père': {'prénom': 'andré'}, 'mère': {'prénom': 'angèle'}, 'hobbies': ['chant', 'footing']}}
xml=<?xml version="1.0" encoding="utf-8"?>
<famille><nom>séléné</nom><père><prénom>andré</prénom></père><mère><prénom>angèle</prénom></mère><hobbies>chant</hobbies><hobbies>footing</hobbies></famille>
ordereddict_dictionary1=OrderedDict([('famille', OrderedDict([('nom', 'séléné'), ('père', OrderedDict([('prénom', 'andré')])), ('mère', OrderedDict([('prénom', 'angèle')])), ('hobbies', ['chant', 'footing'])]))])
dict_dictionary1={'famille': {'nom': 'séléné', 'père': {'prénom': 'andré'}, 'mère': {'prénom': 'angèle'}, 'hobbies': ['chant', 'footing']}}
test 4-------
dictionnaire={'réponse': {'erreurs': ['Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]', 'paramètre [marié] manquant', 'paramètre [enfants] manquant', 'paramètre [salaire] manquant']}}
xml=<?xml version="1.0" encoding="utf-8"?>
<réponse><erreurs>Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]</erreurs><erreurs>paramètre [marié] manquant</erreurs><erreurs>paramètre [enfants] manquant</erreurs><erreurs>paramètre [salaire] manquant</erreurs></réponse>
ordereddict_dictionary1=OrderedDict([('réponse', OrderedDict([('erreurs', ['Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]', 'paramètre [marié] manquant', 'paramètre [enfants] manquant', 'paramètre [salaire] manquant'])]))])
dict_dictionary1={'réponse': {'erreurs': ['Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]', 'paramètre [marié] manquant', 'paramètre [enfants] manquant', 'paramètre [salaire] manquant']}}
test 5-------
dictionnaire={'réponse': {'result': {'id': 0, 'marié': 'oui', 'enfants': 2, 'salaire': 50000, 'impôt': 1384, 'décôte': 384, 'surcôte': 0, 'réduction': 347, 'taux': 0.14}}}
xml=<?xml version="1.0" encoding="utf-8"?>
<réponse><result><id>0</id><marié>oui</marié><enfants>2</enfants><salaire>50000</salaire><impôt>1384</impôt><décôte>384</décôte><surcôte>0</surcôte><réduction>347</réduction><taux>0.14</taux></result></réponse>
ordereddict_dictionary1=OrderedDict([('réponse', OrderedDict([('result', OrderedDict([('id', '0'), ('marié', 'oui'), ('enfants', '2'), ('salaire', '50000'), ('impôt', '1384'), ('décôte', '384'), ('surcôte', '0'), ('réduction', '347'), ('taux', '0.14')]))]))])
dict_dictionary1={'réponse': {'result': {'id': '0', 'marié': 'oui', 'enfants': '2', 'salaire': '50000', 'impôt': '1384', 'décôte': '384', 'surcôte': '0', 'réduction': '347', 'taux': '0.14'}}}
test 6-------
dictionnaire={'root': {'liste': ['un', 'deux', 'trois']}}
xml=<?xml version="1.0" encoding="utf-8"?>
<root><liste>un</liste><liste>deux</liste><liste>trois</liste></root>
ordereddict_dictionary1=OrderedDict([('root', OrderedDict([('liste', ['un', 'deux', 'trois'])]))])
dict_dictionary1={'root': {'liste': ['un', 'deux', 'trois']}}
test 7-------
dictionnaire={'root': {'liste': [{'un': [10, 11]}, {'deux': [20, 21]}, {'trois': [30, 31]}]}}
xml=<?xml version="1.0" encoding="utf-8"?>
<root><liste><un>10</un><un>11</un></liste><liste><deux>20</deux><deux>21</deux></liste><liste><trois>30</trois><trois>31</trois></liste></root>
ordereddict_dictionary1=OrderedDict([('root', OrderedDict([('liste', [OrderedDict([('un', ['10', '11'])]), OrderedDict([('deux', ['20', '21'])]), OrderedDict([('trois', ['30', '31'])])])]))])
dict_dictionary1={'root': {'liste': [{'un': ['10', '11']}, {'deux': ['20', '21']}, {'trois': ['30', '31']}]}}
Process finished with exit code 0
- wiersze 23 i 27 wskazują na ważną kwestię:
- wiersz 23: wartości powiązane z kluczami słownika [result] to liczby;
- wiersz 26: wartości powiązane z kluczami słownika [ordereddict_dictionary1] to ciągi znaków. Jest to słaby punkt biblioteki [xmltodict]. Jej metoda [parse] generuje wyłącznie ciągi znaków. Łatwo to zrozumieć:
- wiersz 25: ciąg znaków XML, na podstawie którego tworzony jest słownik. W tym ciągu znaków nie ma żadnej informacji o typie danych zawartych w tagach XML. [xmltodict.parse] postępuje w sposób najbardziej logiczny: pozostawia wszystko w postaci ciągów znaków w wygenerowanym słowniku. Istnieją inne biblioteki podobne do [xmltodict], w których typ danych zawartych w tagach jest wskazany. Można na przykład znaleźć tag [<enfants type=’int’>2</enfants>];
- w związku z tym, korzystając ze słownika wygenerowanego przez moduł [xmltodict], należy znać typ danych, które on zawiera, aby móc przejść od typu „str” do rzeczywistego typu danych;
Zajmijmy się teraz metodą [ordereddict2dict], która przekształca typ [OrderedDict] w typ [dict]:
# xmltodict.parse, aby przejść z XML do słownika. Słownik musi mieć katalog główny
# wygenerowany słownik jest typu OrderedDict
# xmltodict.unparse, aby przejść ze słownika do XML
def check(value):
# jeśli wartość jest typu OrderedDict, należy ją przekształcić
if isinstance(value, OrderedDict):
value2 = ordereddict2dict(value)
# jeśli wartość jest typu lista, przekształca się ją
elif isinstance(value, list):
value2 = list2list(value)
else:
# mamy do czynienia z typem prostym, a nie z kolekcją
value2 = value
# zwracamy nową wartość
return value2
def list2list(liste: list) -> list:
# nowa lista
newliste = []
# przetwarzamy elementy listy parametrów
for value in liste:
# dodajemy wartość do nowej listy
newliste.append(check(value))
# zwracamy nową listę
return newliste
def ordereddict2dict(ordered_dictionary: OrderedDict) -> dict:
# OrderedDict -> słownik rekurencyjnie
newdict = {}
for key, value in ordered_dictionary.items():
# zapisujemy wartość w nowym słowniku
newdict[key] = check(value)
# zwracamy słownik
return newdict
- wiersz 30: funkcja [ordereddict2dict] przyjmuje jako parametr typ [OrderedDict];
- wiersz 32: słownik typu [dict], który zostanie zwrócony w wierszu 37 przez funkcję;
- wiersz 33: przeglądane są wszystkie krotki (klucz, wartość) słownika [ordered_dictionary];
- wiersz 35: w nowym słowniku klucz [key] zostaje zachowany, ale powiązana z nim wartość to nie [value], lecz [check(value)]. Funkcja [check(value)] ma za zadanie znaleźć, jeśli [value] jest zbiorem, wszystkie elementy typu [OrderedDict] i przekształcić je na typ [dict];
Metoda [check] jest zdefiniowana w wierszach 5–16:
- wiersz 5: nie znamy typu [value], dlatego nie mogliśmy zapisać [value : type];
- wiersze 7–8: jeśli [value] jest typu [OrderedDict], to rekurencyjnie wywołuje się funkcję [ordereddict2dict], którą właśnie skomentowano;
- wiersze 9–11: innym możliwym przypadkiem jest sytuacja, w której [value] jest listą. W takim przypadku, w wierszu 11, wywołuje się funkcję [list2list] z wierszy 19–27;
- wiersze 12–14: ostatnim przypadkiem jest sytuacja, w której [value] nie jest kolekcją, lecz typem prostym. Funkcja [check], podobnie jak funkcje [ordereddict2dict] i [list2list], jest rekurencyjna. Wiadomo, że w takim przypadku należy zawsze uwzględnić sytuację, w której rekurencja się kończy. Wiersze 12–14 dotyczą właśnie tego przypadku;
- wiersz 16: funkcja [check], niezależnie od tego, czy jest wywoływana rekurencyjnie, czy nie, zwraca wartość [valeur2], która musi zastąpić parametr [value] z wiersza 5;
Metoda [list2list] zdefiniowana w wierszach 19–27 przetwarza listę przekazaną jako parametr. Przegląda ją i zastępuje każdą znalezioną w niej wartość typu [OrderedDict] wartością typu [dict].
- wiersz 21: nowa lista, którą utworzy funkcja;
- wiersze 23–25: wszystkie wartości typu [value] z listy są przeglądane i zastępowane wartością typu [check(value)]. Ta wartość [value] może sama zawierać elementy typu [list] lub [OrderedDict]. Zostaną one poprawnie przetworzone przez funkcję rekurencyjną [check];