Skip to content

26. Dal dizionario all'XML e viceversa

Qui esploreremo il modulo [xml2dict], che consente di convertire:

  • una stringa XML in un dizionario:
  • un dizionario in una stringa XML;

Prima dell'avvento di JSON, le risposte dei servizi web erano spesso in formato XML (eXtended Markup Language). Inoltre, il protocollo utilizzato per questi servizi web era spesso SOAP (Simple Object Access Protocol). SOAP è un protocollo basato sul protocollo HTTP del web. Attualmente (2020), i servizi web sono per lo più di tipo REST (Representational State Transfer). I servizi web che abbiamo studiato non sono di nessuno di questi tipi, ma sono sicuramente più vicini a REST che a SOAP. Tuttavia, preferisco dire che sono di tipo "libero" o "sconosciuto" perché non seguono tutte le regole di REST.

Mostreremo quanto sia facile trasformare le nostre architetture client/server JSON in architetture client/server XML. Tutto ciò che occorre fare è utilizzare il modulo [xmltodict].

Inizieremo installandolo in un terminale 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

Ora che abbiamo finito, vediamo un esempio di cosa possiamo fare con questo modulo:

Image

Lo script [xml_01] è il seguente:

from collections import OrderedDict

import xmltodict


#  xmltodict.parse to pass from XML to the dictionary. The dictionary must have a root
#  the dictionary produced is of type OrderedDict
#  xmltodict.unparse to go from the dictionary to XML

def ordereddict2dict(ordered_dictionary) -> dict:
    


def transform(message: str, dictionary: dict):
    #  logs
    print(f"\n{message}-------")
    print(f"dictionnaire={dictionary}")
    #  dict -> xml
    xml1 = xmltodict.unparse(dictionary)
    print(f"xml={xml1}")
    #  xml -> OrderedDict
    ordereddict_dictionary1 = xmltodict.parse(xml1)
    print(f"ordereddict_dictionary1={ordereddict_dictionary1}")
    #  OrderedDict -> dict
    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': {
    '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
transform("test 5", {'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
transform("test 6", {"root": {'liste': ["un", "deux", "trois"]}})
#  test 7
transform("test 7", {"root": {'liste': [{"un": [10, 11]}, {"deux": [20, 21]}, {"trois": [30, 31]}]}})
  • righe 14–25: la funzione [transform] accetta un testo da scrivere [message] e un dizionario [dictionary];
  • riga 16: visualizza il messaggio;
  • riga 17: viene visualizzato il dizionario ricevuto;
  • righe 19–20: questo dizionario viene convertito in una stringa XML, che viene poi visualizzata. Il metodo che esegue questa operazione è [xmltodict.unparse];
  • righe 21–23: la stringa XML precedente viene convertita in un dizionario, che viene poi visualizzato. Il metodo che esegue questa operazione è [xmltodict.parse]. Questo metodo non produce un dizionario di tipo [dict] ma di tipo [OrderedDict] (riga 1);
  • righe 24–25: il tipo [OrderedDict] risultante viene convertito in [dict] utilizzando il metodo (non ancora scritto) [ordereddict2dict]. Questo metodo funziona in modo ricorsivo. Se alcuni valori nel dizionario sono di tipo [OrderedDict, list], i valori di queste collezioni vengono esaminati per determinare se anche loro sono di tipo [OrderedDict]. In tal caso, vengono convertiti in tipo [dict]. Si noti che il metodo [xmltodict.parse] non produce alcun dizionario di tipo [dict];

Prima di esaminare le funzioni mancanti, diamo un'occhiata ai risultati per vedere cosa stiamo cercando:

Il test 1 (righe 28–29) produce i seguenti risultati:


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é'}
  • riga 2: il dizionario sottoposto a test. Nota importante: il metodo [xml2dict.unparse] richiede che il dizionario sia nella forma {‘key’: value}, dove [value] può essere un dizionario, una lista o un tipo semplice;
  • Righe 3–4: la stringa XML generata dal dizionario. È preceduta dall'intestazione [<?xml version="1.0" encoding="utf-8"?>\n], che normalmente è la prima riga di un file XML;
  • riga 5: il tipo [OrderedDict] ottenuto tramite il metodo [xml2dict.parse], che accetta come parametro la stringa XML precedente;
  • riga 6: il dizionario di tipo [dict] ottenuto applicando il metodo [ordereddict2dict] al tipo precedente. Si tratta del dizionario originale della riga 2;

Tutti gli altri test seguono lo stesso schema e dovrebbero aiutarti a capire come convertire un dizionario in una stringa XML e poi da quella stringa XML di nuovo nel dizionario originale.

Gli altri test producono i seguenti risultati:


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
  • Le righe 23 e 27 evidenziano un punto importante:
    • riga 23: i valori associati alle chiavi del dizionario [result] sono numeri;
    • riga 26: i valori associati alle chiavi del dizionario [ordereddict_dictionary1] sono stringhe. Questa è una limitazione della libreria [xmltodict]. Il suo metodo [parse] produce solo stringhe. Questo è facilmente comprensibile:
      • riga 25: la stringa XML da cui viene generato il dizionario. In questa stringa, non vi è alcuna indicazione del tipo di dati incapsulato all'interno dei tag XML. [xmltodict.parse] fa ciò che ha più senso: lascia tutto come stringhe nel dizionario risultante. Esistono altre librerie simili a [xmltodict] in cui il tipo dei dati incapsulati è specificato nei tag. Ad esempio, si potrebbe trovare il tag [<children type='int'>2</children>];
      • la conseguenza di ciò è che quando si utilizza un dizionario prodotto dal modulo [xmltodict], è necessario conoscere il tipo dei dati che incapsula per poter convertire il tipo ‘str’ nel tipo di dati effettivo;

Diamo ora un'occhiata più da vicino al metodo [ordereddict2dict], che converte un tipo [OrderedDict] in un tipo [dict]:

#  xmltodict.parse to pass from XML to the dictionary. The dictionary must have a root
#  the dictionary produced is of type OrderedDict
#  xmltodict.unparse to go from the dictionary to XML

def check(value):
    #  if the value is of type OrderedDict, we transform it
    if isinstance(value, OrderedDict):
        value2 = ordereddict2dict(value)
    #  if the value is of type list, we transform it
    elif isinstance(value, list):
        value2 = list2list(value)
    else:
        #  we're dealing with a simple type, not a collection
        value2 = value
    #  we return the new value
    return value2


def list2list(liste: list) -> list:
    #  the new list
    newliste = []
    #  the elements of the parameter list are used
    for value in liste:
        #  add value to the new list
        newliste.append(check(value))
    #  return the new list
    return newliste


def ordereddict2dict(ordered_dictionary: OrderedDict) -> dict:
    #  OrderedDict -> recursive dictation
    newdict = {}
    for key, value in ordered_dictionary.items():
        #  store the value in the new dictionary
        newdict[key] = check(value)
    #  we return the dictionary
    return newdict
  • riga 30: la funzione [ordereddict2dict] accetta un tipo [OrderedDict] come parametro;
  • riga 32: il dizionario di tipo [dict] che verrà restituito alla riga 37 dalla funzione;
  • riga 33: si esegue un'iterazione su tutte le tuple (chiave, valore) nel dizionario [ordered_dictionary];
  • Riga 35: Nel nuovo dizionario, la chiave [key] viene mantenuta, ma il valore associato non è [value] bensì [check(value)]. La funzione [check(value)] ha il compito di individuare, se [value] è una collezione, tutti gli elementi di tipo [OrderedDict] e di convertirli nel tipo [dict];

Il metodo [check] è definito alle righe 5–16:

  • riga 5: non conosciamo il tipo di [value], quindi non potremmo scrivere [value: type];
  • righe 7–8: se [value] è di tipo [OrderedDict], allora chiamiamo ricorsivamente la funzione [ordereddict2dict] che abbiamo appena commentato;
  • righe 9–11: un altro caso possibile è che [value] sia una lista. In questo caso, alla riga 11, chiamiamo la funzione [list2list] delle righe 19–27;
  • righe 12–14: il caso finale è che [value] non sia una collezione ma un tipo semplice. La funzione [check], come le funzioni [ordereddict2dict] e [list2list], è ricorsiva. Sappiamo che in questo caso dobbiamo sempre gestire la situazione in cui la ricorsione termina. Le righe 12–14 gestiscono questo caso;
  • riga 16: la funzione [check], sia che venga chiamata in modo ricorsivo o meno, produce un valore [value2] che deve sostituire il parametro [value] nella riga 5;

Il metodo [list2list] definito nelle righe 19–27 elabora una lista passata come parametro. La percorrerà e sostituirà qualsiasi valore [OrderedDict] trovato al suo interno con un tipo [dict].

  • riga 21: la nuova lista che la funzione creerà;
  • Righe 23–25: tutti i valori [value] nella lista vengono attraversati e sostituiti con il valore [check(value)]. Questo [value] può a sua volta contenere elementi di tipo [list] o [OrderedDict]. Essi saranno gestiti correttamente dalla funzione ricorsiva [check];