Analizar un documento XML¶
Los documentos XML analizados se representan en la memoria mediante objetos
ElementTree
Element
conectados en una estructura de árbol basada en la
forma en que los nodos están anidados en el documento XML.
Analizar un documento completo con parse()
devuelve una instancia
ElementTree
. El árbol conoce todos los datos en el documento de entrada,
y los nodos del árbol se pueden buscar o manipulados en su lugar. Si bien esta
flexibilidad puede hacer que trabajar con el documento analizado sea más
conveniente, normalmente requiere más memoria que un enfoque de análisis
basado en eventos ya que todo el documento debe ser cargado a la vez.
La huella de memoria de documentos pequeños y simples como esta lista de podcasts representada como un esquema OPML no son significativa:
<?xml version="1.0" encoding="UTF-8"?>
<opml version="1.0">
<head>
<title>My Podcasts</title>
<dateCreated>Sat, 06 Aug 2016 15:53:26 GMT</dateCreated>
<dateModified>Sat, 06 Aug 2016 15:53:26 GMT</dateModified>
</head>
<body>
<outline text="Non-tech">
<outline
text="99% Invisible" type="rss"
xmlUrl="http://feeds.99percentinvisible.org/99percentinvisible"
htmlUrl="http://99percentinvisible.org" />
</outline>
<outline text="Python">
<outline
text="Talk Python to Me" type="rss"
xmlUrl="https://talkpython.fm/episodes/rss"
htmlUrl="https://talkpython.fm" />
<outline
text="Podcast.__init__" type="rss"
xmlUrl="http://podcastinit.podbean.com/feed/"
htmlUrl="http://podcastinit.com" />
</outline>
</body>
</opml>
Para analizar el archivo, pasa un identificador de archivo abierto a parse()
.
from xml.etree import ElementTree
with open('podcasts.opml', 'rt') as f:
tree = ElementTree.parse(f)
print(tree)
Leerá los datos, analizará el XML y devolverá un objeto ElementTree
.
$ python3 ElementTree_parse_opml.py
<xml.etree.ElementTree.ElementTree object at 0x1013e5630>
Atravesar el árbol analizado¶
Para visitar a todos los niños en orden, usa iter()
para crear un generador
que itera sobre la instancia ElementTree
.
from xml.etree import ElementTree
import pprint
with open('podcasts.opml', 'rt') as f:
tree = ElementTree.parse(f)
for node in tree.iter():
print(node.tag)
Este ejemplo imprime todo el árbol, una etiqueta a la vez.
$ python3 ElementTree_dump_opml.py
opml
head
title
dateCreated
dateModified
body
outline
outline
outline
outline
outline
Para imprimir solo los grupos de nombres y URL de fuentes para los podcasts,
dejando fuera de todos los datos en la sección de encabezado iterando sobre
solo los nodos outline
e imprimir los atributos text
y xmlUrl
buscando los valores en el diccionario attrib
.
from xml.etree import ElementTree
with open('podcasts.opml', 'rt') as f:
tree = ElementTree.parse(f)
for node in tree.iter('outline'):
name = node.attrib.get('text')
url = node.attrib.get('xmlUrl')
if name and url:
print(' %s' % name)
print(' %s' % url)
else:
print(name)
El argumento 'outline'
para iter()
significa que el procesamiento esa
limitado solo a nodos con la etiqueta 'outline'
.
$ python3 ElementTree_show_feed_urls.py
Non-tech
99% Invisible
http://feeds.99percentinvisible.org/99percentinvisible
Python
Talk Python to Me
https://talkpython.fm/episodes/rss
Podcast.__init__
http://podcastinit.podbean.com/feed/
Encontrar nodos en un documento¶
Caminar por todo el árbol de esta manera, buscando nodos relevantes, puede ser
propenso a errores. El ejemplo anterior tenía que mirar cada nodo outline
para determinar si era un grupo (solo nodos con un atributo text
) o
podcast (con ambos text
y xmlUrl
). Para producir una lista
simple de las URL de los canales de información de podcast, sin nombres o
grupos, la lógica podría simplificarse utilizando findall()
para buscar
nodos con características de búsqueda más descriptivos.
Como primer paso para convertir la primera versión, se puede utilizar un argumento XPath para buscar todos los nodos 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 lógica en esta versión no es sustancialmente diferente a la versión
utilizando getiterator()
. Todavía tiene que comprobar la presencia de la
URL, excepto que no imprime el nombre del grupo cuando no se encuentra la URL.
$ python3 ElementTree_find_feeds_by_tag.py
http://feeds.99percentinvisible.org/99percentinvisible
https://talkpython.fm/episodes/rss
http://podcastinit.podbean.com/feed/
Es posible aprovechar el hecho de que los nodos outline solo se anidan dos
niveles de profundidad. Cambiando la ruta de búsqueda a .//outline/outline
significa que el bucle procesará solo el segundo nivel de nodos 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)
Se espera que todos los nodos outline anidados dos niveles de profundidad en la
entrada tengan el atributo xmlURL
que se refiere a la fuente de podcast,
por lo que el bucle puede omitir la comprobación del atributo antes de usarlo.
$ python3 ElementTree_find_feeds_by_structure.py
http://feeds.99percentinvisible.org/99percentinvisible
https://talkpython.fm/episodes/rss
http://podcastinit.podbean.com/feed/
Sin embargo, esta versión está limitada a la estructura existente, por lo que si los nodos outline se reorganizan en un árbol más profundo, dejará de trabajar.
Atributos del nodo analizado¶
Los elementos devueltos por findall()
y iter()
son objetos Element
,
cada uno representa un nodo en el árbol de análisis XML. Cada Element
tiene atributos para acceder a los datos extraídos del XML. Esto se puede
ilustrar con un archivo de ejemplo de entrada un poco más ideado data.xml
.
1 2 3 4 5 6 7 8 9 | <?xml version="1.0" encoding="UTF-8"?>
<top>
<child>Regular text.</child>
<child_with_tail>Regular text.</child_with_tail>"Tail" text.
<with_attributes name="value" foo="bar" />
<entity_expansion attribute="This & That">
That & This
</entity_expansion>
</top>
|
Los atributos XML de un nodo están disponibles en la propiedad attrib
que actúa como un diccionario.
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))
El nodo en la línea cinco del archivo de entrada tiene dos atributos,
name
y foo
.
$ python3 ElementTree_node_attributes.py
with_attributes
foo = "bar"
name = "value"
El contenido de texto de los nodos está disponible, junto con el texto tail, que viene después del final de una etiqueta de cierre.
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(' child node text:', node.text)
print(' and tail text :', node.tail)
El nodo child
en la línea tres contiene texto incrustado, y el nodo en la
línea cuatro tiene texto con una cola (incluyendo espacios en blanco).
$ python3 ElementTree_node_text.py
child
child node text: Regular text.
and tail text :
child_with_tail
child node text: Regular text.
and tail text : "Tail" text.
Las referencias de entidad XML incrustadas en el documento se convierten a caracteres apropiados antes de que se devuelvan los valores.
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(' in attribute:', node.attrib['attribute'])
print(' in text :', node.text.strip())
La conversión automática significa que el detalle de implementación de la representación de ciertos caracteres en un documento XML puede ser ignorada.
$ python3 ElementTree_entity_references.py
entity_expansion
in attribute: This & That
in text : That & This
Vigilar eventos mientras analizamos¶
La otra interfaz de programación para procesar documentos XML está basada en
eventos. El analizador genera eventos start
para abrir etiquetas y eventos
end
para cerrar etiquetas. Los datos pueden ser extraídos del documento
durante la fase de análisis mediante la iteración de la secuencia de eventos,
lo que es conveniente si no es necesario manipular todo el documento
posteriormente y no es necesario mantener todo el documento analizado en la
memoria.
Los eventos pueden ser uno de los siguientes:
start
- Se ha encontrado una nueva etiqueta. El corchete angular de cierre de la etiqueta fue procesado, pero no el contenido.
end
- Se ha procesado el corchete de angular de cierre de una etiqueta de cierre. Todos los niños ya fueron procesados.
start-ns
- Inicia una declaración de espacio de nombres.
end-ns
- Finaliza una declaración de espacio de nombres.
iterparse()
devuelve un iterable que produce tuplas que contienen el nombre
del evento y el nodo que desencadena el evento.
from xml.etree.ElementTree import iterparse
depth = 0
prefix_width = 8
prefix_dots = '.' * prefix_width
line_template = ''.join([
'{prefix:<0.{prefix_len}}',
'{event:<8}',
'{suffix:<{suffix_len}} ',
'{node.tag:<12} ',
'{node_id}',
])
EVENT_NAMES = ['start', 'end', 'start-ns', 'end-ns']
for (event, node) in iterparse('podcasts.opml', EVENT_NAMES):
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
Por defecto, solo se generan eventos end
. Para ver otros eventos, pasa la
lista de nombres de eventos deseados a iterparse()
, como en este ejemplo.
$ python3 ElementTree_show_all_events.py
start opml 4312612200
..start head 4316174520
....start title 4316254440
....end title 4316254440
....start dateCreated 4316254520
....end dateCreated 4316254520
....start dateModified 4316254680
....end dateModified 4316254680
..end head 4316174520
..start body 4316254840
....start outline 4316254920
......start outline 4316255080
......end outline 4316255080
....end outline 4316254920
....start outline 4316255160
......start outline 4316255240
......end outline 4316255240
......start outline 4316255320
......end outline 4316255320
....end outline 4316255160
..end body 4316254840
end opml 4312612200
El estilo de procesamiento del evento es más natural para algunas operaciones, como la conversión de entrada XML a algún otro formato. Esta técnica puede ser utilizada para convertir la lista de podcasts de los ejemplos anteriores de un archivo XML a un archivo CSV, para que puedan cargarse en una hoja de cálculo o una aplicación de base de datos.
import csv
from xml.etree.ElementTree import iterparse
import sys
writer = csv.writer(sys.stdout, quoting=csv.QUOTE_NONNUMERIC)
group_name = ''
parsing = iterparse('podcasts.opml', events=['start'])
for (event, node) in parsing:
if node.tag != 'outline':
# Ignore anything not part of the outline
continue
if not node.attrib.get('xmlUrl'):
# Remember the current group
group_name = node.attrib['text']
else:
# Output a podcast entry
writer.writerow(
(group_name, node.attrib['text'],
node.attrib['xmlUrl'],
node.attrib.get('htmlUrl', ''))
)
Este programa de conversión no necesita mantener toda el archivo de entrada analizado en la memoria, y procesa cada nodo a medida que se encuentra en la entrada es más eficiente.
$ python3 ElementTree_write_podcast_csv.py
"Non-tech","99% Invisible","http://feeds.99percentinvisible.org/\
99percentinvisible","http://99percentinvisible.org"
"Python","Talk Python to Me","https://talkpython.fm/episodes/rss\
","https://talkpython.fm"
"Python","Podcast.__init__","http://podcastinit.podbean.com/feed\
/","http://podcastinit.com"
Nota
La salida de ElementTree_write_podcast_csv.py
ha sido reformateada para
caber en esta página. Las líneas de salida que terminan con \
indican un
salto de línea artificial.
Crear un generador de árbol personalizado¶
Un medio potencialmente más eficiente para manejar eventos de análisis es
reemplazar el comportamiento del generador de árbol estándar con una versión
personalizada. El analizador XMLParser
utiliza un TreeBuilder
para
procesar el XML y llama métodos en una clase de destino para guardar los
resultados. La salida habitual es una instancia ElementTree
creada por
clase TreeBuilder
por defecto. Sustituyendo TreeBuilder
con otra clase
le permite recibir los eventos antes que los nodos Element
sea creados,
evitando esa parte de la sobrecarga.
El convertidor de XML a CSV de la sección anterior puede ser re-implementado como un constructor de árboles.
import csv
import io
from xml.etree.ElementTree import XMLParser
import sys
class PodcastListToCSV(object):
def __init__(self, outputFile):
self.writer = csv.writer(
outputFile,
quoting=csv.QUOTE_NONNUMERIC,
)
self.group_name = ''
def start(self, tag, attrib):
if tag != 'outline':
# Ignore anything not part of the outline
return
if not attrib.get('xmlUrl'):
# Remember the current group
self.group_name = attrib['text']
else:
# Output a podcast entry
self.writer.writerow(
(self.group_name,
attrib['text'],
attrib['xmlUrl'],
attrib.get('htmlUrl', ''))
)
def end(self, tag):
"Ignore closing tags"
def data(self, data):
"Ignore data inside nodes"
def close(self):
"Nothing special to do here"
target = PodcastListToCSV(sys.stdout)
parser = XMLParser(target=target)
with open('podcasts.opml', 'rt') as f:
for line in f:
parser.feed(line)
parser.close()
PodcastListToCSV
implementa el protocolo TreeBuilder
. Cada vez que se
encuentra una nueva etiqueta XML, start()
es llamado con el nombre y los
atributos de la etiqueta. Cuando se ve una etiqueta de cierre, end()
se
llama con el nombre. En medio, se llama data()
cuando un nodo tiene
contenido (se espera que el generador de árboles mantenga actual el nodo
«current»). Cuando toda la entrada se procesa, se llama close()
. Puede
devolver un valor, que será devuelto al usuario del TreeBuilder
.
$ python3 ElementTree_podcast_csv_treebuilder.py
"Non-tech","99% Invisible","http://feeds.99percentinvisible.org/\
99percentinvisible","http://99percentinvisible.org"
"Python","Talk Python to Me","https://talkpython.fm/episodes/rss\
","https://talkpython.fm"
"Python","Podcast.__init__","http://podcastinit.podbean.com/feed\
/","http://podcastinit.com"
Nota
La salida de ElementTree_podcast_csv_treebuidler.py
ha sido reformateada
para caber en esta página. Las líneas de salida que terminan con \
indican un salto de línea artificial.
Analizar cadenas¶
Para trabajar con pedazos más pequeños de texto XML, especialmente literales de
cadena que podrían estar incrustados en la fuente de un programa, usa XML()
y la cadena que contiene el XML que se analizará como el único argumento.
from xml.etree.ElementTree import XML
def show_node(node):
print(node.tag)
if node.text is not None and node.text.strip():
print(' text: "%s"' % node.text)
if node.tail is not None and node.tail.strip():
print(' tail: "%s"' % node.tail)
for name, value in sorted(node.attrib.items()):
print(' %-4s = "%s"' % (name, value))
for child in node:
show_node(child)
parsed = XML('''
<root>
<group>
<child id="a">This is child "a".</child>
<child id="b">This is child "b".</child>
</group>
<group>
<child id="c">This is child "c".</child>
</group>
</root>
''')
print('parsed =', parsed)
for elem in parsed:
show_node(elem)
A diferencia de parse()
, el valor de retorno es una instancia Element
en lugar de un ElementTree
. Un Element
soporta el protocolo del
iterador directamente, entonces no hay necesidad de llamar a getiterator()
.
$ python3 ElementTree_XML.py
parsed = <Element 'root' at 0x10079eef8>
group
child
text: "This is child "a"."
id = "a"
child
text: "This is child "b"."
id = "b"
group
child
text: "This is child "c"."
id = "c"
Para XML estructurado que usa el atributo id
para identificar nodos de
interés únicos, XMLID()
es una forma conveniente de acceder a los
resultados del análisis.
from xml.etree.ElementTree import XMLID
tree, id_map = XMLID('''
<root>
<group>
<child id="a">This is child "a".</child>
<child id="b">This is child "b".</child>
</group>
<group>
<child id="c">This is child "c".</child>
</group>
</root>
''')
for key, value in sorted(id_map.items()):
print('%s = %s' % (key, value))
XMLID()
devuelve el árbol analizado como un objeto Element
, junto con
un diccionario que asigna las cadenas de atributo :attr: id a los nodos
individuales en el árbol.
$ python3 ElementTree_XMLID.py
a = <Element 'child' at 0x10133aea8>
b = <Element 'child' at 0x10133aef8>
c = <Element 'child' at 0x10133af98>