Преобразование xml в словарь с помощью ElementTree

Я ищу синтаксический анализатор XML для словаря с использованием ElementTree, я уже нашел некоторые, но они исключают атрибуты, а в моем случае у меня много атрибутов.


person OHLÁLÁ    schedule 07.10.2011    source источник


Ответы (9)


def etree_to_dict(t):
    d = {t.tag : map(etree_to_dict, t.iterchildren())}
    d.update(('@' + k, v) for k, v in t.attrib.iteritems())
    d['text'] = t.text
    return d

Звоните как

tree = etree.parse("some_file.xml")
etree_to_dict(tree.getroot())

Это работает до тех пор, пока у вас фактически нет атрибута text; если вы это сделаете, измените третью строку в теле функции, чтобы использовать другой ключ. Кроме того, вы не можете обрабатывать смешанный контент с этим.

(Проверено на LXML.)

person Fred Foo    schedule 07.10.2011
comment
У меня была ошибка в iterchildren, поэтому я перешел на getchildren. В этом примере я получаю атрибуты, но значения узлов пусты, например: {'Tag': 'Lidars', 'lidars_list': [{'positive_towards_LOS' : 'false', 'scanner_3D': 'true', 'lidar': [{'name': []}, имя - LNAC, но я получаю пустой словарь - person OHLÁLÁ; 07.10.2011
comment
@ OHLÁLÁ Привет, вам удалось изменить код для преобразования XML в словарь? Спасибо - person Confounded; 24.03.2020
comment
Это возвращает map (что бы это ни было) в качестве значения первого ключа словаря, а не вложенного словаря. - person Confounded; 12.08.2020

Следующий фрагмент кода XML-to-Python-dict анализирует объекты, а также атрибуты, следующие за эту" спецификацию "преобразования XML в JSON:

from collections import defaultdict

def etree_to_dict(t):
    d = {t.tag: {} if t.attrib else None}
    children = list(t)
    if children:
        dd = defaultdict(list)
        for dc in map(etree_to_dict, children):
            for k, v in dc.items():
                dd[k].append(v)
        d = {t.tag: {k: v[0] if len(v) == 1 else v
                     for k, v in dd.items()}}
    if t.attrib:
        d[t.tag].update(('@' + k, v)
                        for k, v in t.attrib.items())
    if t.text:
        text = t.text.strip()
        if children or t.attrib:
            if text:
              d[t.tag]['#text'] = text
        else:
            d[t.tag] = text
    return d

Это используется:

from xml.etree import cElementTree as ET
e = ET.XML('''
<root>
  <e />
  <e>text</e>
  <e name="value" />
  <e name="value">text</e>
  <e> <a>text</a> <b>text</b> </e>
  <e> <a>text</a> <a>text</a> </e>
  <e> text <a>text</a> </e>
</root>
''')

from pprint import pprint

d = etree_to_dict(e)

pprint(d)

Результатом этого примера (согласно указанной выше «спецификации») должно быть:

{'root': {'e': [None,
                'text',
                {'@name': 'value'},
                {'#text': 'text', '@name': 'value'},
                {'a': 'text', 'b': 'text'},
                {'a': ['text', 'text']},
                {'#text': 'text', 'a': 'text'}]}}

Не обязательно красиво, но однозначно, и более простые входные данные XML приводят к более простому JSON. :)


Обновлять

Если вы хотите сделать обратное, выдать строку XML из JSON / dict, вы можете использовать:

try:
  basestring
except NameError:  # python3
  basestring = str

def dict_to_etree(d):
    def _to_etree(d, root):
        if not d:
            pass
        elif isinstance(d, str):
            root.text = d
        elif isinstance(d, dict):
            for k,v in d.items():
                assert isinstance(k, str)
                if k.startswith('#'):
                    assert k == '#text' and isinstance(v, str)
                    root.text = v
                elif k.startswith('@'):
                    assert isinstance(v, str)
                    root.set(k[1:], v)
                elif isinstance(v, list):
                    for e in v:
                        _to_etree(e, ET.SubElement(root, k))
                else:
                    _to_etree(v, ET.SubElement(root, k))
        else:
            assert d == 'invalid type', (type(d), d)
    assert isinstance(d, dict) and len(d) == 1
    tag, body = next(iter(d.items()))
    node = ET.Element(tag)
    _to_etree(body, node)
    return node

print(ET.tostring(dict_to_etree(d)))
person K3---rnc    schedule 09.04.2012
comment
Этот код выдает ошибку, если узел не имеет текста (например, первый узел <e> - вы получаете AttributeError: 'NoneType' object has no attribute 'strip' - person Bryan Oakley; 08.05.2012
comment
Есть ли пример обратного (dict - ›xml) преобразования? - person xander27; 08.09.2013
comment
Это один из лучших XML - ›диктов, которые я когда-либо пробовал (а их много: xmltodict, несколько рецептов на разных сайтах и ​​т. Д.) - person Basj; 17.02.2014
comment
@ xander27 Я добавил то, что, по моему мнению, является обратной функцией вышеупомянутой. Извините, немного поздно. :П - person K3---rnc; 19.02.2014
comment
Как сказал @Basj, - это лучшая реализация XML-словаря, которую я когда-либо пробовал. И я много пробовал. - person Amelio Vazquez-Reina; 26.02.2014
comment
В. хорошо. Но немного запутался, пока не понял, что для того, чтобы dict_to_etree действительно был инверсией, он должен возвращать etree, а не строку. Т.е. последняя строка return node - person spiderplant0; 30.07.2015
comment
@ spiderplant0 Спасибо, исправили. Однако SE позволяет любому редактировать ответы. :) - person K3---rnc; 31.07.2015
comment
По моему опыту, это решение не учитывает xmlns в корне документа. Простое решение - просто удалить его. Я нашел этот вопрос полезным дополнением - person jgrump2012; 03.08.2016
comment
Примечание. В python3 dict.iteritems() больше не существует. Измените 3 экземпляра этого метода на items(), и все снова в порядке. - person jcoppens; 07.10.2017

На основе @larsmans, если вам не нужны атрибуты, это даст вам более жесткий словарь -

def etree_to_dict(t):
    return {t.tag : map(etree_to_dict, t.iterchildren()) or t.text}
person s29    schedule 24.10.2013

Вот простая структура данных в xml (сохранить как file.xml):

<?xml version="1.0" encoding="UTF-8"?>
<Data>
  <Person>
    <First>John</First>
    <Last>Smith</Last>
  </Person>
  <Person>
    <First>Jane</First>
    <Last>Doe</Last>
  </Person>
</Data>

Вот код для создания из него списка объектов словаря.

from lxml import etree
tree = etree.parse('file.xml')
root = tree.getroot()
datadict = []
for item in root:
    d = {}
    for elem in item:
        d[elem.tag]=elem.text
    datadict.append(d)

datadict теперь содержит:

[{'First': 'John', 'Last': 'Smith'},{'First': 'Jane', 'Last': 'Doe'}]

и к нему можно получить доступ так:

datadict[0]['First']
'John'
datadict[1]['Last']
'Doe'
person bloodrootfc    schedule 18.01.2017
comment
Если есть какой-то дочерний тег, как мы можем это сделать? - person Murali; 28.03.2017
comment
Рассмотрим так: ‹? Xml version = 1.0 encoding = UTF-8?› ‹Data› ‹Person› ‹First› John ‹/First› ‹Last› Smith ‹/Last› ‹extra› ‹details1 ‹›married› да ‹ / женат ›‹status› богатый ‹/status› ‹/details1› ‹/extra› ‹/Person› ‹Person› ‹First› Джейн ‹/First› ‹Last› Doe ‹/Last› ‹extra› ‹details1›‹ женат ›Да ‹/married› ‹status› богатые ‹/status› ‹/details1› ‹details2› ‹property› yes ‹/property› ‹/details2› ‹/extra› ‹/Person›‹ ›/ Data› - person Murali; 28.03.2017

Вы можете использовать этот фрагмент, который напрямую преобразует его из xml в словарь.

import xml.etree.ElementTree as ET

xml = ('<xml>' +
       '<first_name>Dean Christian</first_name>' +
       '<middle_name>Christian</middle_name>' +
       '<last_name>Armada</last_name>' +
       '</xml>')
root = ET.fromstring(xml)

x = {x.tag: root.find(x.tag).text  for x in root._children}
# returns {'first_name': 'Dean Christian', 'last_name': 'Armada', 'middle_name': 'Christian'}
person Dean Christian Armada    schedule 10.05.2017
comment
Работает отлично. Спасибо - person Olfredos6; 13.02.2021

Для преобразования XML из / в словари Python мне отлично подошел xmltodict:

import xmltodict

xml = '''
<root>
  <e />
  <e>text</e>
  <e name="value" />
  <e name="value">text</e>
  <e> <a>text</a> <b>text</b> </e>
  <e> <a>text</a> <a>text</a> </e>
  <e> text <a>text</a> </e>
</root>
'''

xdict = xmltodict.parse(xml)

xdict теперь будет выглядеть как

OrderedDict([('root',
              OrderedDict([('e',
                            [None,
                             'text',
                             OrderedDict([('@name', 'value')]),
                             OrderedDict([('@name', 'value'),
                                          ('#text', 'text')]),
                             OrderedDict([('a', 'text'), ('b', 'text')]),
                             OrderedDict([('a', ['text', 'text'])]),
                             OrderedDict([('a', 'text'),
                                          ('#text', 'text')])])]))])

Если ваши XML-данные представлены не в виде необработанной строки / байтов, а в каком-то объекте ElementTree, вам просто нужно распечатать его как строку и снова использовать xmldict.parse. Например, если вы используете lxml для обработки XML-документов, тогда

from lxml import etree
e = etree.XML(xml)
xmltodict.parse(etree.tostring(e))

создаст тот же словарь, что и выше.

person albarji    schedule 27.12.2017

В документации lxml приводится пример как преобразовать дерево XML в dict of dicts:

def recursive_dict(element):
    return element.tag, dict(map(recursive_dict, element)) or element.text

Обратите внимание, что этот красивый быстрый и грязный преобразователь ожидает, что у дочерних элементов будут уникальные имена тегов, и будет молча перезаписывать любые данные, которые содержались в предыдущих братьях и сестрах с тем же именем. Для любого реального приложения преобразования xml-to-dict вам лучше написать свою собственную, более длинную версию.

Вы можете создать собственный словарь, чтобы иметь дело с предыдущими братьями и сестрами, у которых перезаписывается одно и то же имя:

from collections import UserDict, namedtuple
from lxml.etree import QName

class XmlDict(UserDict):
    """Custom dict to avoid preceding siblings with the same name being overwritten."""

    __ROOTELM = namedtuple('RootElm', ['tag', 'node'])

    def __setitem__(self, key, value):
        if key in self:
            if type(self.data[key]) is list:
                self.data[key].append(value)
            else:
                self.data[key] = [self.data[key], value]
        else:
            self.data[key] = value

    @staticmethod
    def xml2dict(element):
        """Converts an ElementTree Element to a dictionary."""
        elm = XmlDict.__ROOTELM(
            tag=QName(element).localname,
            node=XmlDict(map(XmlDict.xml2dict, element)) or element.text,
    )
    return elm

использование

from lxml import etree
from pprint import pprint

xml_f = b"""<?xml version="1.0" encoding="UTF-8"?>
            <Data>
              <Person>
                <First>John</First>
                <Last>Smith</Last>
              </Person>
              <Person>
                <First>Jane</First>
                <Last>Doe</Last>
              </Person>
            </Data>"""

elm = etree.fromstring(xml_f)
d = XmlDict.xml2dict(elm)

Вывод

In [3]: pprint(d)
RootElm(tag='Data', node={'Person': [{'First': 'John', 'Last': 'Smith'}, {'First': 'Jane', 'Last': 'Doe'}]})

In [4]: pprint(d.node)
{'Person': [{'First': 'John', 'Last': 'Smith'},
            {'First': 'Jane', 'Last': 'Doe'}]}
person kafran    schedule 12.08.2020
comment
Для меня это возвращает tuple, а не dictionary. - person Confounded; 12.08.2020
comment
Конечно, это можно улучшить. - person kafran; 12.08.2020

Основываясь на @larsmans, если результирующие ключи содержат информацию о пространстве имен xml, вы можете удалить ее перед записью в dict. Установите переменную xmlns равной пространству имен и удалите ее значение.

xmlns = '{http://foo.namespaceinfo.com}'

def etree_to_dict(t):
    if xmlns in t.tag:
        t.tag = t.tag.lstrip(xmlns)
    if d = {t.tag : map(etree_to_dict, t.iterchildren())}
    d.update(('@' + k, v) for k, v in t.attrib.iteritems())
    d['text'] = t.text
    return d
person DaveL17    schedule 27.01.2016

Уже несколько ответов, но вот одно компактное решение, которое отображает атрибуты, текстовое значение и дочерние элементы с помощью dict-computing:

def etree_to_dict(t):
    if type(t) is ET.ElementTree: return etree_to_dict(t.getroot())
    return {
        **t.attrib,
        'text': t.text,
        **{e.tag: etree_to_dict(e) for e in t}
    }
person Paamand    schedule 22.06.2021