26. Do dicionário para XML e vice-versa
Aqui, vamos explorar o módulo [xml2dict], que permite converter:
- uma cadeia de caracteres XML num dicionário:
- um dicionário numa cadeia de caracteres XML;
Antes do advento do JSON, as respostas dos serviços web eram frequentemente em XML (eXtended Markup Language). Além disso, o protocolo para estes serviços web era frequentemente o SOAP (Simple Object Access Protocol). O SOAP é um protocolo baseado no protocolo HTTP da Web. Atualmente (2020), os serviços Web são, na sua maioria, do tipo REST (Representational State Transfer). Os serviços Web que estudámos não são de nenhum destes tipos, mas estão definitivamente mais próximos do REST do que do SOAP. No entanto, prefiro dizer que são do tipo «livre» ou «desconhecido», porque não seguem todas as regras do REST.
Vamos mostrar como é fácil transformar as nossas arquiteturas cliente/servidor JSON em arquiteturas cliente/servidor XML. Basta usar o módulo [xmltodict].
Começaremos por instalá-lo num terminal 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
Agora que isto está feito, vamos ver um exemplo do que podemos fazer com este módulo:

O script [xml_01] é o seguinte:
- linhas 14–25: a função [transform] recebe um texto a ser escrito [message] e um dicionário [dictionary];
- linha 16: exibe a mensagem;
- linha 17: o dicionário recebido é exibido;
- linhas 19–20: este dicionário é convertido numa cadeia XML, que é então exibida. O método que realiza esta operação é [xmltodict.unparse];
- linhas 21–23: a sequência XML anterior é convertida num dicionário, que é então exibido. O método que realiza esta tarefa é [xmltodict.parse]. Este método não produz um dicionário do tipo [dict], mas do tipo [OrderedDict] (linha 1);
- linhas 24–25: o tipo [OrderedDict] resultante é convertido para [dict] utilizando o método (ainda não escrito) [ordereddict2dict]. Este método funciona de forma recursiva. Se determinados valores no dicionário forem do tipo [OrderedDict, list], os valores dessas coleções são examinados para determinar se também são do tipo [OrderedDict]. Se for o caso, são convertidos para o tipo [dict]. Note-se que o método [xmltodict.parse] não produz quaisquer dicionários do tipo [dict];
Antes de examinar as funções em falta, vamos analisar os resultados para ver o que estamos à procura:
O Teste 1 (linhas 28–29) produz os seguintes 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é'}
- linha 2: o dicionário que está a ser testado. Note um ponto importante: o método [xml2dict.unparse] requer que o dicionário esteja na forma {‘chave’: valor}, onde [valor] pode ser um dicionário, uma lista ou um tipo simples;
- Linhas 3–4: A cadeia XML gerada a partir do dicionário. É precedida pelo cabeçalho [<?xml version="1.0" encoding="utf-8"?>\n], que é normalmente a primeira linha de um ficheiro XML;
- linha 5: o tipo [OrderedDict] obtido pelo método [xml2dict.parse], que recebe a cadeia XML anterior como parâmetro;
- linha 6: o dicionário do tipo [dict] obtido pela aplicação do método [ordereddict2dict] ao tipo anterior. Este é o dicionário original da linha 2;
Todos os outros testes seguem o mesmo padrão e devem ajudá-lo a compreender como converter um dicionário numa cadeia de caracteres XML e, em seguida, dessa cadeia de caracteres XML de volta ao dicionário original.
Os outros testes produzem os seguintes 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
- As linhas 23 e 27 destacam um ponto importante:
- linha 23: os valores associados às chaves do dicionário [result] são números;
- linha 26: os valores associados às chaves do dicionário [ordereddict_dictionary1] são cadeias de caracteres. Esta é uma limitação da biblioteca [xmltodict]. O seu método [parse] produz apenas cadeias de caracteres. Isto é fácil de compreender:
- linha 25: a cadeia de caracteres XML a partir da qual o dicionário é gerado. Nesta cadeia, não há indicação do tipo de dados encapsulado nas tags XML. [xmltodict.parse] faz o que faz mais sentido: deixa tudo como cadeias de caracteres no dicionário resultante. Existem outras bibliotecas semelhantes à [xmltodict] em que o tipo dos dados encapsulados é especificado nas tags. Por exemplo, pode encontrar-se a tag [<children type='int'>2</children>];
- a consequência disto é que, ao utilizar um dicionário produzido pelo módulo [xmltodict], é necessário conhecer o tipo dos dados que este encapsula para converter do tipo «str» para o tipo de dados real;
Vamos agora analisar mais de perto o método [ordereddict2dict], que converte um tipo [OrderedDict] num tipo [dict]:
- linha 30: a função [ordereddict2dict] recebe um tipo [OrderedDict] como parâmetro;
- linha 32: o dicionário do tipo [dict] que será devolvido na linha 37 pela função;
- linha 33: iteramos sobre todas as tuplas (chave, valor) no dicionário [ordered_dictionary];
- Linha 35: No novo dicionário, a chave [key] é mantida, mas o valor associado não é [value], mas sim [check(value)]. A função [check(value)] é responsável por encontrar, se [value] for uma coleção, todos os elementos do tipo [OrderedDict] e convertê-los para o tipo [dict];
O método [check] é definido nas linhas 5–16:
- linha 5: não sabemos o tipo de [value], por isso não poderíamos escrever [value: type];
- linhas 7–8: se [value] for do tipo [OrderedDict], então chamamos recursivamente a função [ordereddict2dict] que acabámos de comentar;
- linhas 9–11: outro caso possível é que [value] seja uma lista. Neste caso, na linha 11, chamamos a função [list2list] das linhas 19–27;
- linhas 12–14: o caso final é que [value] não é uma coleção, mas um tipo simples. A função [check], tal como as funções [ordereddict2dict] e [list2list], é recursiva. Sabemos que, neste caso, temos de tratar sempre a situação em que a recursão termina. As linhas 12–14 tratam deste caso;
- linha 16: a função [check], seja chamada recursivamente ou não, produz um valor [value2] que deve substituir o parâmetro [value] na linha 5;
O método [list2list] definido nas linhas 19–27 processa uma lista passada como parâmetro. Ele irá percorrê-la e substituir quaisquer valores [OrderedDict] encontrados nela por um tipo [dict].
- linha 21: a nova lista que a função irá criar;
- Linhas 23–25: Todos os valores [value] na lista são percorridos e substituídos pelo valor [check(value)]. Este [value] pode, por sua vez, conter elementos do tipo [list] ou [OrderedDict]. Estes serão tratados corretamente pela função recursiva [check];