L'API XML ElementTree de Python fournit plusieurs méthodes d'analyse. Concentrez-vous sur la vitesse et comparez.
L'environnement utilisé pour la mesure est le suivant.
Python a une API XML ElementTree pour travailler avec XML.
Il existe quatre méthodes principales proposées.
fromstring
XMLParser
XMLPullParser
iterparse
Ceux-ci sont utilisés correctement selon des conditions telles que l'efficacité de la mémoire et l'évitement du blocage. Cette fois, je me fiche de ces conditions, je me concentre uniquement sur le temps de traitement d'un énorme XML.
Wiktionary cible les fichiers de vidage de la version anglaise. Les données de vidage sont fournies compressées avec bzip2. Il est utilisé tel qu'il est compressé sans le décompresser. (Ce sera environ 6 Go une fois étendu)
Le fichier de vidage a la structure de balise suivante.
<mediawiki>
<siteinfo> ⋯ </siteinfo>
<page> ⋯ </page>
<page> ⋯ </page>
⋮
<page> ⋯ </page>
</mediawiki>
Le tout est tellement énorme qu'il est compressé par une méthode appelée multi-stream afin que seule une partie puisse être retirée et traitée.
siteinfo | page × 100 | page × 100 | ⋯ |
Cette fois, nous traiterons le tout en récupérant les flux un par un.
Le deuxième flux et les suivants contiennent 100 balises «
<page> ⋯ </page>
<page> ⋯ </page>
⋮
<page> ⋯ </page>
Chaque balise <page>
a la structure suivante.
<page>
<title>Titre</title>
<ns>Espace de nom</ns>
<id>ID</id>
<revision>
<id>Revision ID</id>
<parentid>ID de révision avant la mise à jour</parentid>
<timestamp>Mettre à jour la date et l'heure</timestamp>
<contributor>
<username>Éditeur</username>
<id>ID de l'éditeur</id>
</contributor>
<comment>Commentaires au moment de l'édition</comment>
<model>wikitext</model>
<format>text/x-wiki</format>
<text bytes="longueur" xml:space="preserve">Contenu de l'article</text>
<sha1>Valeur de hachage</sha1>
</revision>
</page>
Parmi ceux-ci, le contenu de la balise «
Les scripts utilisés dans cet article sont contenus dans les référentiels suivants:
Implémentez getpage
pour récupérer <id> ʻet
de
La partie qui compte le nombre de lignes en utilisant getpage
est partagée.
partie commune
lines = 0
with open(target, "rb") as f:
f.seek(slen[0])
for length in slen[1:-1]:
for id, text in getpages(f.read(length)):
for line in text():
lines += 1
print(f"lines: {lines:,}")
Slen
est la longueur de chaque flux étudié à l'avance.fromstring
Créez une arborescence en passant XML sous forme de chaîne. Effectuer des opérations sur l'arborescence (rechercher des balises, les récupérer, etc.).
Code | Temps de traitement |
---|---|
python/research/countlines-text-xml.py | 5m50.826s |
def getpages(bz2data):
xml = bz2.decompress(bz2data).decode("utf-8")
pages = ET.fromstring(f"<pages>{xml}</pages>")
for page in pages:
if int(page.find("ns").text) == 0:
id = int(page.find("id").text)
with io.StringIO(page.find("revision/text").text) as t:
def text():
while (line := t.readline()):
yield line
yield id, text
Puisque XML nécessite une balise racine, il est temporairement inclus dans `
«
Il existe plusieurs types de «page.find (" id ")
s'applique uniquement à <id>
directement sous <page>
. Puisque <text>
est à l'intérieur de <revision>
, spécifiez page.find (" revision / text ")
. Cette méthode de spécification est appelée XPath.
XMLParser
fromstring
est l'analyseur utilisé en interne pour construire l'arborescence. L'utiliser directement est rapide car vous pouvez ignorer la construction de l'arborescence et simplement l'analyser.
C'est un analyseur du type appelé ** type push **, qui est presque le même que la méthode appelée SAX. Préparez une classe qui a des méthodes pour gérer les événements tels que le début et la fin de la balise.
Code | Temps de traitement |
---|---|
python/research/countlines-text-xmlparser.py | 6m46.163s |
class XMLTarget:
def __init__(self):
self._ns = 0
self._id = 0
self._data = []
self.pages = []
def start(self, tag, attrib):
self._data = []
def data(self, data):
self._data.append(data)
def end(self, tag):
if tag == "ns":
self._ns = int(self._data[0])
self._id = 0
elif self._id == 0 and tag == "id":
self._id = int(self._data[0])
elif self._ns == 0 and tag == "text":
text = []
cur = ""
for d in self._data:
if d == "\n":
text.append(cur)
cur = ""
else:
cur += d
if cur: text.append(cur)
self.pages.append((self._id, text))
self._data = []
def getpages(bz2data):
target = XMLTarget()
parser = ET.XMLParser(target=target)
parser.feed("<pages>")
parser.feed(bz2.decompress(bz2data).decode("utf-8"))
return target.pages
data
est le contenu de la balise. Comme il est envoyé séparé par de longues lignes ou des sauts de ligne, il est groupé par ligne.
La documentation présente un exemple de code similaire au suivant.
>>> class MaxDepth: # The target object of the parser
... maxDepth = 0
... depth = 0
... def start(self, tag, attrib): # Called for each opening tag.
... self.depth += 1
... if self.depth > self.maxDepth:
... self.maxDepth = self.depth
... def end(self, tag): # Called for each closing tag.
... self.depth -= 1
... def data(self, data):
... pass # We do not need to do anything with data.
... def close(self): # Called when all data has been parsed.
... return self.maxDepth
Les variables maxDepth
et depth
définies directement sous la classe sont des variables de classe et sont communes à toutes les instances. Dans ce code, les variables d'instance sont créées et les variables de classe sont masquées lors des mises à jour telles que «+ =» et «=». Par conséquent, la valeur de la variable de classe n'est jamais mise à jour avec «0».
Mais l'histoire est différente lorsque les variables de classe utilisent des méthodes telles que «ajouter» dans les listes. J'étais accro à cela et je ne pouvais pas écrire le code qui fonctionnait comme prévu.
XMLPullParser
C'est un type d'analyseur appelé ** type pull **. En Java, il s'appelle StAX pour SAX.
Contrairement au type push, il n'est pas nécessaire de préparer une classe, et elle peut être traitée par des boucles et des branchements conditionnels, ce qui simplifie le code.
[Référence] Comparaison entre XmlReader et SAX Reader | Microsoft Docs
Code | Temps de traitement |
---|---|
python/research/countlines-text-xmlpull.py | 6m4.553s |
def getpages(bz2data):
xml = bz2.decompress(bz2data).decode("utf-8")
parser = ET.XMLPullParser()
parser.feed("<pages>")
parser.feed(xml)
ns, id = 0, 0
for ev, el in parser.read_events():
if el.tag == "ns":
ns = int(el.text)
id = 0
elif id == 0 and el.tag == "id":
id = int(el.text)
elif ns == 0 and el.tag == "text":
with io.StringIO(el.text) as t:
def text():
while (line := t.readline()):
yield line
yield id, text
Puisqu'il existe plusieurs types de <id>
, initialisez et vérifiez avec ʻid = 0pour que seul celui qui se trouve immédiatement après
iterparse
C'est un analyseur de type pull. La différence avec «XMLPullParser» est expliquée comme suit.
XMLPullParser
est si flexible qu'il peut être gênant pour les utilisateurs qui veulent simplement l'utiliser. Si votre application n'empêche pas le blocage lors de la lecture de données XML, mais souhaite pouvoir analyser de manière incrémentielle, consultez ʻiterparse () `. Ceci est utile si vous lisez un grand document XML et que vous ne voulez pas qu'il soit entièrement en mémoire.
ʻIterparse () retourne un itérateur et avance l'analyse à chaque fois que vous continuez avec
next () . La structure du code utilisé est presque la même que celle de
XMLPullParser`.
Code | Temps de traitement |
---|---|
python/research/countlines-text-xmliter.py | 6m29.298s |
def getpages(bz2data):
xml = bz2.decompress(bz2data).decode("utf-8")
ns, id = 0, 0
for ev, el in ET.iterparse(io.StringIO(f"<pages>{xml}</pages>")):
if el.tag == "ns":
ns = int(el.text)
id = 0
elif id == 0 and el.tag == "id":
id = int(el.text)
elif ns == 0 and el.tag == "text":
with io.StringIO(el.text) as t:
def text():
while (line := t.readline()):
yield line
yield id, text
De tous les essais que j'ai essayés, la construction d'arbres fromstring
était la plus rapide. Cependant, la différence n'est pas grande, donc à moins que vous n'ayez affaire à des données de cette échelle (6 Go), ce ne sera pas un gros problème.
méthode | code | temps de traitement |
---|---|---|
fromstring | python/research/countlines-text-xml.py | 5m50.826s |
XMLParser | python/research/countlines-text-xmlparser.py | 6m46.163s |
XMLPullParser | python/research/countlines-text-xmlpull.py | 6m04.553s |
iterparse | python/research/countlines-text-xmliter.py | 6m29.298s |
Pour référence, le .NET Framework utilise également le type d'extraction XmlReader
pour créer l'arborescence. Il est plus rapide d'utiliser directement XmlReader
sans créer d'arborescence.
méthode | code | temps de traitement | Remarques |
---|---|---|---|
XmlDocument | fsharp/research/countlines-text-split-doc.fsx | 6m21.588s | Il y a un arbre |
XmlReader | fsharp/research/countlines-text-split-reader.fsx | 3m17.916s | Pas d'arbre |
Recommended Posts