La mia risposta affronta il caso specifico (e piuttosto comune) in cui non è davvero necessario convertire l'intero XML in JSON, ma ciò di cui hai bisogno è attraversare / accedere a parti specifiche dell'XML, e hai bisogno che sia veloce , e semplice (usando operazioni tipo json / dict).
Approccio
Per questo, è importante notare che l'analisi di un XML per etree usando lxml
è super veloce. La parte lenta nella maggior parte delle altre risposte è il secondo passaggio: attraversare la struttura etree (di solito in terra di pitone), convertendola in json.
Il che mi porta all'approccio che ho trovato migliore per questo caso: analizzare l'xml usando lxml
e quindi avvolgere i nodi etree (pigramente), fornendo loro un'interfaccia simil-dict.
Codice
Ecco il codice:
from collections import Mapping
import lxml.etree
class ETreeDictWrapper(Mapping):
def __init__(self, elem, attr_prefix = '@', list_tags = ()):
self.elem = elem
self.attr_prefix = attr_prefix
self.list_tags = list_tags
def _wrap(self, e):
if isinstance(e, basestring):
return e
if len(e) == 0 and len(e.attrib) == 0:
return e.text
return type(self)(
e,
attr_prefix = self.attr_prefix,
list_tags = self.list_tags,
)
def __getitem__(self, key):
if key.startswith(self.attr_prefix):
return self.elem.attrib[key[len(self.attr_prefix):]]
else:
subelems = [ e for e in self.elem.iterchildren() if e.tag == key ]
if len(subelems) > 1 or key in self.list_tags:
return [ self._wrap(x) for x in subelems ]
elif len(subelems) == 1:
return self._wrap(subelems[0])
else:
raise KeyError(key)
def __iter__(self):
return iter(set( k.tag for k in self.elem) |
set( self.attr_prefix + k for k in self.elem.attrib ))
def __len__(self):
return len(self.elem) + len(self.elem.attrib)
# defining __contains__ is not necessary, but improves speed
def __contains__(self, key):
if key.startswith(self.attr_prefix):
return key[len(self.attr_prefix):] in self.elem.attrib
else:
return any( e.tag == key for e in self.elem.iterchildren() )
def xml_to_dictlike(xmlstr, attr_prefix = '@', list_tags = ()):
t = lxml.etree.fromstring(xmlstr)
return ETreeDictWrapper(
t,
attr_prefix = '@',
list_tags = set(list_tags),
)
Questa implementazione non è completa, ad esempio, non supporta in modo chiaro casi in cui un elemento ha sia testo che attributi o testo e figli (solo perché non ne avevo bisogno quando l'ho scritto ...) Dovrebbe essere facile per migliorarlo, però.
Velocità
Nel mio caso specifico impiego, dove avevo bisogno di solo elementi di processo specifici del xml, questo approccio ha dato una sorprendente e aumento di velocità sorprendente di un fattore di 70 (!) Rispetto all'utilizzo di @ Martin Blech xmltodict e poi attraversando direttamente il dict.
indennità
Come bonus, dal momento che la nostra struttura è già simile a una regola, otteniamo un'altra implementazione alternativa di xml2json
gratuitamente. Dobbiamo solo passare la nostra struttura simile a un dict json.dumps
. Qualcosa di simile a:
def xml_to_json(xmlstr, **kwargs):
x = xml_to_dictlike(xmlstr, **kwargs)
return json.dumps(x)
Se il tuo xml include attributi, dovrai usare un po 'alfanumerico attr_prefix
(ad esempio "ATTR_"), per assicurarti che le chiavi siano chiavi json valide.
Non ho confrontato questa parte.