Skip to content

26. З словника в XML і навпаки

Тут ми пропонуємо ознайомитися з модулем [xml2dict], який дозволяє перетворити:

  • рядок XML у словник:
  • словник у рядок XML;

До появи jSON відповіді веб-сервісів часто мали формат XML (eXtended Markup Language). Крім того, протоколом цих веб-сервісів часто був SOAP (Simple Object Process Protocol). SOAP — це протокол, що базується на веб-протоколі HTTP. Наразі (2020 рік) веб-сервіси переважно належать до типу REST (Representational State Transfer). Веб-сервіси, які ми досліджували, не належать до жодного з цих типів, але, безперечно, ближчі до REST, ніж до SOAP. Проте я вважаю за краще сказати, що вони належать до «вільного» або «невідомого» типу, оскільки не дотримуються всіх правил REST.

Ми покажемо, наскільки легко перетворити наші архітектури «клієнт/сервер» jSON на архітектури «клієнт/сервер» XML. Для цього достатньо використати модуль [xmltodict].

Почнемо з його встановлення в терміналі 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

Зробивши це, розглянемо на прикладі, що можна зробити за допомогою цього модуля:

Image

Скрипт [xml_01] виглядає так:


from collections import OrderedDict

import xmltodict


# xmltodict.parse для переходу з XML до словника. Словник повинен мати корінь
# створений словник має тип OrderedDict
# xmltodict.unparse для переходу зі словника до XML

def ordereddict2dict(ordered_dictionary) -> dict:
    


def transform(message: str, dictionary: dict):
    # журнали
    print(f"\n{message}-------")
    print(f"dictionnaire={dictionary}")
    # словник -> xml
    xml1 = xmltodict.unparse(dictionary)
    print(f"xml={xml1}")
    # xml → OrderedDict
    ordereddict_dictionary1 = xmltodict.parse(xml1)
    print(f"ordereddict_dictionary1={ordereddict_dictionary1}")
    # OrderedDict -> словник
    print(f"dict_dictionary1={ordereddict2dict(ordereddict_dictionary1)}")


# тест 1
transform("test 1", {"nom": "séléné"})
# тест 2
transform("test 2", {"famille": {"père": {"prénom": "andré"}, "mère": {"prénom": "angèle"}, "nom": "séléné"}})
# тест 3
transform("test 3", {"famille": {"nom": "séléné", "père": {"prénom": "andré"}, "mère": {"prénom": "angèle"},
                                 "hobbies": ["chant", "footing"]}})
# тест 4
transform("test 4", {'réponse': {
    'помилки': ['Для методу GET потрібні лише параметри [marié, enfants, salaire]', 'відсутній параметр [marié]',
                'відсутній параметр [enfants]', 'відсутній параметр [salaire]']}})
# тест 5
transform("test 5", {'réponse': {
    'результат': {'id': 0, 'одружений': 'так', 'діти': 2, 'зарплата': 50000, 'податок': 1384, 'знижка': 384, 'надбавка': 0,
               'знижка': 347, 'ставка': 0.14}}})
# тест 6
transform("test 6", {"root": {'liste': ["un", "deux", "trois"]}})
# тест 7
transform("test 7", {"root": {'liste': [{"un": [10, 11]}, {"deux": [20, 21]}, {"trois": [30, 31]}]}})
  • рядки 14–25: функція [transform] отримує текст для виведення [message] та словник [dictionary];
  • рядок 16: виведення повідомлення;
  • рядок 17: виводиться отриманий словник;
  • рядки 19–20: цей словник перетворюється на рядок XML, який і виводиться на екран. Метод, що це виконує, — [xmltodict.unparse];
  • рядки 21–23: попередній рядок XML перетворюється на словник, який виводиться на екран. Метод, який це виконує, — [xmltodict.parse]. Цей метод створює не словник типу [dict], а типу [OrderedDict] (рядок 1);
  • рядки 24–25: отриманий тип [OrderedDict] перетворюється на тип [dict] за допомогою методу (ще не написаного) [ordereddict2dict]. Цей метод працює рекурсивно. Якщо деякі значення словника мають тип [OrderedDict, list], значення цих колекцій перевіряються на те, чи мають вони також тип [OrderedDict]. Якщо це так, їх перетворюють на тип [dict]. Слід зауважити, що метод [xmltodict.parse] не створює жодного словника типу [dict];

Перш ніж розглянути відсутні функції, проаналізуємо результати, щоб зрозуміти, що саме шукається:

Тест 1 (рядки 28–29) дає такі результати:


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é'}
  • рядок 2: тестований словник. Слід звернути увагу на важливий момент: метод [xml2dict.unparse] вимагає, щоб словник мав вигляд {‘ключ’: значення}, де [valeur] може бути словником, списком або простим типом;
  • рядки 3–4: рядок XML, отриманий із словника. Йому передує заголовок [<?xml version="1.0" encoding="utf-8"?>\n], який зазвичай є першим рядком файлу XML;
  • рядок 5: тип [OrderedDict], отриманий за допомогою методу [xml2dict.parse], який приймає як параметр попередній рядок XML;
  • рядок 6: словник типу [dict], отриманий шляхом застосування методу [ordereddict2dict] до попереднього типу. Тут ми бачимо вихідний словник із рядка 2;

Усі інші тести побудовані за тим самим принципом і мають допомогти вам зрозуміти, як перейти від словника до рядка XML, а потім від цього рядка XML до вихідного словника.

Інші тести дають такі результати:


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
  • рядки 23 і 27 показують важливий момент:
    • рядок 23: значення, пов’язані з ключами словника [result], є числами;
    • рядок 26: значення, пов’язані з ключами словника [ordereddict_dictionary1], є символьними рядками. Це недолік бібліотеки [xmltodict]. Її метод [parse] генерує лише символьні рядки. Це легко зрозуміти:
      • рядок 25: рядок XML, на основі якого створюється словник. У цьому рядку немає жодних вказівок щодо типу даних, інкапсульованих у тегах XML. [xmltodict.parse] діє найлогічніше: вона залишає все у вигляді рядків символів у створеному словнику. Існують й інші бібліотеки, подібні до [xmltodict], де тип інкапсульованих даних вказується у тегах. Наприклад, можна знайти тег [<enfants type=’int’>2</enfants>];
      • наслідком цього є те, що під час роботи зі словником, створеним модулем [xmltodict], необхідно знати тип даних, які він інкапсулює, щоб мати змогу перейти від типу «str» до реального типу даних;

Зараз зупинимося на методі [ordereddict2dict], який перетворює тип [OrderedDict] на тип [dict]:


# xmltodict.parse для переходу з XML до словника. Словник повинен мати корінь
# створений словник має тип OrderedDict
# xmltodict.unparse для переходу зі словника до XML

def check(value):
    # якщо значення має тип OrderedDict, його перетворюють
    if isinstance(value, OrderedDict):
        value2 = ordereddict2dict(value)
    # якщо значення має тип «список», його перетворюють
    elif isinstance(value, list):
        value2 = list2list(value)
    else:
        # маємо справу з простим типом, а не з колекцією
        value2 = value
    # повертається нове значення
    return value2


def list2list(liste: list) -> list:
    # новий список
    newliste = []
    # використовуємо елементи списку-параметра
    for value in liste:
        # додаємо value до нового списку
        newliste.append(check(value))
    # повертаємо новий список
    return newliste


def ordereddict2dict(ordered_dictionary: OrderedDict) -> dict:
    # OrderedDict -> словник рекурсивно
    newdict = {}
    for key, value in ordered_dictionary.items():
        # зберігаємо значення в новому словнику
        newdict[key] = check(value)
    # повертаємо словник
    return newdict
  • рядок 30: функція [ordereddict2dict] отримує тип [OrderedDict] як параметр;
  • рядок 32: словник типу [dict], який буде повернений у рядку 37 функцією;
  • рядок 33: обходимо всі кортежі (ключ, значення) словника [ordered_dictionary];
  • рядок 35: у новому словнику ключ [key] зберігається, але відповідне значення — не [value], а [check(value)]. Функція [check(value)] призначена для того, щоб, якщо [value] є колекцією, знайти всі елементи типу [OrderedDict] і перетворити їх на тип [dict];

Метод [check] визначено в рядках 5–16:

  • рядок 5: тип [value] невідомий, тому не можна було записати [value : type];
  • рядки 7–8: якщо [value] має тип [OrderedDict], то рекурсивно викликається функція [ordereddict2dict], яку щойно було прокоментовано;
  • рядки 9–11: інший можливий випадок — [value] є списком. У цьому випадку, у рядку 11, викликається функція [list2list] з рядків 19–27;
  • рядки 12–14: останній випадок — [value] є не колекцією, а простим типом. Функція [check], як і функції [ordereddict2dict] та [list2list], є рекурсивними. Відомо, що в такому разі завжди потрібно передбачити випадок, коли рекурсія зупиняється. Рядки 12–14 і є цим випадком;
  • рядок 16: функція [check], незалежно від того, викликається вона рекурсивно чи ні, повертає значення [valeur2], яке має замінити параметр [value] у рядку 5;

Метод [list2list], визначений у рядках 19–27, обробляє список, переданий як параметр. Він проходить по ньому та замінює всі знайдені в ньому значення типу [OrderedDict] на значення типу [dict].

  • рядок 21: новий список, який створить функція;
  • рядки 23–25: усі значення [value] у списку проходять обробку та замінюються на значення [check(value)]. Це значення [value] може само містити елементи типу [list] або [OrderedDict]. Вони будуть правильно оброблені рекурсивною функцією [check];