Moja odpowiedź dotyczy konkretnego (i dość powszechnego) przypadku, w którym tak naprawdę nie musisz konwertować całego pliku xml na json, ale potrzebujesz przejść / uzyskać dostęp do określonych części xml i potrzebujesz, aby był szybki , i proste (przy użyciu operacji podobnych do json / dict).
Podejście
W tym celu należy zauważyć, że parsowanie XML do etree przy użyciu lxml
jest bardzo szybkie. Powolną częścią większości innych odpowiedzi jest drugie przejście: przejście struktury etree (zwykle w python-land), konwersja do json.
Co prowadzi mnie do podejścia, które znalazłem najlepsze w tym przypadku: analizowanie XML za pomocą lxml
, a następnie owijanie węzłów etree (leniwie), zapewniając im interfejs podobny do dyktu.
Kod
Oto kod:
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),
)
Ta implementacja nie jest kompletna, np. Nie obsługuje czysto przypadków, w których element ma zarówno tekst, jak i atrybuty, lub zarówno tekst, jak i dzieci (tylko dlatego, że nie potrzebowałem go, kiedy go pisałem ...) Powinno być łatwe żeby go jednak ulepszyć.
Prędkość
W moim konkretnym przypadku użycia, w którym musiałem przetworzyć tylko określone elementy XML, to podejście dało zaskakujące i uderzające przyspieszenie o współczynnik 70 (!) W porównaniu z użyciem xmltodict @ Martina Blecha, a następnie przechodząc bezpośrednio przez dyktę .
Premia
Jako bonus, ponieważ nasza struktura jest już dyktowana, otrzymujemy kolejną alternatywną implementację xml2json
za darmo. Musimy tylko przekazać naszą strukturę podobną do dyktowania json.dumps
. Coś jak:
def xml_to_json(xmlstr, **kwargs):
x = xml_to_dictlike(xmlstr, **kwargs)
return json.dumps(x)
Jeśli Twój xml zawiera atrybuty, musisz użyć pewnych attr_prefix
znaków alfanumerycznych (np. „ATTR_”), aby upewnić się, że klucze są prawidłowymi kluczami json.
Nie testowałem tej części.