Mi respuesta aborda el caso específico (y algo común) en el que realmente no necesita convertir todo el xml a json, pero lo que necesita es atravesar / acceder a partes específicas del xml, y necesita que sea rápido , y simple (usando operaciones json / dict-like).
Acercarse
Para esto, es importante tener en cuenta que analizar un xml para usar etree lxml
es muy rápido. La parte lenta en la mayoría de las otras respuestas es el segundo paso: atravesar la estructura etree (generalmente en python-land), convertirla a json.
Lo que me lleva al enfoque que encontré mejor para este caso: analizar el xml usando lxml
, y luego envolver los nodos etree (perezosamente), proporcionándoles una interfaz tipo dict.
Código
Aquí está el código:
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),
)
Esta implementación no está completa, por ejemplo, no admite de manera clara los casos en que un elemento tiene texto y atributos, o texto y elementos secundarios (solo porque no lo necesitaba cuando lo escribí ...) Debería ser fácil para mejorarlo, sin embargo.
Velocidad
En mi caso de uso específico, donde necesitaba únicos elementos procesos específicos del xml, este enfoque dio un suprising y aceleración sorprendente en un factor de 70 (!) En comparación con el uso de @ Martin Blech xmltodict y luego atravesar el dict directamente.
Prima
Como beneficio adicional, dado que nuestra estructura ya es similar a un dict, obtenemos otra implementación alternativa de forma xml2json
gratuita. Solo necesitamos pasar nuestra estructura tipo dict a json.dumps
. Algo como:
def xml_to_json(xmlstr, **kwargs):
x = xml_to_dictlike(xmlstr, **kwargs)
return json.dumps(x)
Si su xml incluye atributos, necesitaría usar algo alfanumérico attr_prefix
(por ejemplo, "ATTR_"), para asegurarse de que las claves son claves json válidas.
No he evaluado esta parte.