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 de 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 son más bien del tipo REST (Representational State Transfer). Los servicios web que hemos estudiado no son de ninguno de estos tipos, pero se acercan definitivamente más a REST que a SOAP. No obstante, prefiero decir que son de tipo «libre» o «desconocido», ya que no respetan todas las reglas del REST.
Vamos a mostrar lo fácil que es transformar nuestras arquitecturas cliente/servidor jSON en arquitecturas cliente/servidor XML. Basta con utilizar el módulo [xmltodict].
Empezamos 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:

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 resultante 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': {
'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']}})
# prueba 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}}})
# 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 escribir [message] y un diccionario [dictionary];
- línea 16: visualización del 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 XML anterior se transforma en un diccionario y este se muestra. El método que realiza esta operación 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 tipo [dict] mediante el método (aún sin escribir) [ordereddict2dict]. Este método funciona de forma recursiva. Si algunos valores del diccionario son de tipo [OrderedDict, list], se examinan los valores de estas colecciones para determinar si también son de tipo [OrderedDict]. Si es así, se transforman en el 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 probado. 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 procedente 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], que recibe 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. Encontramos 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 esta cadena XML al diccionario original.
Las demás pruebas dan 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. Se trata de una deficiencia de la biblioteca [xmltodict]. Su método [parse] solo genera cadenas de caracteres. Esto se entiende 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: lo 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. Podríamos encontrar, por ejemplo, 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;
Centrémonos ahora en 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 resultante 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 añade 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 forma 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 será devuelto en la línea 37 por la función;
- 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 en el tipo [dict];
El método [check] se define en las líneas 5-16:
- línea 5: se desconoce el tipo de [value], por lo que no se ha podido escribir [value : type];
- líneas 7-8: si [value] es de tipo [OrderedDict], entonces se llama de forma 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 la situación en la que la recursividad se detiene. Las líneas 12-14 corresponden a este caso;
- línea 16: la función [check], llamada de forma recursiva o no, produce un valor [valeur2] que debe sustituir al parámetro [value] de la línea 5;
El método [list2list], definido en las líneas 19-27, procesa una lista pasada como parámetro. La recorrerá y sustituirá cualquier valor de tipo [OrderedDict] que encuentre en ella por un 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 exploran y se sustituyen por el valor [check(value)]. Este valor [value] puede contener a su vez elementos de tipo [list] o [OrderedDict]. Estos serán procesados correctamente por la función recursiva [check];