Как преобразовать xml-строку в словарь на Python?
у меня есть программа, которая читает xml-документ из сокета. У меня есть xml-документ, хранящийся в строке, которую я хотел бы преобразовать непосредственно в словарь Python, так же, как это делается в Django's simplejson библиотека.
возьмем в качестве примера:
str ="<?xml version="1.0" ?><person><name>john</name><age>20</age></person"
dic_xml = convert_to_dic(str)
затем dic_xml будет выглядеть как {'person' : { 'name' : 'john', 'age' : 20 } }
15 ответов:
Это отличный модуль, который кто-то создал. Я использовал его несколько раз. http://code.activestate.com/recipes/410469-xml-as-dictionary/
вот код с сайта на всякий случай ссылка идет плохо.
import cElementTree as ElementTree class XmlListConfig(list): def __init__(self, aList): for element in aList: if element: # treat like dict if len(element) == 1 or element[0].tag != element[1].tag: self.append(XmlDictConfig(element)) # treat like list elif element[0].tag == element[1].tag: self.append(XmlListConfig(element)) elif element.text: text = element.text.strip() if text: self.append(text) class XmlDictConfig(dict): ''' Example usage: >>> tree = ElementTree.parse('your_file.xml') >>> root = tree.getroot() >>> xmldict = XmlDictConfig(root) Or, if you want to use an XML string: >>> root = ElementTree.XML(xml_string) >>> xmldict = XmlDictConfig(root) And then use xmldict for what it is... a dict. ''' def __init__(self, parent_element): if parent_element.items(): self.update(dict(parent_element.items())) for element in parent_element: if element: # treat like dict - we assume that if the first two tags # in a series are different, then they are all different. if len(element) == 1 or element[0].tag != element[1].tag: aDict = XmlDictConfig(element) # treat like list - we assume that if the first two tags # in a series are the same, then the rest are the same. else: # here, we put the list in dictionary; the key is the # tag name the list elements all share in common, and # the value is the list itself aDict = {element[0].tag: XmlListConfig(element)} # if the tag has attributes, add those to the dict if element.items(): aDict.update(dict(element.items())) self.update({element.tag: aDict}) # this assumes that if you've got an attribute in a tag, # you won't be having any text. This may or may not be a # good idea -- time will tell. It works for the way we are # currently doing XML configuration files... elif element.items(): self.update({element.tag: dict(element.items())}) # finally, if there are no child tags and no attributes, extract # the text else: self.update({element.tag: element.text})пример использования:
tree = ElementTree.parse('your_file.xml') root = tree.getroot() xmldict = XmlDictConfig(root)//или, если вы хотите использовать XML-строку:
root = ElementTree.XML(xml_string) xmldict = XmlDictConfig(root)
xmltodict (полное раскрытие: я написал это) делает именно это:
xmltodict.parse(""" <?xml version="1.0" ?> <person> <name>john</name> <age>20</age> </person>""") # {u'person': {u'age': u'20', u'name': u'john'}}
следующий фрагмент XML-to-Python-dict анализирует сущности, а также атрибуты, следующие это XML-to-JSON "спецификация". Это наиболее общее решение, обрабатывающее все случаи XML.
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 pprint(etree_to_dict(e))вывод этого примера (согласно приведенной выше "спецификации") должен быть:
{'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, basestring): root.text = d elif isinstance(d, dict): for k,v in d.items(): assert isinstance(k, basestring) if k.startswith('#'): assert k == '#text' and isinstance(v, basestring) root.text = v elif k.startswith('@'): assert isinstance(v, basestring) 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: raise TypeError('invalid type: ' + str(type(d))) assert isinstance(d, dict) and len(d) == 1 tag, body = next(iter(d.items())) node = ET.Element(tag) _to_etree(body, node) return ET.tostring(node) pprint(dict_to_etree(d))
эта облегченная версия, пока не конфигурируемая, довольно легка для того чтобы портняжничать как нужно, и работает в старых питонах. Также он является жестким-это означает, что результаты одинаковы независимо от наличия атрибутов.
import xml.etree.ElementTree as ET from copy import copy def dictify(r,root=True): if root: return {r.tag : dictify(r, False)} d=copy(r.attrib) if r.text: d["_text"]=r.text for x in r.findall("./*"): if x.tag not in d: d[x.tag]=[] d[x.tag].append(dictify(x,False)) return dТак:
root = ET.fromstring("<erik><a x='1'>v</a><a y='2'>w</a></erik>") dictify(root)результаты:
{'erik': {'a': [{'x': '1', '_text': 'v'}, {'y': '2', '_text': 'w'}]}}
самые последние версии библиотек PicklingTools (1.3.0 и 1.3.1) поддерживают инструменты для преобразования XML в Python dict.
скачать можно здесь:PicklingTools 1.3.1
существует довольно много документации для конвертеров здесь: в документации подробно описаны все решения и проблемы, которые возникнут при преобразовании между XML и Python словарями (есть несколько крайних случаев: атрибуты, списки, анонимные списки, анонимные дикты, eval и т. д. что большинство конвертеров не справляются). В общем, хоть, конвертеры просты в использовании. Если это пример.xml ' содержит:
<top> <a>1</a> <b>2.2</b> <c>three</c> </top>затем, чтобы преобразовать его в словарь:
>>> from xmlloader import * >>> example = file('example.xml', 'r') # A document containing XML >>> xl = StreamXMLLoader(example, 0) # 0 = all defaults on operation >>> result = xl.expect XML() >>> print result {'top': {'a': '1', 'c': 'three', 'b': '2.2'}}есть инструменты для преобразования как в C++, так и в Python: C++ и Python делают индентичное преобразование, но C++ примерно в 60 раз быстрее
вы можете сделать это довольно легко с помощью lxml. Сначала установите его:
[sudo] pip install lxmlвот рекурсивная функция, которую я написал, что делает тяжелую работу для вас:
from lxml import objectify as xml_objectify def xml_to_dict(xml_str): """ Convert xml to dict, using lxml v3.4.2 xml processing library """ def xml_to_dict_recursion(xml_object): dict_object = xml_object.__dict__ if not dict_object: return xml_object for key, value in dict_object.items(): dict_object[key] = xml_to_dict_recursion(value) return dict_object return xml_to_dict_recursion(xml_objectify.fromstring(xml_str)) xml_string = """<?xml version="1.0" encoding="UTF-8"?><Response><NewOrderResp> <IndustryType>Test</IndustryType><SomeData><SomeNestedData1>1234</SomeNestedData1> <SomeNestedData2>3455</SomeNestedData2></SomeData></NewOrderResp></Response>""" print xml_to_dict(xml_string)ниже вариант сохраняет Родительский ключ / элемент:
def xml_to_dict(xml_str): """ Convert xml to dict, using lxml v3.4.2 xml processing library, see http://lxml.de/ """ def xml_to_dict_recursion(xml_object): dict_object = xml_object.__dict__ if not dict_object: # if empty dict returned return xml_object for key, value in dict_object.items(): dict_object[key] = xml_to_dict_recursion(value) return dict_object xml_obj = objectify.fromstring(xml_str) return {xml_obj.tag: xml_to_dict_recursion(xml_obj)}если вы хотите только вернуть поддерево и преобразовать его в dict, вы можете использовать элемент.найти() получить поддерево, а затем преобразовать его:
xml_obj.find('.//') # lxml.objectify.ObjectifiedElement instanceсмотрите документы lxml здесь. Я надеюсь, это поможет!
самый простой в использовании XML-парсер для Python-это ElementTree (начиная с 2.5 x и выше он находится в стандартной библиотеке xml.etree.ElementTree). Я не думаю, что есть что-то, что делает именно то, что вы хотите из коробки. Было бы довольно тривиально написать что-то, чтобы сделать то, что вы хотите, используя ElementTree, но зачем конвертировать в словарь, и почему бы просто не использовать ElementTree напрямую.
def xml_to_dict(node): u''' @param node:lxml_node @return: dict ''' return {'tag': node.tag, 'text': node.text, 'attrib': node.attrib, 'children': {child.tag: xml_to_dict(child) for child in node}}
код от http://code.activestate.com/recipes/410469-xml-as-dictionary/ работает хорошо, но если есть несколько элементов, которые являются одинаковыми в данном месте в иерархии он просто переопределяет их.
Я добавил прокладку между тем, что выглядит, чтобы увидеть, если элемент уже существует до себя.обновление.)( Если это так, извлекает существующую запись и создает списки из существующих и новых. Все последующие дубликаты добавляются в список.
Не уверен, что это может быть обработано более изящно, но это работает:
import xml.etree.ElementTree as ElementTree class XmlDictConfig(dict): def __init__(self, parent_element): if parent_element.items(): self.updateShim(dict(parent_element.items())) for element in parent_element: if len(element): aDict = XmlDictConfig(element) if element.items(): aDict.updateShim(dict(element.items())) self.updateShim({element.tag: aDict}) elif element.items(): self.updateShim({element.tag: dict(element.items())}) else: self.updateShim({element.tag: element.text.strip()}) def updateShim (self, aDict ): for key in aDict.keys(): if key in self: value = self.pop(key) if type(value) is not list: listOfDicts = [] listOfDicts.append(value) listOfDicts.append(aDict[key]) self.update({key: listOfDicts}) else: value.append(aDict[key]) self.update({key: value}) else: self.update(aDict)
от @K3 - - - rnc ответ (лучшее для меня) я добавил небольшие изменения, чтобы получить OrderedDict из XML-текста (несколько раз порядок имеет значение):
def etree_to_ordereddict(t): d = OrderedDict() d[t.tag] = OrderedDict() if t.attrib else None children = list(t) if children: dd = OrderedDict() for dc in map(etree_to_ordereddict, children): for k, v in dc.iteritems(): if k not in dd: dd[k] = list() dd[k].append(v) d = OrderedDict() d[t.tag] = OrderedDict() for k, v in dd.iteritems(): if len(v) == 1: d[t.tag][k] = v[0] else: d[t.tag][k] = v if t.attrib: d[t.tag].update(('@' + k, v) for k, v in t.attrib.iteritems()) 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следуя примеру @K3 - - - rnc, вы можете использовать его:
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 pprint(etree_to_ordereddict(e))надеюсь, что это помогает ;)
отказ от ответственности: Этот модифицированный синтаксический анализатор XML был вдохновлен Адам Кларк Исходный синтаксический анализатор XML работает для большинства простых случаев. Однако это не сработало для некоторых сложных XML-файлов. Я отладил код строка за строкой и, наконец, исправил некоторые проблемы. Если вы найдете какие-то ошибки, пожалуйста, дайте мне знать. Я рад это исправить.
class XmlDictConfig(dict): ''' Note: need to add a root into if no exising Example usage: >>> tree = ElementTree.parse('your_file.xml') >>> root = tree.getroot() >>> xmldict = XmlDictConfig(root) Or, if you want to use an XML string: >>> root = ElementTree.XML(xml_string) >>> xmldict = XmlDictConfig(root) And then use xmldict for what it is... a dict. ''' def __init__(self, parent_element): if parent_element.items(): self.updateShim( dict(parent_element.items()) ) for element in parent_element: if len(element): aDict = XmlDictConfig(element) # if element.items(): # aDict.updateShim(dict(element.items())) self.updateShim({element.tag: aDict}) elif element.items(): # items() is specialy for attribtes elementattrib= element.items() if element.text: elementattrib.append((element.tag,element.text )) # add tag:text if there exist self.updateShim({element.tag: dict(elementattrib)}) else: self.updateShim({element.tag: element.text}) def updateShim (self, aDict ): for key in aDict.keys(): # keys() includes tag and attributes if key in self: value = self.pop(key) if type(value) is not list: listOfDicts = [] listOfDicts.append(value) listOfDicts.append(aDict[key]) self.update({key: listOfDicts}) else: value.append(aDict[key]) self.update({key: value}) else: self.update({key:aDict[key]}) # it was self.update(aDict)
вот ссылка решение ActiveState - и код на случай, если он снова исчезнет.
================================================== xmlreader.py: ================================================== from xml.dom.minidom import parse class NotTextNodeError: pass def getTextFromNode(node): """ scans through all children of node and gathers the text. if node has non-text child-nodes, then NotTextNodeError is raised. """ t = "" for n in node.childNodes: if n.nodeType == n.TEXT_NODE: t += n.nodeValue else: raise NotTextNodeError return t def nodeToDic(node): """ nodeToDic() scans through the children of node and makes a dictionary from the content. three cases are differentiated: - if the node contains no other nodes, it is a text-node and {nodeName:text} is merged into the dictionary. - if the node has the attribute "method" set to "true", then it's children will be appended to a list and this list is merged to the dictionary in the form: {nodeName:list}. - else, nodeToDic() will call itself recursively on the nodes children (merging {nodeName:nodeToDic()} to the dictionary). """ dic = {} for n in node.childNodes: if n.nodeType != n.ELEMENT_NODE: continue if n.getAttribute("multiple") == "true": # node with multiple children: # put them in a list l = [] for c in n.childNodes: if c.nodeType != n.ELEMENT_NODE: continue l.append(nodeToDic(c)) dic.update({n.nodeName:l}) continue try: text = getTextFromNode(n) except NotTextNodeError: # 'normal' node dic.update({n.nodeName:nodeToDic(n)}) continue # text node dic.update({n.nodeName:text}) continue return dic def readConfig(filename): dom = parse(filename) return nodeToDic(dom) def test(): dic = readConfig("sample.xml") print dic["Config"]["Name"] print for item in dic["Config"]["Items"]: print "Item's Name:", item["Name"] print "Item's Value:", item["Value"] test() ================================================== sample.xml: ================================================== <?xml version="1.0" encoding="UTF-8"?> <Config> <Name>My Config File</Name> <Items multiple="true"> <Item> <Name>First Item</Name> <Value>Value 1</Value> </Item> <Item> <Name>Second Item</Name> <Value>Value 2</Value> </Item> </Items> </Config> ================================================== output: ================================================== My Config File Item's Name: First Item Item's Value: Value 1 Item's Name: Second Item Item's Value: Value 2
в какой-то момент мне пришлось разобрать и написать XML, который состоял только из элементов без атрибутов, поэтому отображение 1:1 из XML в dict было легко возможно. Это то, что я придумал в случае, если кто-то еще также не нуждается в атрибутах:
def xmltodict(element): if not isinstance(element, ElementTree.Element): raise ValueError("must pass xml.etree.ElementTree.Element object") def xmltodict_handler(parent_element): result = dict() for element in parent_element: if len(element): obj = xmltodict_handler(element) else: obj = element.text if result.get(element.tag): if hasattr(result[element.tag], "append"): result[element.tag].append(obj) else: result[element.tag] = [result[element.tag], obj] else: result[element.tag] = obj return result return {element.tag: xmltodict_handler(element)} def dicttoxml(element): if not isinstance(element, dict): raise ValueError("must pass dict type") if len(element) != 1: raise ValueError("dict must have exactly one root key") def dicttoxml_handler(result, key, value): if isinstance(value, list): for e in value: dicttoxml_handler(result, key, e) elif isinstance(value, basestring): elem = ElementTree.Element(key) elem.text = value result.append(elem) elif isinstance(value, int) or isinstance(value, float): elem = ElementTree.Element(key) elem.text = str(value) result.append(elem) elif value is None: result.append(ElementTree.Element(key)) else: res = ElementTree.Element(key) for k, v in value.items(): dicttoxml_handler(res, k, v) result.append(res) result = ElementTree.Element(element.keys()[0]) for key, value in element[element.keys()[0]].items(): dicttoxml_handler(result, key, value) return result def xmlfiletodict(filename): return xmltodict(ElementTree.parse(filename).getroot()) def dicttoxmlfile(element, filename): ElementTree.ElementTree(dicttoxml(element)).write(filename) def xmlstringtodict(xmlstring): return xmltodict(ElementTree.fromstring(xmlstring).getroot()) def dicttoxmlstring(element): return ElementTree.tostring(dicttoxml(element))
@dibrovsd: решение не будет работать, если xml имеет более одного тега с тем же именем
по вашему мнению, я немного изменил код и написал его для общего узла вместо корня:
from collections import defaultdict def xml2dict(node): d, count = defaultdict(list), 1 for i in node: d[i.tag + "_" + str(count)]['text'] = i.findtext('.')[0] d[i.tag + "_" + str(count)]['attrib'] = i.attrib # attrib gives the list d[i.tag + "_" + str(count)]['children'] = xml2dict(i) # it gives dict return d
У меня есть рекурсивный метод, чтобы получить словарь из элемента lxml
def recursive_dict(element): return (element.tag.split('}')[1], dict(map(recursive_dict, element.getchildren()), **element.attrib))
Comments