xml.etree.ElementTree - Api per la Manipolazione XML

Scopo Genera ed analizza documenti XML
Versione Python 2.5 e successive

La libreria ElementTree è un contributo alla libreria standard di Fredrick Lundh. Comprende gli strumenti per analizzare XML usando API basate su eventi e documenti, per cercare nei documenti analizzati tramite espressioni XPath e per creare o modificare documenti.

Creare documenti XML con ElementTree

Tutti gli esempi in questa sezione usano l'implementazione Python di ElementTree per semplicità, ma esiste anche una implementazione C in xml.etree.cElementTree.

I documenti XML analizzati sono rappresentati in memoria da da oggetti ElementTree ed Element connessi in una struttura ad albero basata sul modo nel quale sono annidati i nodi del documento XML

Analizzare un Intero Documento

Quando si analizza un intero documento con parse() viene restituita una istanza di ElementTree. La struttura ad albero si riferisce a tutti i dati del documento di input ed i nodi dell'albero possono essere cercati o manipolati sul posto. Sebbene questa flessibilità possa facilitare il lavoro con il documento analizzato, in genere richiede più memoria rispetto ad un approccio di analisi basato sugli eventi, visto che l'intero documento deve essere caricato in una volta.

L'ammontare di memoria richiesto per piccoli, semplici documenti tipo questo elenco di podcast rappresentati come uno schema OPML non è significativo.

 <?xml version="1.0" encoding="UTF-8"?>
 <opml version="1.0">
 <head>
	<title>My Podcasts</title>
	<dateCreated>Sun, 07 Mar 2010 15:53:26 GMT</dateCreated>
	<dateModified>Sun, 07 Mar 2010 15:53:26 GMT</dateModified>
 </head>
 <body>
  <outline text="Science and Tech">
    <outline text="APM: Future Tense" type="rss"
             xmlUrl="http://www.publicradio.org/columns/futuretense/podcast.xml"
             htmlUrl="http://www.publicradio.org/columns/futuretense/" />
	<outline text="Engines Of Our Ingenuity Podcast" type="rss"
             xmlUrl="http://www.npr.org/rss/podcast.php?id=510030"
             htmlUrl="http://www.uh.edu/engines/engines.htm" />
	<outline text="Science &#38; the City" type="rss"
             xmlUrl="http://www.nyas.org/Podcasts/Atom.axd"
             htmlUrl="http://www.nyas.org/WhatWeDo/SciencetheCity.aspx" />
  </outline>
  <outline text="Books and Fiction">
	<outline text="Podiobooker" type="rss"
             xmlUrl="http://feeds.feedburner.com/podiobooks"
             htmlUrl="http://www.podiobooks.com/blog" />
	<outline text="The Drabblecast" type="rss"
             xmlUrl="http://web.me.com/normsherman/Site/Podcast/rss.xml"
             htmlUrl="http://web.me.com/normsherman/Site/Podcast/Podcast.html" />
	<outline text="tor.com / category / tordotstories" type="rss"
             xmlUrl="http://www.tor.com/rss/category/TorDotStories"
             htmlUrl="http://www.tor.com/" />
  </outline>
  <outline text="Computers and Programming">
	<outline text="MacBreak Weekly" type="rss"
             xmlUrl="http://leo.am/podcasts/mbw"
             htmlUrl="http://twit.tv/mbw" />
	<outline text="FLOSS Weekly" type="rss"
             xmlUrl="http://leo.am/podcasts/floss"
             htmlUrl="http://twit.tv" />
	<outline text="Core Intuition" type="rss"
             xmlUrl="http://www.coreint.org/podcast.xml"
             htmlUrl="http://www.coreint.org/" />
  </outline>
  <outline text="Python">
    <outline text="PyCon Podcast" type="rss"
             xmlUrl="http://advocacy.python.org/podcasts/pycon.rss"
             htmlUrl="http://advocacy.python.org/podcasts/" />
	<outline text="A Little Bit of Python" type="rss"
             xmlUrl="http://advocacy.python.org/podcasts/littlebit.rss"
             htmlUrl="http://advocacy.python.org/podcasts/" />
	<outline text="Django Dose Everything Feed" type="rss"
             xmlUrl="http://djangodose.com/everything/feed/" />
  </outline>
  <outline text="Miscelaneous">
	<outline text="dhellmann's CastSampler Feed" type="rss"
             xmlUrl="http://www.castsampler.com/cast/feed/rss/dhellmann/"
             htmlUrl="http://www.castsampler.com/users/dhellmann/" />
  </outline>
 </body>
 </opml>

Per analizzare il file si passa un handle di file aperto a parse(), che legge i dati, elabora l'XML, e restituisce un oggetto ElementTree.

from xml.etree import ElementTree

with open('podcasts.opml', 'rt') as f:
    tree = ElementTree.parse(f)

print tree
$ python ElementTree_parse_opml.py
<xml.etree.ElementTree.ElementTree instance at 0xb76dc24c>

Attraversare l'albero analizzato

Ora che si è ottenuto un albero XML analizzato, si possono eseguire iterazioni su di esso, visitando tutti i figli per esaminare i loro attributi e contenuto.

from xml.etree import ElementTree

with open('podcasts.opml', 'rt') as f:
    tree = ElementTree.parse(f)

for node in tree.getiterator():
    print node.tag, node.attrib

In questo caso viene stampato l'intero albero, un tag alla volta

$ python ElementTree_dump_opml.py
opml {'version': '1.0'}
head {}
title {}
dateCreated {}
dateModified {}
body {}
outline {'text': 'Science and Tech'}
outline {'xmlUrl': 'http://www.publicradio.org/columns/futuretense/podcast.xml', 'text': 'APM: Future Tense', 'type': 'rss', 'htmlUrl': 'http://www.publicradio.org/columns/futuretense/'}
outline {'xmlUrl': 'http://www.npr.org/rss/podcast.php?id=510030', 'text': 'Engines Of Our Ingenuity Podcast', 'type': 'rss', 'htmlUrl': 'http://www.uh.edu/engines/engines.htm'}
outline {'xmlUrl': 'http://www.nyas.org/Podcasts/Atom.axd', 'text': 'Science & the City', 'type': 'rss', 'htmlUrl': 'http://www.nyas.org/WhatWeDo/SciencetheCity.aspx'}
outline {'text': 'Books and Fiction'}
outline {'xmlUrl': 'http://feeds.feedburner.com/podiobooks', 'text': 'Podiobooker', 'type': 'rss', 'htmlUrl': 'http://www.podiobooks.com/blog'}
outline {'xmlUrl': 'http://web.me.com/normsherman/Site/Podcast/rss.xml', 'text': 'The Drabblecast', 'type': 'rss', 'htmlUrl': 'http://web.me.com/normsherman/Site/Podcast/Podcast.html'}
outline {'xmlUrl': 'http://www.tor.com/rss/category/TorDotStories', 'text': 'tor.com / category / tordotstories', 'type': 'rss', 'htmlUrl': 'http://www.tor.com/'}
outline {'text': 'Computers and Programming'}
outline {'xmlUrl': 'http://leo.am/podcasts/mbw', 'text': 'MacBreak Weekly', 'type': 'rss', 'htmlUrl': 'http://twit.tv/mbw'}
outline {'xmlUrl': 'http://leo.am/podcasts/floss', 'text': 'FLOSS Weekly', 'type': 'rss', 'htmlUrl': 'http://twit.tv'}
outline {'xmlUrl': 'http://www.coreint.org/podcast.xml', 'text': 'Core Intuition', 'type': 'rss', 'htmlUrl': 'http://www.coreint.org/'}
outline {'text': 'Python'}
outline {'xmlUrl': 'http://advocacy.python.org/podcasts/pycon.rss', 'text': 'PyCon Podcast', 'type': 'rss', 'htmlUrl': 'http://advocacy.python.org/podcasts/'}
outline {'xmlUrl': 'http://advocacy.python.org/podcasts/littlebit.rss', 'text': 'A Little Bit of Python', 'type': 'rss', 'htmlUrl': 'http://advocacy.python.org/podcasts/'}
outline {'xmlUrl': 'http://djangodose.com/everything/feed/', 'text': 'Django Dose Everything Feed', 'type': 'rss'}
outline {'text': 'Miscelaneous'}
outline {'xmlUrl': 'http://www.castsampler.com/cast/feed/rss/dhellmann/', 'text': "dhellmann's CastSampler Feed", 'type': 'rss', 'htmlUrl': 'http://www.castsampler.com/users/dhellmann/'}

Se si vuole stampare solo i gruppi di nomi e gli URL dei feed per i podcast, escludendo tutti i dati nella sezione di intestazione, si può iterare solo attraverso i nodi outline e stampare gli attributi text e xmlUrl.

from xml.etree import ElementTree

with open('podcasts.opml', 'rt') as f:
    tree = ElementTree.parse(f)

for node in tree.getiterator('outline'):
    name = node.attrib.get('text')
    url = node.attrib.get('xmlUrl')
    if name and url:
        print '  %s :: %s' % (name, url)
    else:
        print name

Visto che viene passato 'outline' a tree.getiterator() l'elaborazione viene limitata ai soli nodi con il tag 'outline'

$ python ElementTree_show_feed_urls.py Science and Tech
  APM: Future Tense :: http://www.publicradio.org/columns/futuretense/podcast.xml
  Engines Of Our Ingenuity Podcast :: http://www.npr.org/rss/podcast.php?id=510030
  Science & the City :: http://www.nyas.org/Podcasts/Atom.axd
Books and Fiction
  Podiobooker :: http://feeds.feedburner.com/podiobooks
  The Drabblecast :: http://web.me.com/normsherman/Site/Podcast/rss.xml
  tor.com / category / tordotstories :: http://www.tor.com/rss/category/TorDotStories
Computers and Programming
  MacBreak Weekly :: http://leo.am/podcasts/mbw
  FLOSS Weekly :: http://leo.am/podcasts/floss
  Core Intuition :: http://www.coreint.org/podcast.xml
Python
  PyCon Podcast :: http://advocacy.python.org/podcasts/pycon.rss
  A Little Bit of Python :: http://advocacy.python.org/podcasts/littlebit.rss
  Django Dose Everything Feed :: http://djangodose.com/everything/feed/
Miscelaneous
  dhellmann's CastSampler Feed :: http://www.castsampler.com/cast/feed/rss/dhellmann/

Trovare Nodi in un Documento

Il metodo di attraversare l'intero albero in questo modo per cercare i nodi che interessano può essre incline ad errori. Nell'esempio di cui sopra, si è dovuto cercare in ogni nodo 'outline' per determinare se si trattava di un gruppo (nodi che hanno solo un attributo "text") o di un podcast (che ha sia "text" che "xmlUrl"). Se si volesse scrivere un programma che esegue il download di podcast ed occorre produrre un semplice elanco di URL dei feed dei podcast, senza nomi o gruppi, si potrebbe semplificare la logica usando findall() per trovare i nodi con caratteristiche di ricerca più descrittiva.

Un primo passo per convertire l'esempio di cui sopra potrebbe essere la costruzione di un parametro XPath per trovare tutti i nodi 'outline'

from xml.etree import ElementTree

with open('podcasts.opml', 'rt') as f:
    tree = ElementTree.parse(f)

for node in tree.findall('.//outline'):
    url = node.attrib.get('xmlUrl')
    if url:
        print url

La logica in questa versione non è sostanzialmente diversa da quella che usa getiterator(). Occorre ancora verificare la presenta dell'URL, ad eccezione del fatto che non viene stampato il nome del gruppo quando l'URL non viene trovato.

$ python ElementTree_find_feeds_by_tag.py
http://www.publicradio.org/columns/futuretense/podcast.xml
http://www.npr.org/rss/podcast.php?id=510030
http://www.nyas.org/Podcasts/Atom.axd
http://feeds.feedburner.com/podiobooks
http://web.me.com/normsherman/Site/Podcast/rss.xml
http://www.tor.com/rss/category/TorDotStories
http://leo.am/podcasts/mbw
http://leo.am/podcasts/floss
http://www.coreint.org/podcast.xml
http://advocacy.python.org/podcasts/pycon.rss
http://advocacy.python.org/podcasts/littlebit.rss
http://djangodose.com/everything/feed/
http://www.castsampler.com/cast/feed/rss/dhellmann/

Un'altra versione potrebbe trarre vantaggio dal fatto che si è a conoscenza che i nodi 'outline' hanno solo due livelli di nidificazione. Se si modifica il percorso di ricerca in .//outline/outline verrà esaminato solo il secondo livello dei nodi 'outline'.

from xml.etree import ElementTree

with open('podcasts.opml', 'rt') as f:
    tree = ElementTree.parse(f)

for node in tree.findall('.//outline/outline'):
    url = node.attrib.get('xmlUrl')
    print url

Ci si aspetta che tutti i nodi 'outline' nidificati due livelli in profondità nell'input abbiano l'attributo xmlURL che si riferisce al feed del podcast, così, se si è sufficientemente coraggiosi si può evitare di verificare la presenza dell'attributo prima dell'uso.

$ python ElementTree_find_feeds_by_structure.py
http://www.publicradio.org/columns/futuretense/podcast.xml
http://www.npr.org/rss/podcast.php?id=510030
http://www.nyas.org/Podcasts/Atom.axd
http://feeds.feedburner.com/podiobooks
http://web.me.com/normsherman/Site/Podcast/rss.xml
http://www.tor.com/rss/category/TorDotStories
http://leo.am/podcasts/mbw
http://leo.am/podcasts/floss
http://www.coreint.org/podcast.xml
http://advocacy.python.org/podcasts/pycon.rss
http://advocacy.python.org/podcasts/littlebit.rss
http://djangodose.com/everything/feed/
http://www.castsampler.com/cast/feed/rss/dhellmann/

Questa versione è limitata alla struttura esistente per questo esempio, quindi se i nodi 'outline' sono in un qualche modo riorganizzati in una struttura ad albero più profonda, lo script non funzionerà più.

Analizzare gli Attributi dei Nodi

Gli elementi restituiti da findall() e getiterator() sono oggetti Element, ognuno dei quali rappresenta un nodo nell'albero XML elaborato. Ogni Element possiede attributi per accedere ai dati estratti dall'XML. La cosa può essere illustrata da un file di input di esempio in qualche modo più stringato: data.xml:

1
2
3
4
5
6
7
 <?xml version="1.0" encoding="UTF-8"?>
 <top>
   <child>Questo figlio contiene testo.</child>
   <child_with_tail>Questo figlio contiene testo normale</child_with_tail>e testo in "coda".
   <with_attributes name="value" foo="bar" />
   <entity_expansion attribute="Questo &#38; Quello">Quello &#38; Questo</entity_expansion>
 </top>

Gli 'attributi' di un nodo sono disponibili tramite la proprietà attrib, che si comporta come un dizionario.

from xml.etree import ElementTree

with open('data.xml', 'rt') as f:
    tree = ElementTree.parse(f)

node = tree.find('./with_attributes')
print node.tag
for name, value in sorted(node.attrib.items()):
    print '  %-4s = "%s"' % (name, value)

Il nodo nella riga 5 del file in input ha due attributi, name e foo.

$ python ElementTree_node_attributes.py
with_attributes
  foo  = "bar"
  name = "value"

Il testo contenuto nei nodi è disponibile, assieme al testo "coda" che segue la fine del tag di chiusura.

from xml.etree import ElementTree

with open('data.xml', 'rt') as f:
    tree = ElementTree.parse(f)

for path in [ './child', './child_with_tail' ]:
    node = tree.find(path)
    print node.tag
    print '  testo del nodo figlio:', node.text
    print '  e testo di coda      :', node.tail

Il nodo child della riga 3 contiene testo incorporato, ed il nodo nella riga 4 contiene testo in coda (inclusi spazi).

$ python ElementTree_node_text.py
child
  testo del nodo figlio: Questo figlio contiene testo.
  e testo di coda      :

child_with_tail
  testo del nodo figlio: Questo figlio contiene testo normale
  e testo di coda      : e testo in "coda".

Convenientemente, i riferimenti alle entità XML incorporati nel documenti sono convertiti nei caratteri appropriati prima che i valori vengano restituiti.

from xml.etree import ElementTree

with open('data.xml', 'rt') as f:
    tree = ElementTree.parse(f)

node = tree.find('entity_expansion')
print node.tag
print "  nell'attributo:", node.attrib['attribute']
print '  nel testo     :', node.text

La conversione risparmia al programmatore la preoccupazione di dovere implementare la rappresentazione di alcuni caratteri in un documento XML.

$ python ElementTree_entity_references.py
entity_expansion
  nell'attributo: Questo & Quello
  nel testo     : Quello & Questo

Osservare gli Eventi durante l'Analisi

L'altra API utile per l'elaborazione dei documenti XML è basata sugli eventi. Il parser genera eventi start per i tag di apertura ed eventi end per quelli di chiusura. Iterare attraverso il flusso di eventi consente di estrarre i dati dal documento mentre lo si analizza, che è conveniente se non si deve manipolare l'intero documento successivamente e si vuole evitare di mantenere l'intero documento analizzato in memoria.

iterparse() restituisce un iterabile che produce delle tuple che contengono il nome dell'evento ed il nodo che ha scatenato l'evento. Gli eventi possono essere uno tra:

start
E' stato rilevato un nuovo tag. E' stata elaborata la parentesi angolare di chiusura del tag ma non il contenuto.
end
La parentesi angolare di chiusura di un tag di chiusura è stata elaborata. Tutti i figli sono già stati processati.
start-ns
Inizia una dichiarazione di spazio dei nomi.
end-ns
Fine di una dichiarazione di spazio dei nomi.
from xml.etree.ElementTree import iterparse

depth = 0
prefix_width = 8
prefix_dots = '.' * prefix_width
line_template = '{prefix:<0.{prefix_len}}{event:<8}{suffix:<{suffix_len}} {node.tag:<12} {node_id}'

for (event, node) in iterparse('podcasts.opml', ['start', 'end', 'start-ns', 'end-ns']):
    if event == 'end':
        depth -= 1

    prefix_len = depth * 2

    print line_template.format(prefix=prefix_dots,
                               prefix_len=prefix_len,
                               suffix='',
                               suffix_len=(prefix_width - prefix_len),
                               node=node,
                               node_id=id(node),
                               event=event,
                               )

    if event == 'start':
        depth += 1

Nella modalità predefinita, vengono generati solo gli eventi end. Per vedere altri eventi, si passa la lista dei nomi di evento che si vuole ricevere ad iterparse(), come in questo esempio.

$ python ElementTree_show_all_events.py
start            opml         3077771180
..start          head         3077788012
....start        title        3077788044
....end          title        3077788044
....start        dateCreated  3077788332
....end          dateCreated  3077788332
....start        dateModified 3077788396
....end          dateModified 3077788396
..end            head         3077788012
..start          body         3077788492
....start        outline      3077788620
......start      outline      3077788844
......end        outline      3077788844
......start      outline      3077788908
......end        outline      3077788908
......start      outline      3077789100
......end        outline      3077789100
....end          outline      3077788620
....start        outline      3077789292
......start      outline      3077789356
......end        outline      3077789356
......start      outline      3077789196
......end        outline      3077789196
......start      outline      3077789484
......end        outline      3077789484
....end          outline      3077789292
....start        outline      3077789676
......start      outline      3077789740
......end        outline      3077789740
......start      outline      3077789580
......end        outline      3077789580
......start      outline      3077789868
......end        outline      3077789868
....end          outline      3077789676
....start        outline      3077790092
......start      outline      3077790188
......end        outline      3077790188
......start      outline      3077790252
......end        outline      3077790252
......start      outline      3077790444
......end        outline      3077790444
....end          outline      3077790092
....start        outline      3077790636
......start      outline      3077790700
......end        outline      3077790700
....end          outline      3077790636
..end            body         3077788492
end              opml         3077771180

Lo stile di elaborazione ad eventi potrebbe essere più naturale per alcune operazioni, tipo convertire un input XML in qualche altro formato. Ad esempio si supponga di volere convertire l'elenco di podcast sul quale si sta lavorando da un file XML ad un file di dati, che si possa caricare in un foglio elettronico o database. Non occorre mantenere tutto l'insieme di dati in memoria, visto che si sta semplicemente cambiando il formato.

import csv
from xml.etree.ElementTree import iterparse
import sys

writer = csv.writer(sys.stdout, quoting=csv.QUOTE_NONNUMERIC)

group_name = ''

for (event, node) in iterparse('podcasts.opml', events=['start']):
    if node.tag != 'outline':
        # Ignora qualsiasi parte al di fuori di outline
        continue
    if not node.attrib.get('xmlUrl'):
        # Ricorda il gruppo corrente
        group_name = node.attrib['text']
    else:
        # Scrive una voce di podcast
        writer.writerow( (group_name, node.attrib['text'],
                          node.attrib['xmlUrl'],
                          node.attrib.get('htmlUrl', ''),
                          )
                         )

Questo programma di esempio converte l'elenco di podcast in un file CSV, pronto per essere importato da un'altra applicazione.

xml.etree.ElementTree - Api per la Manipolazione XML
$ python ElementTree_write_podcast_csv.py
"Science and Tech","APM: Future Tense","http://www.publicradio.org/columns/futuretense/podcast.xml","http://www.publicradio.org/columns/futuretense/"
"Science and Tech","Engines Of Our Ingenuity Podcast","http://www.npr.org/rss/podcast.php?id=510030","http://www.uh.edu/engines/engines.htm"
"Science and Tech","Science & the City","http://www.nyas.org/Podcasts/Atom.axd","http://www.nyas.org/WhatWeDo/SciencetheCity.aspx"
"Books and Fiction","Podiobooker","http://feeds.feedburner.com/podiobooks","http://www.podiobooks.com/blog"
"Books and Fiction","The Drabblecast","http://web.me.com/normsherman/Site/Podcast/rss.xml","http://web.me.com/normsherman/Site/Podcast/Podcast.html"
"Books and Fiction","tor.com / category / tordotstories","http://www.tor.com/rss/category/TorDotStories","http://www.tor.com/"
"Computers and Programming","MacBreak Weekly","http://leo.am/podcasts/mbw","http://twit.tv/mbw"
"Computers and Programming","FLOSS Weekly","http://leo.am/podcasts/floss","http://twit.tv"
"Computers and Programming","Core Intuition","http://www.coreint.org/podcast.xml","http://www.coreint.org/"
"Python","PyCon Podcast","http://advocacy.python.org/podcasts/pycon.rss","http://advocacy.python.org/podcasts/"
"Python","A Little Bit of Python","http://advocacy.python.org/podcasts/littlebit.rss","http://advocacy.python.org/podcasts/"
"Python","Django Dose Everything Feed","http://djangodose.com/everything/feed/",""
"Miscelaneous","dhellmann's CastSampler Feed","http://www.castsampler.com/cast/feed/rss/dhellmann/","http://www.castsampler.com/users/dhellmann/"

Creare il Proprio Costruttore di Albero

Un metodo potenzialmente più efficiente per gestire gli eventi di elaborazione è rimpiazzare il comportamento del costruttore di albero standard con il proprio. Il parser di ElementTree usa uno XMLTreeBuilder per elaborare l'XML e chiamare i metodi su una classe destinazione per salvare i risultati. Il risultato usuale è una istanza di ElementTree creata in modo predefinito dalla classe TreeBuilder. Rimpiazzando TreeBuilder con la propria classe, si possono ricevere gli eventi prima che i nodi Element vengano istanziati, risparmiando quella porzione di sovraccarico.

L'applicazione Da-XML-a-CSV di cui sopra può essere trasposta verso un tree builder.

import csv
from xml.etree.ElementTree import XMLTreeBuilder
import sys

class PodcastListToCSV(object):

    def __init__(self, outputFile):
        self.writer = csv.writer(outputFile, quoting=csv.QUOTE_NONNUMERIC)
        self.group_name = ''
        return

    def start(self, tag, attrib):
        if tag != 'outline':
            # Ignora qualsiasi cosa al di fuori di outline
            return
        if not attrib.get('xmlUrl'):
            # Ricorda il gruppo corrente
            self.group_name = attrib['text']
        else:
            # Scrive una voce di podcast
            self.writer.writerow( (self.group_name, attrib['text'],
                                   attrib['xmlUrl'],
                                   attrib.get('htmlUrl', ''),
                                   )
                                  )

    def end(self, tag):
        # Ignora i tag di chiusura
        pass
    def data(self, data):
        # Ignora i dati all'interno dei nodi
        pass
    def close(self):
        # Nulla di speciale da fare qui
        return


target = PodcastListToCSV(sys.stdout)
parser = XMLTreeBuilder(target=target)
with open('podcasts.opml', 'rt') as f:
    for line in f:
        parser.feed(line)
parser.close()

PodcastListToCSV implementa il protocollo TreeBuilder. Ogni volta che si incontra un nuovo tag XML, viene chiamato start() con il nome del tag e gli attributi. Quando viene visto un tag di chiusura viene chiamato end() con il nome. In mezzo viene chiamato data() quando un nodo ha un contenuto (il costruttore dell'albero tiene traccia del nodo 'corrente'). Quando tutto l'input è stato processato viene chiamato close(). E' possibile la restituzione di un valore, il quale può essere ritornato all'utilizzatore di XMLTreeBuilder.

$ python ElementTree_podcast_csv_treebuilder.py
"Science and Tech","APM: Future Tense","http://www.publicradio.org/columns/futuretense/podcast.xml","http://www.publicradio.org/columns/futuretense/"
"Science and Tech","Engines Of Our Ingenuity Podcast","http://www.npr.org/rss/podcast.php?id=510030","http://www.uh.edu/engines/engines.htm"
"Science and Tech","Science & the City","http://www.nyas.org/Podcasts/Atom.axd","http://www.nyas.org/WhatWeDo/SciencetheCity.aspx"
"Books and Fiction","Podiobooker","http://feeds.feedburner.com/podiobooks","http://www.podiobooks.com/blog"
"Books and Fiction","The Drabblecast","http://web.me.com/normsherman/Site/Podcast/rss.xml","http://web.me.com/normsherman/Site/Podcast/Podcast.html"
"Books and Fiction","tor.com / category / tordotstories","http://www.tor.com/rss/category/TorDotStories","http://www.tor.com/"
"Computers and Programming","MacBreak Weekly","http://leo.am/podcasts/mbw","http://twit.tv/mbw"
"Computers and Programming","FLOSS Weekly","http://leo.am/podcasts/floss","http://twit.tv"
"Computers and Programming","Core Intuition","http://www.coreint.org/podcast.xml","http://www.coreint.org/"
"Python","PyCon Podcast","http://advocacy.python.org/podcasts/pycon.rss","http://advocacy.python.org/podcasts/"
"Python","A Little Bit of Python","http://advocacy.python.org/podcasts/littlebit.rss","http://advocacy.python.org/podcasts/"
"Python","Django Dose Everything Feed","http://djangodose.com/everything/feed/",""
"Miscelaneous","dhellmann's CastSampler Feed","http://www.castsampler.com/cast/feed/rss/dhellmann/","http://www.castsampler.com/users/dhellmann/"

Analizzare Stringhe

Per lavorare con porzioni più piccole di testo XML, specialmente stringhe letterali come potrebbero essere incorporate nel sorgente di un programma, si usa xml.etree.ElementTree.XML e si passa un singolo parametro, la stringa che contiene l'XML che deve essere analizzato.

from xml.etree.ElementTree import XML

parsed = XML('''
.<root>
  <group>
    <child id="a">Questo è il figlio "a".</child>
    <child id="b">Questo è il figlio "b".</child>
  </group>
  <group>
    <child id="c">Questo è il figlio "c".</child>
  </group>
.</root>
''')

print 'parsed =', parsed

for elem in parsed.getiterator():
    print elem.tagxml.etree.ElementTree - Api per la Manipolazione XML
    if elem.text is not None and elem.text.strip():
        print '  text: "%s"' % elem.text
    if elem.tail is not None and elem.tail.strip():
        print '  tail: "%s"' % elem.tail
    for name, value in sorted(elem.attrib.items()):
        print '  %-4s = "%s"' % (name, value)
    print

Si noti che al contrario di parse(), il valore restituito è una istanza di Element invece che di ElementTree.

$ python ElementTree_XML.py
parsed = 
root

group

child
  text: "Questo è il figlio "a"."
  id   = "a"

child
  text: "Questo è il figlio "b"."
  id   = "b"

group

child
  text: "Questo è il figlio "c"."
  id   = "c"

In caso di XML strutturato che usi l'attributo "id" per identificare in modo univoco i nodi di interesse, XMLID() rappresenta un modo conveniente di accedere ai risultati analizzati.

from xml.etree.ElementTree import XMLID
tree, id_map = XMLID('''
<root>
  <group>
    <child id="a">Questo è il figlio "a".</child>
    <child id="b">Questo è il figlio "b".</child>
  </group>
  <group>
    <child id="c">Questo è il figlio "c".</child>
  </group>
</root>
''')

for key, value in sorted(id_map.items()):
    print '%s = %s' % (key, value)

XMLID( restituisce l'albero analizzato come un oggetto Element, assieme ad un dizionario che mapppa le stringhe dell'attributo id ai singoli nodi nell'albero

$ python ElementTree_XMLID.py
a = 
b = 
c = 

Oltre alla capacità di analisi, ElementTree supporta anche la creazione di documenti XML well-formed da oggetti Element costruiti nella propria applicazione. La classe Element usata quando viene analizzato un documento è anche in grado di generare una forma serializzata del suo contenuto, che poi può essere scritta in un file od altro flusso di dati.

Costruire i Nodi Element

Ci sono tre funzioni utili alla creazione di una gerarchia di nodi Element. Element() crea un nodo standard, SubElement() attacca il nuovo nodo al nodo genitore, e Comment() crea un nodo che serializza usando la sintassi per i commenti XML.

from xml.etree.ElementTree import Element, SubElement, Comment, tostring

top = Element('top')

comment = Comment('Generato per PyMOTW')
top.append(comment)

child = SubElement(top, 'figlio')
child.text = 'Questo figlio contiene testo.'

child_with_tail = SubElement(top, 'figlio_con_coda')
child_with_tail.text = 'Questo figlio contiene testo normale.'
child_with_tail.tail = 'E testo "in coda".'

child_with_entity_ref = SubElement(top, 'figlio_con_rif_entita')
child_with_entity_ref.text = 'Questo & Quello'

print tostring(top)

L'output contiene solo i nodi XML nell'albero, non la dichiarazione XML con la versione e la codifica.

$ python ElementTree_create.py Questo figlio contiene testo.Questo figlio contiene testo normale.E testo "in coda".Questo & Quello

Si noti che il carattere & nel testo di figlio_con_rif_entita viene convertito nella corrispondente entità &amp; automaticamente.

XML Pretty-Printing

Non viene compiuto alcuno sforzo da ElementTree per stampare l'output prodotto da tostring() in modo facilmente leggibile, visto che l'aggiunta di spazi modificherebbe il contenuto del documento. Per rendere il risultato pià facile da seguire all'occhio umano, gli esempi seguenti faranno uso in un suggerimento trovato in Internet e rielaboreranno il testo XML con xml.dom.minidom, per usare il suo metodo toprettyxml().

from xml.etree import ElementTree
from xml.dom import minidom

def prettify(elem):
    """Ritorna una string XML pretty-printed per Element.
    """
    rough_string = ElementTree.tostring(elem, 'utf-8')
    reparsed = minidom.parseString(rough_string)
    return reparsed.toprettyxml(indent="  ")

L'esempio aggiornato è diventato:

from xml.etree.ElementTree import Element, SubElement, Comment, tostring
from ElementTree_pretty import prettify

top = Element('top')

comment = Comment('Generato per PyMOTW')
top.append(comment)

child = SubElement(top, 'figlio')
child.text = 'Questo figlio contiene testo.'

child_with_tail = SubElement(top, 'figlio_con_coda')
child_with_tail.text = 'Questo figlio contiene testo normale.'
child_with_tail.tail = 'E testo "in coda".'

child_with_entity_ref = SubElement(top, 'figlio_con_rif_entita')
child_with_entity_ref.text = 'Questo & Quello'

print prettify(top)

ed il risultato è più facile da leggere:

$ python ElementTree_create.py
 <?xml version="1.0" ?>
  <top>
   <!-- Generato per PyMOTW -->
   <figlio>
    Questo figlio contiene testo.
   </figlio>
   <figlio_con_coda>
    Questo figlio contiene testo normale.
   </figlio_con_coda>
   E testo "in coda".
   <figlio_con_rif_entita>
     Questo & Quello
   </figlio_con_rif_entita>
  </top>

Oltre allo spazio supplementare per la formattazione, il pretty-printer di xml.dom.minidom aggiunge anche una dichiarazione XML nell'output.

Impostare le Proprietà di Element

L'esempio precedente creava dei nodi con tag e contenuto testo, ma non impostava alcun attributo dei nodi. Molti degli esempi della parte relativa all'elaborazione dei documenti XML lavoravano con un file OPML che elencava dei podcast ed i loro feed. I nodi outline nell'albero usavano attributi per i nomi del gruppo e le proprietà dei podcast. Si può usare ElementTree_create per costruire un file XML simile usando un file CSV come input, impostando tutti gli attributi degli elementi mano a mano che l'albero viene costruito

import csv
from xml.etree.ElementTree import Element, SubElement, Comment, tostring
import datetime
from ElementTree_pretty import prettify

generated_on = str(datetime.datetime.now())

# Configura un attributo con set()
root = Element('opml')
root.set('version', '1.0')

root.append(Comment('Generato da ElementTree_csv_to_xml.py per PyMOTW-it'))

head = SubElement(root, 'head')
title = SubElement(head, 'title')
title.text = 'My Podcasts'
dc = SubElement(head, 'dateCreated')
dc.text = generated_on
dm = SubElement(head, 'dateModified')
dm.text = generated_on

body = SubElement(root, 'body')

with open('podcasts.csv', 'rt') as f:
    current_group = None
    reader = csv.reader(f)
    for row in reader:
        group_name, podcast_name, xml_url, html_url = row
        if not current_group or group_name != current_group.text:
            # Inizia un nuovo gruppo
            current_group = SubElement(body, 'outline', {'text':group_name})
        # Aggiunge questo podcast al gruppo
        # impostandone gli attibuti tutti in
        # una volta.
        podcast = SubElement(current_group, 'outline',
                             {'text':podcast_name,
                              'xmlUrl':xml_url,
                              'htmlUrl':html_url,
                              })

print prettify(root)

I valori degli attributi possono essere configurati uno alla volta con set() (come nel nodo root), o tutti in una volta passando un dizionario al costruttore di nodi (come con ogni nodo group e podcast).

$ python ElementTree_csv_to_xml.py
 <?xml version="1.0" ?>
 <opml version="1.0">
   <!-- Generato da ElementTree_csv_to_xml.py per PyMOTW-it -->
   <head>
     <title>
      My Podcasts
     </title>
     <dateCreated>
      2010-05-01 09:38:11.182745
     </dateCreated>
     <dateModified>
      2010-05-01 09:38:11.182745
     </dateModified>
   </head>
   <body>
     <outline text="Science and Tech">
       <outline htmlUrl="http://www.publicradio.org/columns/futuretense/" text="APM: Future Tense" xmlUrl="http://www.publicradio.org/columns/futuretense/podcast.xml"/>
     </outline>
     <outline text="Science and Tech">
       <outline htmlUrl="http://www.uh.edu/engines/engines.htm" text="Engines Of Our Ingenuity Podcast" xmlUrl="http://www.npr.org/rss/podcast.php?id=510030"/>
     </outline>
     <outline text="Science and Tech">
       <outline htmlUrl="http://www.nyas.org/WhatWeDo/SciencetheCity.aspx" text="Science & the City" xmlUrl="http://www.nyas.org/Podcasts/Atom.axd"/>
     </outline>
     <outline text="Books and Fiction">
       <outline htmlUrl="http://www.podiobooks.com/blog" text="Podiobooker" xmlUrl="http://feeds.feedburner.com/podiobooks"/>
     </outline>
     <outline text="Books and Fiction">
       <outline htmlUrl="http://web.me.com/normsherman/Site/Podcast/Podcast.html" text="The Drabblecast" xmlUrl="http://web.me.com/normsherman/Site/Podcast/rss.xml"/>
     </outline>
     <outline text="Books and Fiction">
       <outline htmlUrl="http://www.tor.com/" text="tor.com / category / tordotstories" xmlUrl="http://www.tor.com/rss/category/TorDotStories"/>
     </outline>
     <outline text="Computers and Programming">
       <outline htmlUrl="http://twit.tv/mbw" text="MacBreak Weekly" xmlUrl="http://leo.am/podcasts/mbw"/>
     </outline>
     <outline text="Computers and Programming">
       <outline htmlUrl="http://twit.tv" text="FLOSS Weekly" xmlUrl="http://leo.am/podcasts/floss"/>
     </outline>
     <outline text="Computers and Programming">
       <outline htmlUrl="http://www.coreint.org/" text="Core Intuition" xmlUrl="http://www.coreint.org/podcast.xml"/>
     </outline>
     <outline text="Python">
       <outline htmlUrl="http://advocacy.python.org/podcasts/" text="PyCon Podcast" xmlUrl="http://advocacy.python.org/podcasts/pycon.rss"/>
     </outline>
     <outline text="Python">
       <outline htmlUrl="http://advocacy.python.org/podcasts/" text="A Little Bit of Python" xmlUrl="http://advocacy.python.org/podcasts/littlebit.rss"/>
     </outline>
     <outline text="Python">
       <outline htmlUrl="" text="Django Dose Everything Feed" xmlUrl="http://djangodose.com/everything/feed/"/>
     </outline>
     <outline text="Miscelaneous">
       <outline htmlUrl="http://www.castsampler.com/users/dhellmann/" text="dhellmann's CastSampler Feed" xmlUrl="http://www.castsampler.com/cast/feed/rss/dhellmann/"/>
     </outline>
   </body>
 </opml>

Serializzare XML ad un Flusso

tostring() in realtà scrive ad un oggetto in memoria tipo file, quindi restituisce la stringa che rappresenta l'intero albero degli elementi. Quando si lavora con un grande numero di dati, occorrerà meno memoria e si avrà un uso più efficiente delle librerie di I/O scrivendo direttamente verso un handle di file usando il metodo write() di ElementTree.

import sys
from xml.etree.ElementTree import Element, SubElement, Comment, ElementTree

top = Element('top')

comment = Comment('Generato per PyMOTW')
top.append(comment)

child = SubElement(top, 'figlio')
child.text = 'Questo figlio contiene testo.'

child_with_tail = SubElement(top, 'figlio_con_coda')
child_with_tail.text = 'Questo figlio contiene testo normale.'
child_with_tail.tail = 'E testo "in coda".'

child_with_entity_ref = SubElement(top, 'figlio_con_rif_entita')
child_with_entity_ref.text = 'Questo & Quello'

ElementTree(top).write(sys.stdout)

L'esempio usa sys.stdout per scrivere verso la console, ma potrebbe anche scrivere verso un file aperto od un socket.

$ python ElementTree_write.py
<top><figlio>Questo figlio contiene testo.</figlio><figlio_con_coda>Questo figlio contiene testo normale.</figlio_con_coda>E testo "in coda".<figlio_con_rif_entita>Questo & Quello</figlio_con_rif_entita></top>

Vedere anche:

Outline Processor Markup Language (OPML)
Le specifiche dell'OPML di Dave Winer e la documentazione.
Supporto XPath in ElementTree
Parte della documentazione originale per ElementTree di Fredrick Lundh
csv
Leggere e scrivere file con valori separati da virgola