Skip to content

26. Del diccionario a XML y viceversa

Aquí nos proponemos descubrir el módulo [xml2dict], que permite transformar:

  • una cadena XML en un diccionario:
  • un diccionario en una cadena XML;

Antes de la llegada del jSON, la respuesta de los servicios web solía ser XML (eXtended Markup Language). Por otra parte, el protocolo de estos servicios web solía ser SOAP (Simple Object Process Protocol). SOAP es un protocolo que se basa en el protocolo HTTP de la web. Actualmente (2020), los servicios web suelen ser del tipo REST (Representational State Transfer). Los servicios web que hemos estudiado no son de ninguno de estos tipos, pero definitivamente se acercan más a REST que a SOAP. Sin embargo, prefiero decir que son de tipo «libre» o «desconocido», ya que no cumplen con todas las reglas del REST.

Vamos a demostrar qué tan fácil es transformar nuestras arquitecturas cliente/servidor jSON en arquitecturas cliente/servidor XML. Solo hay que usar el módulo [xmltodict].

Comenzamos instalándolo en un terminal de 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

Una vez hecho esto, veremos con un ejemplo qué se puede hacer con este módulo:

Image

El script [xml_01] es el siguiente:


from collections import OrderedDict

import xmltodict


# xmltodict.parse para pasar de XML al diccionario. El diccionario debe tener una raíz
# el diccionario generado es de tipo OrderedDict
# xmltodict.unparse para pasar del diccionario a XML

def ordereddict2dict(ordered_dictionary) -> dict:
    


def transform(message: str, dictionary: dict):
    # registros
    print(f"\n{message}-------")
    print(f"dictionnaire={dictionary}")
    # diccionario -> xml
    xml1 = xmltodict.unparse(dictionary)
    print(f"xml={xml1}")
    # xml → OrderedDict
    ordereddict_dictionary1 = xmltodict.parse(xml1)
    print(f"ordereddict_dictionary1={ordereddict_dictionary1}")
    # OrderedDict -> diccionario
    print(f"dict_dictionary1={ordereddict2dict(ordereddict_dictionary1)}")


# prueba 1
transform("test 1", {"nom": "séléné"})
# prueba 2
transform("test 2", {"famille": {"père": {"prénom": "andré"}, "mère": {"prénom": "angèle"}, "nom": "séléné"}})
# prueba 3
transform("test 3", {"famille": {"nom": "séléné", "père": {"prénom": "andré"}, "mère": {"prénom": "angèle"},
                                 "hobbies": ["chant", "footing"]}})
# prueba 4
transform("test 4", {'réponse': {
    'errores': ['Se requiere el método GET con los únicos parámetros [marié, enfants, salaire]', 'falta el parámetro [marié]',
                'falta el parámetro [enfants]', 'falta el parámetro [salaire]']}})
# prueba 5
transform("test 5", {'réponse': {
    'resultado': {'id': 0, 'casado': 'sí', 'hijos': 2, 'salario': 50000, 'impuesto': 1384, 'descuento': 384, 'recargo': 0,
               '«descuento»: 347, 'tasa': 0.14}}})
# prueba 6
transform("test 6", {"root": {'liste': ["un", "deux", "trois"]}})
# prueba 7
transform("test 7", {"root": {'liste': [{"un": [10, 11]}, {"deux": [20, 21]}, {"trois": [30, 31]}]}})
  • líneas 14-25: la función [transform] recibe un texto que se debe escribir [message] y un diccionario [dictionary];
  • línea 16: se muestra el mensaje;
  • línea 17: se muestra el diccionario recibido;
  • líneas 19-20: este diccionario se transforma en la cadena XML y esta se muestra. El método que realiza esta tarea es [xmltodict.unparse];
  • líneas 21-23: la cadena anterior XML se transforma en un diccionario y este se muestra. El método que realiza esto es [xmltodict.parse]. Este método no genera un diccionario de tipo [dict], sino de tipo [OrderedDict] (línea 1);
  • líneas 24-25: se transforma el tipo [OrderedDict] obtenido en el tipo [dict] mediante el método (aún no escrito) [ordereddict2dict]. Este método funciona de manera recursiva. Si algunos valores del diccionario son de tipo [OrderedDict, list], se examinan los valores de esas colecciones para determinar si también son de tipo [OrderedDict]. Si es así, se transforman al tipo [dict]. Cabe señalar que el método [xmltodict.parse] no genera ningún diccionario de tipo [dict];

Antes de analizar las funciones que faltan, revisemos los resultados para ver qué es lo que se busca:

La prueba 1 (líneas 28-29) produce los siguientes resultados:


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é'}
  • línea 2: el diccionario que se está probando. Cabe destacar un punto importante: el método [xml2dict.unparse] requiere que el diccionario tenga la forma {‘clave’: valor}, donde [valeur] puede ser, a su vez, un diccionario, una lista o un tipo simple;
  • líneas 3-4: la cadena XML derivada del diccionario. Va precedida del encabezado [<?xml version="1.0" encoding="utf-8"?>\n], que normalmente es la primera línea de un archivo XML;
  • línea 5: el tipo [OrderedDict] obtenido mediante el método [xml2dict.parse], al que se le pasa como parámetro la cadena XML anterior;
  • línea 6: el diccionario de tipo [dict] obtenido al aplicar el método [ordereddict2dict] al tipo anterior. Aquí se encuentra el diccionario original de la línea 2;

Todas las demás pruebas siguen el mismo esquema y deberían permitirle comprender cómo pasar de un diccionario a una cadena XML y, a continuación, de esa cadena XML al diccionario original.

Las demás pruebas arrojan los siguientes resultados:


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
  • las líneas 23 y 27 muestran un punto importante:
    • línea 23: los valores asociados a las claves del diccionario [result] son números;
    • línea 26: los valores asociados a las claves del diccionario [ordereddict_dictionary1] son cadenas de caracteres. Esta es una limitación de la biblioteca [xmltodict]. Su método [parse] solo genera cadenas de caracteres. Esto se puede entender fácilmente:
      • línea 25: la cadena XML a partir de la cual se genera el diccionario. En esta cadena, no hay ninguna indicación del tipo de datos encapsulados en las etiquetas XML. [xmltodict.parse] hace lo más lógico: deja todo como cadenas de caracteres en el diccionario generado. Existen otras bibliotecas similares a [xmltodict] en las que el tipo de datos encapsulados se indica en las etiquetas. Por ejemplo, podríamos encontrar la etiqueta [<enfants type=’int’>2</enfants>];
      • la consecuencia de esto es que, al utilizar un diccionario generado por el módulo [xmltodict], es necesario conocer el tipo de datos que encapsula para poder pasar del tipo «str» al tipo real de los datos;

Ahora analicemos el método [ordereddict2dict], que transforma un tipo [OrderedDict] en un tipo [dict]:


# xmltodict.parse para pasar de XML al diccionario. El diccionario debe tener una raíz
# el diccionario generado es de tipo OrderedDict
# xmltodict.unparse para pasar del diccionario a XML

def check(value):
    # si el valor es de tipo OrderedDict, se transforma
    if isinstance(value, OrderedDict):
        value2 = ordereddict2dict(value)
    # si el valor es de tipo lista, se transforma
    elif isinstance(value, list):
        value2 = list2list(value)
    else:
        # se trata de un tipo simple, no de una colección
        value2 = value
    # se devuelve el nuevo valor
    return value2


def list2list(liste: list) -> list:
    # la nueva lista
    newliste = []
    # se procesan los elementos de la lista de parámetros
    for value in liste:
        # se agrega value a la nueva lista
        newliste.append(check(value))
    # se devuelve la nueva lista
    return newliste


def ordereddict2dict(ordered_dictionary: OrderedDict) -> dict:
    # OrderedDict -> diccionario de manera recursiva
    newdict = {}
    for key, value in ordered_dictionary.items():
        # se almacena el valor en el nuevo diccionario
        newdict[key] = check(value)
    # se devuelve el diccionario
    return newdict
  • línea 30: la función [ordereddict2dict] recibe un tipo [OrderedDict] como parámetro;
  • línea 32: el diccionario de tipo [dict] que la función devolverá en la línea 37;
  • línea 33: se exploran todas las tuplas (clave, valor) del diccionario [ordered_dictionary];
  • línea 35: en el nuevo diccionario, se conserva la clave [key], pero el valor asociado no es [value], sino [check(value)]. La función [check(value)] se encarga de buscar, si [value] es una colección, todos los elementos de tipo [OrderedDict] y transformarlos al tipo [dict];

El método [check] se define en las líneas 5 a 16:

  • línea 5: no se conoce el tipo de [value], por lo que no se pudo escribir [value : type];
  • líneas 7-8: si [value] es de tipo [OrderedDict], entonces se llama de manera recursiva a la función [ordereddict2dict] que acabamos de comentar;
  • líneas 9-11: otro caso posible es que [value] sea una lista. En este caso, en la línea 11, se llama a la función [list2list] de las líneas 19-27;
  • líneas 12-14: el último caso es que [value] no es una colección, sino un tipo simple. La función [check], al igual que las funciones [ordereddict2dict] y [list2list], es recursiva. Sabemos que, en ese caso, siempre hay que prever el caso en que la recursión se detenga. Las líneas 12-14 representan ese caso;
  • línea 16: la función [check], ya sea llamada de forma recursiva o no, produce un valor [valeur2] que debe reemplazar el parámetro [value] de la línea 5;

El método [list2list], definido en las líneas 19 a 27, procesa una lista pasada como parámetro. La recorrerá y reemplazará cualquier valor de tipo [OrderedDict] que encuentre en ella por uno de tipo [dict].

  • línea 21: la nueva lista que creará la función;
  • líneas 23-25: todos los valores [value] de la lista se revisan y se reemplazan por el valor [check(value)]. Este valor [value] puede contener a su vez elementos del tipo [list] o [OrderedDict]. Estos serán procesados correctamente por la función recursiva [check];