Minha resposta aborda o caso específico (e um tanto comum) em que você realmente não precisa converter o xml inteiro em json, mas o que você precisa é atravessar / acessar partes específicas do xml, e você precisa ser rápido , e simples (usando operações do tipo json / dict).
Abordagem
Para isso, é importante observar que a análise de um xml para etree usando lxml
é super rápida. A parte lenta na maioria das outras respostas é a segunda passagem: atravessando a estrutura etree (geralmente em python-land), convertendo-a em json.
O que me leva à abordagem que achei melhor para este caso: analisar o xml usando lxml
e, em seguida, agrupar os nós etree (preguiçosamente), fornecendo a eles uma interface semelhante a um ditado.
Código
Aqui está o 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),
)
Essa implementação não está completa, por exemplo, não oferece suporte a casos em que um elemento possui texto e atributos, ou texto e filhos (apenas porque eu não precisei quando o escrevi ...) para melhorá-lo, no entanto.
Rapidez
No meu caso de uso específico, onde eu precisava processar apenas elementos específicos do xml, essa abordagem deu uma surpreendente e aceleração impressionante por um fator de 70 (!) Em relação ao uso do @ Martin Blech xmltodict e depois atravessando o dict diretamente.
Bônus
Como bônus, como nossa estrutura já é semelhante a um ditado, obtemos outra implementação alternativa de xml2json
graça. Só precisamos passar nossa estrutura semelhante a um ditado parajson.dumps
. Algo como:
def xml_to_json(xmlstr, **kwargs):
x = xml_to_dictlike(xmlstr, **kwargs)
return json.dumps(x)
Se o seu xml incluir atributos, você precisará usar alguns caracteres alfanuméricos attr_prefix
(por exemplo, "ATTR_"), para garantir que as chaves sejam chaves json válidas.
Eu não comparei essa parte.