2014-06-05 6 views
7

Angenommen, ich habe folgendes XML-Dokument:Iterate über Text und Elemente in lxml etree

<species> 
    Mammals: <dog/> <cat/> 
    Reptiles: <snake/> <turtle/> 
    Birds: <seagull/> <owl/> 
</species> 

Dann habe ich das species Element wie folgt:

import lxml.etree 
doc = lxml.etree.fromstring(xml) 
species = doc.xpath('/species')[0] 

Nun würde Ich mag ein drucken Liste der Tiere nach Arten gruppiert. Wie kann ich das mit der ElementTree API machen?

+0

, wenn Sie auf der rechten Seite durchsehen ... es sieht aus wie der 4. ein unter ähnlichen sollten Sie in die richtige Richtung ... –

+0

haben Sie die Kontrolle über das XML-Format haben?Normalerweise werden Klassifizierer wie Säugetiere usw. als xml-Elementnamen oder -attribute (z. B. ) ausgedrückt, so dass xpath-Selektoren leicht geschrieben werden können. – tdelaney

+0

Nein, ich kann das XML nicht ändern. – Alicia

Antwort

4

Wenn Sie alle Knoten aufzuzählen, werden Sie einen Textknoten mit der Klasse sehen, gefolgt von Elementknoten mit der Spezies:

>>> for node in species.xpath("child::node()"): 
...  print type(node), node 
... 
<class 'lxml.etree._ElementStringResult'> 
    Mammals: 
<type 'lxml.etree._Element'> <Element dog at 0xe0b3c0> 
<class 'lxml.etree._ElementStringResult'> 
<type 'lxml.etree._Element'> <Element cat at 0xe0b410> 
<class 'lxml.etree._ElementStringResult'> 
    Reptiles: 
<type 'lxml.etree._Element'> <Element snake at 0xe0b460> 
<class 'lxml.etree._ElementStringResult'> 
<type 'lxml.etree._Element'> <Element turtle at 0xe0b4b0> 
<class 'lxml.etree._ElementStringResult'> 
    Birds: 
<type 'lxml.etree._Element'> <Element seagull at 0xe0b500> 
<class 'lxml.etree._ElementStringResult'> 
<type 'lxml.etree._Element'> <Element owl at 0xe0b550> 
<class 'lxml.etree._ElementStringResult'> 

So können Sie es von dort bauen:

my_species = {} 
current_class = None 
for node in species.xpath("child::node()"): 
    if isinstance(node, lxml.etree._ElementStringResult): 
     text = node.strip(' \n\t:') 
     if text: 
      current_class = my_species.setdefault(text, []) 
    elif isinstance(node, lxml.etree._Element): 
     if current_class is not None: 
      current_class.append(node.tag) 
print my_species 

Ergebnisse in

{'Mammals': ['dog', 'cat'], 'Reptiles': ['snake', 'turtle'], 'Birds': ['seagull', 'owl']} 

Das ist alles zerbrechlich ... kleine Änderungen, wie die Textknoten angeordnet sind, kann das Parsing durcheinander bringen.

+0

Ich mag diese, einfache XPath :) – Alicia

+0

@alecxe - Sie verarbeiten eine ständig steigende Anzahl von früheren Textknoten und verwerfen alle bis auf die letzte jedes Mal ... Ich denke, meine Lösung ist einfacher. – tdelaney

+0

In Python 3 lautet der Typ des Textknotens 'lxml.etree._ElementUnicodeResult'. – saaj

2

Projektierungshinweisea

Die Antwort von @tdelaney grundsätzlich richtig ist, aber ich möchte eine Nuance von Python Elementstruktur API zeigen. Hier ist ein Zitat aus the lxml tutorial:

Elemente Text enthalten:

<root>TEXT</root> 

In vielen XML-Dokumente (datenzentrierte Dokumente), das ist der einzige Ort, an dem Text zu finden ist. Es wird von einem Blatt-Tag ganz unten in der Baumhierarchie eingekapselt.

Wenn jedoch XML für getaggten Textdokumente wie (X) HTML verwendet wird, Text kann auch zwischen verschiedenen Elementen, direkt in der Mitte des Baumes erscheinen:

<html><body>Hello<br/>World</body></html> 

Hier ist der <br/>-Tag umgeben von Text. Dies wird oft als XML im Document-Stil oder Mixed-Content bezeichnet. Elemente unterstützen dies durch ihre tail Eigenschaft. Es enthält den Text, der direkt auf das Element folgt, bis zum nächsten Element in der XML-Struktur.

Die beiden Eigenschaften text und tail reichen aus, um beliebigen Textinhalt in einem XML-Dokument darzustellen. Auf diese Weise benötigt die ElementTree-API keine speziellen Textknoten zusätzlich zur Element-Klasse, die dazu neigen, ziemlich oft im Weg zu sein (wie Sie vielleicht von klassischen DOM-APIs wissen).

Implementierung

Wenn man diese Eigenschaften berücksichtigt es möglich ist, Dokumenttext abzurufen, ohne den Baum zu Ausgangstextknoten zu zwingen.

#!/usr/bin/env python3.3 


import itertools 
from pprint import pprint 

try: 
    from lxml import etree 
except ImportError: 
    from xml.etree import cElementTree as etree 


def textAndElement(node): 
    '''In py33+ recursive generators are easy''' 

    yield node 

    text = node.text.strip() if node.text else None 
    if text: 
    yield text 

    for child in node: 
    yield from textAndElement(child) 

    tail = node.tail.strip() if node.tail else None 
    if tail: 
    yield tail 


if __name__ == '__main__': 
    xml = ''' 
    <species> 
     Mammals: <dog/> <cat/> 
     Reptiles: <snake/> <turtle/> 
     Birds: <seagull/> <owl/> 
    </species> 
    ''' 
    doc = etree.fromstring(xml) 

    pprint(list(textAndElement(doc))) 
    #[<Element species at 0x7f2c538727d0>, 
    #'Mammals:', 
    #<Element dog at 0x7f2c538728c0>, 
    #<Element cat at 0x7f2c53872910>, 
    #'Reptiles:', 
    #<Element snake at 0x7f2c53872960>, 
    #<Element turtle at 0x7f2c538729b0>, 
    #'Birds:', 
    #<Element seagull at 0x7f2c53872a00>, 
    #<Element owl at 0x7f2c53872a50>] 

    gen = textAndElement(doc) 
    next(gen) # skip root 
    groups = [] 
    for _, g in itertools.groupby(gen, type): 
    groups.append(tuple(g)) 

    pprint(dict(zip(*[iter(groups)] * 2))) 
    #{('Birds:',): (<Element seagull at 0x7fc37f38aaa0>, 
    #    <Element owl at 0x7fc37f38a820>), 
    #('Mammals:',): (<Element dog at 0x7fc37f38a960>, 
    #    <Element cat at 0x7fc37f38a9b0>), 
    #('Reptiles:',): (<Element snake at 0x7fc37f38aa00>, 
    #    <Element turtle at 0x7fc37f38aa50>)}