Construir Documentos Con Nodos Element

Además de sus capacidades de análisis, xml.etree.ElementTree también admite la creación de documentos XML bien formados desde objetos Element construidos en una aplicación. La clase Element utilizada cuando se analiza un documento también sabe cómo generar una forma serializada de su contenido, que luego se puede escribir en un archivo u otro flujo de datos.

Hay tres funciones auxiliares útiles para crear una jerarquía de nodos Element. Element() crea un nodo estándar, SubElement() adjunta un nuevo nodo a un padre, y Comment() crea un nodo que se serializa usando la sintaxis de comentario de XML.

ElementTree_create.py
from xml.etree.ElementTree import (
    Element, SubElement, Comment, tostring,
)

top = Element('top')

comment = Comment('Generated for PyMOTW')
top.append(comment)

child = SubElement(top, 'child')
child.text = 'This child contains text.'

child_with_tail = SubElement(top, 'child_with_tail')
child_with_tail.text = 'This child has text.'
child_with_tail.tail = 'And "tail" text.'

child_with_entity_ref = SubElement(top, 'child_with_entity_ref')
child_with_entity_ref.text = 'This & that'

print(tostring(top))

La salida contiene solo los nodos XML en el árbol, no la declaración XML con versión y codificación.

$ python3 ElementTree_create.py

b'<top><!--Generated for PyMOTW--><child>This child contains text.</
child><child_with_tail>This child has text.</child_with_tail>And "ta
il" text.<child_with_entity_ref>This &amp; that</child_with_entity_r
ef></top>'

El carácter & en el texto de child_with_entity_ref es convertido a la referencia de entidad &amp; automáticamente.

XML de impresión bonita

ElementTree no hace ningún esfuerzo para formatear la salida de tostring() para que sea fácil de leer porque agregar espacios extra en blanco cambia los contenidos del documento. Para hacer la salida más fácil de seguir, el resto de los ejemplos utilizarán. xml.dom.minidom para volver a analizar el XML y luego usar su toprettyxml() método.

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


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

El ejemplo actualizado ahora es

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

top = Element('top')

comment = Comment('Generated for PyMOTW')
top.append(comment)

child = SubElement(top, 'child')
child.text = 'This child contains text.'

child_with_tail = SubElement(top, 'child_with_tail')
child_with_tail.text = 'This child has text.'
child_with_tail.tail = 'And "tail" text.'

child_with_entity_ref = SubElement(top, 'child_with_entity_ref')
child_with_entity_ref.text = 'This & that'

print(prettify(top))

y la salida es más fácil de leer.

$ python3 ElementTree_create_pretty.py

<?xml version="1.0" ?>
<top>
  <!--Generated for PyMOTW-->
  <child>This child contains text.</child>
  <child_with_tail>This child has text.</child_with_tail>
  And &quot;tail&quot; text.
  <child_with_entity_ref>This &amp; that</child_with_entity_ref>
</top>

Además del espacio en blanco adicional para el formateo, la impresora bonita :mod: xml.dom.minidom también agrega una declaración XML a la salida.

Configurar de las propiedades del Element

El ejemplo anterior creó nodos con etiquetas y contenido de texto, pero no estableció ningún atributo de los nodos. Muchos de los ejemplos de Analizar un documento XML trabajaban con un listado OPML de podcasts y sus feeds. Los nodos outline en el arbol utilizaban atributos para los nombres de grupo y propiedades del podcast. ElementTree puede usarse para construir un archivo XML similar desde un archivo de entrada CSV, estableciendo todos los atributos del elemento mientras el árbol es construido.

ElementTree_csv_to_xml.py
import csv
from xml.etree.ElementTree import (
    Element, SubElement, Comment, tostring,
)
import datetime
from ElementTree_pretty import prettify

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

# Configure one attribute with set()
root = Element('opml')
root.set('version', '1.0')

root.append(
    Comment('Generated by ElementTree_csv_to_xml.py for PyMOTW')
)

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 (current_group is None or
                group_name != current_group.text):
            # Start a new group
            current_group = SubElement(
                body, 'outline',
                {'text': group_name},
            )
        # Add this podcast to the group,
        # setting all its attributes at
        # once.
        podcast = SubElement(
            current_group, 'outline',
            {'text': podcast_name,
             'xmlUrl': xml_url,
             'htmlUrl': html_url},
        )

print(prettify(root))

Este ejemplo utiliza dos técnicas para establecer los valores de atributo de los nuevos nodos. El nodo raíz se configura usando set() para cambiar un atributo a la vez. Los nodos de podcast reciben todos sus atributos a la vez pasando un diccionario a la fábrica de nodos.

$ python3 ElementTree_csv_to_xml.py

<?xml version="1.0" ?>
<opml version="1.0">
  <!--Generated by ElementTree_csv_to_xml.py for PyMOTW-->
  <head>
    <title>My Podcasts</title>
    <dateCreated>2016-08-06 17:09:00.524979</dateCreated>
    <dateModified>2016-08-06 17:09:00.524979</dateModified>
  </head>
  <body>
    <outline text="Non-tech">
      <outline htmlUrl="http://99percentinvisible.org" text="99%\
 Invisible" xmlUrl="http://feeds.99percentinvisible.org/99percen\
tinvisible"/>
    </outline>
    <outline text="Python">
      <outline htmlUrl="https://talkpython.fm" text="Talk Python\
 to Me" xmlUrl="https://talkpython.fm/episodes/rss"/>
    </outline>
    <outline text="Python">
      <outline htmlUrl="http://podcastinit.com" text="Podcast.__\
init__" xmlUrl="http://podcastinit.podbean.com/feed/"/>
    </outline>
  </body>
</opml>

Construir árboles a partir de listas de nodos

Se pueden agregar varios hijos a una instancia de Element junto con el método extend(). El argumento para extend() es cualquier iterable, incluyendo un list u otra instancia Element.

ElementTree_extend.py
from xml.etree.ElementTree import Element, tostring
from ElementTree_pretty import prettify

top = Element('top')

children = [
    Element('child', num=str(i))
    for i in range(3)
]

top.extend(children)

print(prettify(top))

Cuando se da un list, los nodos en la lista se agregan directamente al nuevo padre.

$ python3 ElementTree_extend.py

<?xml version="1.0" ?>
<top>
  <child num="0"/>
  <child num="1"/>
  <child num="2"/>
</top>

Cuando se da otra instancia de Element, los hijos de ese nodo se agregan al nuevo padre.

ElementTree_extend_node.py
from xml.etree.ElementTree import (
    Element, SubElement, tostring, XML,
)
from ElementTree_pretty import prettify

top = Element('top')

parent = SubElement(top, 'parent')

children = XML(
    '<root><child num="0" /><child num="1" />'
    '<child num="2" /></root>'
)
parent.extend(children)

print(prettify(top))

En este caso, el nodo con la etiqueta root creado al analizar la cadena XML tiene tres hijos, que se agregan al nodo parent. El nodo root no es parte del árbol de salida.

$ python3 ElementTree_extend_node.py

<?xml version="1.0" ?>
<top>
  <parent>
    <child num="0"/>
    <child num="1"/>
    <child num="2"/>
  </parent>
</top>

Es importante entender que extend() no modifica ninguna relación padre-hijo existente con los nodos. Si los valores pasados a extend() ya existen en algún lugar del árbol, todavía estarán allí, y se repetirán en la salida.

ElementTree_extend_node_copy.py
from xml.etree.ElementTree import (
    Element, SubElement, tostring, XML,
)
from ElementTree_pretty import prettify

top = Element('top')

parent_a = SubElement(top, 'parent', id='A')
parent_b = SubElement(top, 'parent', id='B')

# Create children
children = XML(
    '<root><child num="0" /><child num="1" />'
    '<child num="2" /></root>'
)

# Set the id to the Python object id of the node
# to make duplicates easier to spot.
for c in children:
    c.set('id', str(id(c)))

# Add to first parent
parent_a.extend(children)

print('A:')
print(prettify(top))
print()

# Copy nodes to second parent
parent_b.extend(children)

print('B:')
print(prettify(top))
print()

Establecer el atributo id de estos hijos en el identificador Python único de objeto destaca el hecho de que los mismos objetos de nodo aparecen en el árbol de salida más de una vez.

$ python3 ElementTree_extend_node_copy.py

A:
<?xml version="1.0" ?>
<top>
  <parent id="A">
    <child id="4316789880" num="0"/>
    <child id="4316789960" num="1"/>
    <child id="4316790040" num="2"/>
  </parent>
  <parent id="B"/>
</top>


B:
<?xml version="1.0" ?>
<top>
  <parent id="A">
    <child id="4316789880" num="0"/>
    <child id="4316789960" num="1"/>
    <child id="4316790040" num="2"/>
  </parent>
  <parent id="B">
    <child id="4316789880" num="0"/>
    <child id="4316789960" num="1"/>
    <child id="4316790040" num="2"/>
  </parent>
</top>

Serialización de XML a un flujo

tostring() está implementado escribiendo en un objeto de memoria similar a un archivo, devolviendo una cadena que representa el árbol de elementos completo. Al trabajar con grandes cantidades de datos, toma menos memoria y hace un uso más eficiente de las bibliotecas de E/S el escribir directamente en un identificador de archivo usando el método write() de ElementTree.

ElementTree_write.py
import io
import sys
from xml.etree.ElementTree import (
    Element, SubElement, Comment, ElementTree,
)

top = Element('top')

comment = Comment('Generated for PyMOTW')
top.append(comment)

child = SubElement(top, 'child')
child.text = 'This child contains text.'

child_with_tail = SubElement(top, 'child_with_tail')
child_with_tail.text = 'This child has regular text.'
child_with_tail.tail = 'And "tail" text.'

child_with_entity_ref = SubElement(top, 'child_with_entity_ref')
child_with_entity_ref.text = 'This & that'

empty_child = SubElement(top, 'empty_child')

ElementTree(top).write(sys.stdout.buffer)

El ejemplo usa sys.stdout.buffer para escribir en la consola en lugar de sys.stdout porque ElementTree produce bytes codificados en lugar de una cadena Unicode. También podría escribir a un archivo abierto en modo binario o un conector.

$ python3 ElementTree_write.py

<top><!--Generated for PyMOTW--><child>This child contains text.</ch
ild><child_with_tail>This child has regular text.</child_with_tail>A
nd "tail" text.<child_with_entity_ref>This &amp; that</child_with_en
tity_ref><empty_child /></top>

El último nodo del árbol no contiene texto ni subnodos, por lo que es escrito como una etiqueta vacía, <empty_child/>. write() toma un argumento method para controlar el manejo de nodos vacíos.

ElementTree_write_method.py
import io
import sys
from xml.etree.ElementTree import (
    Element, SubElement, ElementTree,
)

top = Element('top')

child = SubElement(top, 'child')
child.text = 'Contains text.'

empty_child = SubElement(top, 'empty_child')

for method in ['xml', 'html', 'text']:
    print(method)
    sys.stdout.flush()
    ElementTree(top).write(sys.stdout.buffer, method=method)
    print('\n')

Tres métodos son soportados:

xml
El método por defecto, produce <empty_child />.
html
Produce el par de etiquetas, como se requiere en los documentos HTML (<empty_child></empty_child>).
texto
Imprime solo el texto de los nodos y omite las etiquetas vacías por completo.
$ python3 ElementTree_write_method.py

xml
<top><child>Contains text.</child><empty_child /></top>

html
<top><child>Contains text.</child><empty_child></empty_child></t
op>

text
Contains text.