Apartados


7. XML

Nivel de dificultad:4 sobre 5

``En el gobierno de Aristemo,
Draco aplicó sus ordenanzas.''
--Aristóteles

7.1 Inmersión

La mayoría de los capítulos de este libro se han desarrollado alrededor de un código de ejemplo. Pero XML no trata sobre código, trata sobre datos. Un uso muy común para XML es la ``provisión de contenidos sindicados'' que lista los últimos artículos de un blog, foro u otro sitio web con actualizaciones frecuentes. El software para blogs más popular puede generar fuentes de información y actualizarlas cada vez que hay nuevos artículos, hilos de discusión o nuevas entradas en un blog. Puedes seguir un blog ``escribiéndote'' a su canal (feed), y puedes seguir diversos blogs mediante un agregador de canales como el lector de Google.

Aquí están los datos de XML que utilizaremos en este capítulo. Es un canal --específicamente, una fuente de información sindicada Atom.

<?xml version='1.0' encoding='utf-8'?>
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
  <title>dive into mark</title>
  <subtitle>currently between addictions</subtitle>
  <id>tag:diveintomark.org,2001-07-29:/</id>
  <updated>2009-03-27T21:56:07Z</updated>
  <link rel='alternate' type='text/html' 
        href='http://diveintomark.org/'/>
  <link rel='self' type='application/atom+xml' 
        href='http://diveintomark.org/feed/'/>
  <entry>
    <author>
      <name>Mark</name>
      <uri>http://diveintomark.org/</uri>
    </author>
    <title>Dive into history, 2009 edition</title>
    <link rel='alternate' type='text/html'
      href='http://diveintomark.org/archives/2009/03/27/ (sigue abajo)
      dive-into-history-2009-edition'/>
    <id>tag:diveintomark.org,2009-03-27:/archives/20090327172042</id>
    <updated>2009-03-27T21:56:07Z</updated>
    <published>2009-03-27T17:20:42Z</published>
    <category scheme='http://diveintomark.org' term='diveintopython'/>
    <category scheme='http://diveintomark.org' term='docbook'/>
    <category scheme='http://diveintomark.org' term='html'/>
  <summary type='html'>Putting an entire chapter on one page sounds
    bloated, but consider this &amp;mdash; my longest chapter so far
    would be 75 printed pages, and it loads in under 5 
    seconds&amp;hellip; On dialup.</summary>
  </entry>
  <entry>
    <author>
      <name>Mark</name>
      <uri>http://diveintomark.org/</uri>
    </author>
    <title>Accessibility is a harsh mistress</title>
    <link rel='alternate' type='text/html'
      href='http://diveintomark.org/archives/2009/03/21/ (sigue)
      accessibility-is-a-harsh-mistress'/>
    <id>tag:diveintomark.org,2009-03-21:/archives/20090321200928</id>
    <updated>2009-03-22T01:05:37Z</updated>
    <published>2009-03-21T20:09:28Z</published>
    <category scheme='http://diveintomark.org' term='accessibility'/>
    <summary type='html'>The accessibility orthodoxy does not permit
      people to question the value of features that are rarely 
      useful and rarely used.</summary>
  </entry>
  <entry>
    <author>
      <name>Mark</name>
    </author>
    <title>A gentle introduction to video encoding, part 1: 
           container formats</title>
    <link rel='alternate' type='text/html'
      href='http://diveintomark.org/archives/2008/12/18/ (sigue)
            give-part-1-container-formats'/>
    <id>tag:diveintomark.org,2008-12-18:/archives/20081218155422</id>
    <updated>2009-01-11T19:39:22Z</updated>
    <published>2008-12-18T15:54:22Z</published>
    <category scheme='http://diveintomark.org' term='asf'/>
    <category scheme='http://diveintomark.org' term='avi'/>
    <category scheme='http://diveintomark.org' term='encoding'/>
    <category scheme='http://diveintomark.org' term='flv'/>
    <category scheme='http://diveintomark.org' term='GIVE'/>
    <category scheme='http://diveintomark.org' term='mp4'/>
    <category scheme='http://diveintomark.org' term='ogg'/>
    <category scheme='http://diveintomark.org' term='video'/>
    <summary type='html'>These notes will eventually become part of a
      tech talk on video encoding.</summary>
  </entry>
</feed>

7.2 Curso rápido de 5 minutos sobre XML

Si conoces ya XML puedes saltarte esta sección.

XML es una forma generalizada de describir una estructura de datos jerárquica. Un documento XML contiene uno o más elementos, que están delimitados por etiquetas de inicio y fin. Lo siguiente es un documento XML completo (aunque bastante aburrido).

<foo>
</foo>

  1. Línea 1: Esta es la etiqueta de inicio del elemento foo.

  2. Línea 2: Esta es la etiqueta de fin del elemento foo, que es pareja de la anterior. Como los paréntesis en la escritura, matemáticas o código, toda etiqueta de inicio debe cerrase con una etiqueta de fin.

Los elementos se pueden anidar a cualquier profundidad. Si un elemento bar se encuentra dentro de un elemento foo, se dice que bar es un subelemento o hijo de foo.

<foo>
  <bar></bar>
</foo>

Al primer elemento de un documento XML se le llama el elemento raíz. Un documento XML únicamente puede tener un elemento raíz. Lo siguiente no es un documento XML porque tiene dos elementos raíz:

<foo></foo>
<bar></bar>

Los elementos pueden tener atributos, que son parejas de nombres con valores. Los atributos se deben incluir dentro de la etiqueta de inicio del elemento y deben estar separados por un espacio en blanco. Los nombres de atributo no se pueden repetir dentro de un elemento. Los valores de los atributos deben ir entre comillas. Es posible utilizar tanto comillas simples como dobles.

<foo lang='en'>
  <bar id='papayawhip' lang="fr"></bar>
</foo>

  1. Línea 1: El elemento foo tiene un atributo denominado lang. El valor del atributo lang es en.

  2. Línea 2: El elemento bar tiene dos atributos. El valor del atributo lang es fr. Esto no entra en conflicto con el elemento foo, cada elemento tiene su propio conjunto de atributos.

Si un elemento tiene más de un atributo, el orden de los mismos no es significativo. Los atributos de un elemento forman un conjunto desordenado de claves y valores, como en un diccionario de Python. No existe límite en el número de atributos que puedes definir para cada elemento.

Los elementos pueden contener texto.

<foo lang='en'>
  <bar lang='fr'>PapayaWhip</bar>
</foo>

Existe una forma de escribir elementos vacíos de forma compacta. Colocando un carácter / al final de la etiqueta de inicio se puede evitar tener que escribir la etiqueta de fin. El documento XML del ejemplo anterior se puede escribir de esta otra forma:

<foo/>

Como pasa con las funciones de Python que se pueden declarar en diferentes módulos, los elementos XML se pueden declarar en diferentes espacios de nombre. Los espacios de nombre se suelen representar como URLs. Se puede utilizar una declaración xmlns para definir un espacio de nombres por defecto. Una declaración de un espacio de nombres es parecida a un atributo, pero tiene un significado y propósito diferente.

<feed xmlns='http://www.w3.org/2005/Atom'>
  <title>dive into mark</title>
</feed>

  1. Línea 1: El elemento feed se encuentra en el espacio de nombres http://www.w3.org/2005/Atom.

  2. Línea 2: El elemento title se encuentra también en el espacio de nombres http://www.w3.org/2005/Atom. La declaración del espacio de nombres afecta al elemento en el que está declarado y a todos los elementos hijo.

<atom:feed xmlns:atom='http://www.w3.org/2005/Atom'>
  <atom:title>dive into mark</atom:title>
</atom:feed>

  1. Línea 1: El elemento feed se encuentra en el espacio de nombres http://www.w3.org/2005/Atom.

  2. Línea 2: El elemento title también se encuentra en el espacio de nombres http://www.w3.org/2005/Atom.

En lo que concierne al analizador de XML, los dos documentos anteriores son idénticos. Espacio de nombres + nombre de elemento = identidad en XML. Los prefijos existen únicamente para referirse a los espacios de nombres, por lo que el prefijo utilizado en la práctica (atom:) es irrelevante. Los espacios de nombre coinciden, los nombres de elemento coinciden, los atributos (o falta de ellos) coinciden y cada contenido de texto coincide, por lo que estos dos documentos XML son el idénticos a efectos prácticos.

Finalmente, los documentos XML pueden contener en la primera línea información sobre la codificación de caracteres, antes del elemento raíz. Si tienes curiosidad sobre cómo un documento puede contener información que necesita conocerse antes de que el documento pueda analizarse consulta la Sección F de la especificación XML (http://www.w3.org/TR/REC-xml/#sec-guessing-no-ext-info) para ver los detalles sobre cómo resolver este problema.

<?xml version='1.0' encoding='utf-8'?>

Y con esto ya conoces suficiente XML como para ¡ser peligroso!

7.3 La estructura de una fuente de información Atom

Piensa en un blog o en cualquier sitio web que tenga contenido frecuentemente actualizado como CNN.com. El propio sitio dispone de un título (CNN.com), un subtítulo (Breaking News, U.S., World, Weather, Entertaintment y Video News), una fecha de última actualización (actualizado a 12:43 p.m. EDT, Sat May 16, 2009) y una lista de artículos publicados en diferente momentos. Cada artículo, a su vez, tiene título, una fecha de primera publicación (y posiblemente una fecha de última actualización, si se publicó una corrección) y una URL única.

El formato de sindicación de contenidos Atom está diseñado para capturar toda esta información en un formato estándar. Mi blog y CNN.com son muy diferentes en diseño, ámbito y audiencia; pero ambos tienen la misma estructura básica. CNN.com tiene un título, mi blog tiene un título. CNN.com publica artículos, yo publico artículos.

En el nivel más alto existe el elemento raíz, que toda fuente Atom comparte: el elemento feed del espacio de nombres http://www.w3.org/2005/Atom.

<feed xmlns='http://www.w3.org/2005/Atom'
      xml:lang='en'>

  1. Línea 1: El espacio de nombres de Atom es http://www.w3.org/2005/Atom.

  2. Línea 2: Cualquier elemento puede contener un atributo xml:lang que sirve para declarar el idioma del elemento y de sus hijos. En este caso, el atributo xml:lang se declara una única vez en el elemento raíz, lo que significa que toda la fuente se encuentra en inglés.

Una fuente Atom contiene diversas partes de información sobre la propia fuente. Se declaran como hijas del elemento raíz feed.

<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
  <title>dive into mark</title>
  <subtitle>currently between addictions</subtitle>
  <id>tag:diveintomark.org,2001-07-29:/</id>
  <updated>2009-03-27T21:56:07Z</updated>
  <link rel='alternate' type='text/html'
        href='http://diveintomark.org/'/>

  1. Línea 2: El título de esta fuente es dive into mark.

  2. Línea 3: El subtítulo es currently between addictions.

  3. Línea 4: Toda fuente necesita un identificador único global. Hay que mirar la RFC 41517.1 para ver cómo crear uno.

  4. Línea 5: Esta fuente fue actualizada por última vez el 27 de marzo de 2009 a las 21:56 GMT. Normalmente es equivalente a la fecha de última modificación del artículo más reciente.

  5. Línea 6: Ahora las cosas comienzan a ponerse interesantes. Este elemento link no tiene contenido de texto, pero tiene tres atributos: rel, type y href. El valor de rel indica la clase de enlace que es; rel='alternate' significa que es un enlace a una representación alternativa de esta fuente. El atributo type='text/html' significa que es un enlace a una página HTML. Por último, el destino del enlace se indica en el atributo href.

Ahora ya conocemos que esta fuente lo es de un sitio denominado ``dive into mark'' que está disponible en http://diveintomark.org y que fue actualizada por última vez el 27 de marzo de 2009.

Aunque el orden de los elementos puede ser relevante en algunos documentos XML, no es relevante en una fuente Atom.

Después de los metadatos de la fuente se encuentra una lista con los artículos más recientes. Un artículo se representa así:

<entry>
  <author>
    <name>Mark</name>
    <uri>http://diveintomark.org/</uri>
  </author>
  <title>Dive into history, 2009 edition</title>
  <link rel='alternate' type='text/html'     
    href='http://diveintomark.org/archives/2009/03/27/
dive-into-history-2009-edition'/>
  <id>tag:diveintomark.org,2009-03-27:/archives/20090327172042</id>
  <updated>2009-03-27T21:56:07Z</updated>     
  <published>2009-03-27T17:20:42Z</published>        
  <category scheme='http://diveintomark.org' term='diveintopython'/>
  <category scheme='http://diveintomark.org' term='docbook'/>
  <category scheme='http://diveintomark.org' term='html'/>
  <summary type='html'>Putting an entire chapter on one page sounds
    bloated, but consider this &amp;mdash; my longest chapter so far
    would be 75 printed pages, and it loads in under 5 seconds
    &amp;hellip; On dialup.</summary>
</entry>             

  1. Línea 2: El elemento author indica quién escribió este artículo: un individuo llamado Mark, a quién puedes encontrar en http://diveintomark.org/ (Es el mismo sitio que el enlace alternativo para la fuente, pero no tiene porqué serlo. Muchos blogs tienen varios autores, cada uno con su propio sitio web personal).

  2. Línea 6: El elemento title indica el título del artículo. ``Dive into history, 2009 edition''.

  3. Línea 7: Como con el enlace alternativo en el nivel de la fuente, este elemento link indica la dirección de la versión HTML de este artículo.

  4. Línea 10: Las entradas, como la fuente, necesitan un identificador único.

  5. Línea 11: Las entradas tienen dos fechas: la fecha de primera publicación (published) y la fecha de última modificación (updated).

  6. Línea 13: Las entradas pueden tener un número arbitrario de categorías. Este artículo está archivado bajo las categorías diveintopython, docbook y html.

  7. Línea 16: El elemento summary ofrece un breve resumen del artículo (Existe también un elemento content, que no se muestra aquí, por si quieres incluir el texto completo del artículo en tu fuente). Este resumen tiene el atributo específico de Atom type='html' que indica que este resumen está escrito en HTML, no es texto plano. Esto es importante puesto que existen entidades específicas de HTML e el texto (&mdash; y &hellip;) que se deben mostrar como ``--'' y ``...'' en lugar de que se muestre el texto directamente.

  8. Línea 20: Por último, la etiqueta de cierre del elemento entry, que señala el final de los metadatos de este artículo.

7.4 Análisis de XML

Python puede analizar documentos XML de diversas formas. Dispone de analizadores DOM y SAX como otros lenguajes, pero me centraré en una librería diferente denominada ElementTree.

»> import xml.etree.ElementTree as etree
»> tree = etree.parse('examples/feed.xml')
»> root = tree.getroot()
»> root
<Element http://www.w3.org/2005/Atomfeed at cd1eb0>

  1. Línea 1: La librería ElementTree forma parte de la librería estándar de Python, se encuentra en xml.etree.ElementTree.

  2. Línea 2: El punto de entrada primario de la librería es la función parse() que puede tomar como parámetro el nombre de un fichero o un objeto de flujo. Esta función analiza el documento entero de una vez. Si la memoria es escasa, existen formas para analizar un documento XML de forma incremental7.2.

  3. Línea 3: La función parse() devuelve un objeto que representa al documento completo. No es el elemento raíz. Para obtener una referencia al elemento raíz, debes llamar al método getroot().

  4. Línea 4: Como cabría esperar, el elemento raíz es el elemento feed del espacio de nombres http://www.w3.org/2005/Atom. La representación en cadena de texto de este elemento incide en un punto importante: un elemento XML es una combinación de su espacio de nombres y la etiqueta de su nombre (también de nominado el nombre local). Todo elemento de este documento se encuentra en el espacio de nombres Atom, por lo que el elemento raíz se representa como {http://www.w3.org/2005/Atom}feed.

ElementTree representa a los elementos XML como {espacio_de_nombres}nombre_local. Verás y utilizarás este formato en muchos lugares de la API de ElementTree.

7.4.1 Los elementos son listas

En la API de ElementTree los elementos se comportan como listas. Los elementos de la lista son los hijos del elemento.

# sigue del ejemplo anterior
»> root.tag
'http://www.w3.org/2005/Atomfeed'
»> len(root)
8
»> for child in root:
...   print(child)
... 
<Element http://www.w3.org/2005/Atomtitle at e2b5d0>
<Element http://www.w3.org/2005/Atomsubtitle at e2b4e0>
<Element http://www.w3.org/2005/Atomid at e2b6c0>
<Element http://www.w3.org/2005/Atomupdated at e2b6f0>
<Element http://www.w3.org/2005/Atomlink at e2b4b0>
<Element http://www.w3.org/2005/Atomentry at e2b720>
<Element http://www.w3.org/2005/Atomentry at e2b510>
<Element http://www.w3.org/2005/Atomentry at e2b750>

  1. Línea 2: Continuando con el ejemplo anterior, el elemento raíz es {http://www.w3.org/2005/Atom}feed.

  2. Línea 4: La ``longitud'' del elemento raíz es el número de elementos hijo.

  3. Línea 6: Puedes utilizar el elemento como iterador para recorrer todos los elementos hijo.

  4. Línea 7: Como ves por la salida, existen ocho elementos hijos: todos los metadatos de la fuente (title. subtitle, id, updated y link) seguidos por los tres elementos entry.

Puede que ya te hayas dado cuenta, pero quiero dejarlo explícito: la lista de los elementos hijo, únicamente incluye los hijos directos. Cada uno de los elementos entry tiene sus propios hijos, pero no se muestran en esta lista. Estarán incluidos en la lista de hijos del elemento entry, pero no se encuentran en la lista de feed. Existen formas de encontrar elementos independientemente de los profundamente anidados que se encuentren; lo veremos más adelante en este mismo capítulo.

7.4.2 Los atributos son diccionarios

XML no solamente es una colección de elementos; cada elemento puede tener también su propio conjunto de atributos. Una vez tienes la referencia a un elemento específico puedes recuperar fácilmente sus atributos utilizando un diccionario de Python.

# sigue del ejemplo anterior
»> root.attrib
'http://www.w3.org/XML/1998/namespacelang': 'en'
»> root[4]
<Element http://www.w3.org/2005/Atomlink at e181b0>
»> root[4].attrib
'href': 'http://diveintomark.org/',
 'type': 'text/html',
 'rel': 'alternate'
»> root[3]
<Element http://www.w3.org/2005/Atomupdated at e2b4e0>
»> root[3].attrib

  1. Línea 2: La propiedad attrib es un diccionario que contiene los atributos del elemento. El texto XML original era <feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>. El prefijo xml: se refiere a un espacio de nombres interno que todo documento XML puede utilizar sin necesidad de declararlo.

  2. Línea 4: El quinto hijo --[4] en una lista cuyo primer elemento se cuenta como cero-- es el elemento link.

  3. Línea 6: El elemento link tiene tres atributos: href, type y rel.

  4. Línea 10: El cuarto hijo --[3]-- es elemento updated.

  5. Línea 12: El elemento updated no tiene atributos por lo que .attrib es un diccionario vacío.

7.5 Búsqueda de nodos en un documento XML

Hasta ahora hemos trabajado con este documento XML de ``arriba hacia abajo'', comenzando por el elemento raíz, recuperando sus hijos y luego los nietos, etc. Pero muchas aplicaciones de XML necesitan encontrar elementos específicos. ElementTree puede hacer esto también.

»> import xml.etree.ElementTree as etree
»> tree = etree.parse('examples/feed.xml')
»> root = tree.getroot()
»> root.findall('http://www.w3.org/2005/Atomentry')
[<Element http://www.w3.org/2005/Atomentry at e2b4e0>,
 <Element http://www.w3.org/2005/Atomentry at e2b510>,
 <Element http://www.w3.org/2005/Atomentry at e2b540>]
»> root.tag
'http://www.w3.org/2005/Atomfeed'
»> root.findall('http://www.w3.org/2005/Atomfeed')
[]
»> root.findall('http://www.w3.org/2005/Atomauthor')
[]

  1. Línea 4: El método findall() encuentra todos los elementos hijo que coinciden con una consulta específica (En breve veremos los formatos posibles de la consulta).

  2. Línea 10: Cada elemento --incluido el elemento raíz, pero también sus hijos-- tiene un método findall(). Permite encontrar todos los elementos que coinciden entre sus hijos. Pero ¿porqué no devuelve esta consulta ningún resultado? Aunque no sea obvio, esta consulta particular únicamente busca entre los hijos del elemento. Puesto que el elemento raíz feed no tiene ningún hijo denominado feed, esta consulta devuelve una lista vacía.

  3. Línea 12: También te puede sorprender este resultado. Existe un elemento author en este documento; de hecho hay tres (uno en cada entry). Pero estos elementos author no son hijos directos el elemento raíz; son ``nietos'' (literalmente, un elemento hijo de otro elemento hijo). Si quieres buscar elementos author en cualquier nivel de profundidad puedes hacerlo, pero el formato de la consulta es algo distinto.

»> tree.findall('http://www.w3.org/2005/Atomentry')
[<Element http://www.w3.org/2005/Atomentry at e2b4e0>,
 <Element http://www.w3.org/2005/Atomentry at e2b510>,
 <Element http://www.w3.org/2005/Atomentry at e2b540>]
»> tree.findall('http://www.w3.org/2005/Atomauthor')
[]

  1. Línea 1: Por comodidad, el objeto tree (devuelto por la función etree.parse() tiene varios métodos que replican aquellos disponibles en el elemento raíz. Los resultados son idénticos a los que se obtienen si se llamase a tree.getroot().findall().

  2. Linea 5: Tal vez te pueda sorprender, pero esta consulta no encuentra a los elementos author del documento. ¿Porqué no? porque es simplemente una forma de llamar a tree.getroot().findall('{http://www.w3.org/2005/Atom}author'), lo que significa ``encuentra todos los elementos author que sean hijos directos del elemento raíz''. Los elementos author no son hijos del elemento raíz; son hijos de los elementos entry. Por eso la consulta no retorna ninguna coincidencia.

También hay un método find() que retorna el primer elemento que coincide. Es útil para aquellas situaciones en las que únicamente esperas una coincidencia, o cuando haya varias pero solamente te importa la primera de ellas.

»> entries = tree.findall('http://www.w3.org/2005/Atomentry')
»> len(entries)
3
»> title_element = entries[0].find('http://www.w3.org/2005/Atomtitle')
»> title_element.text
'Dive into history, 2009 edition'
»> foo_element = entries[0].find('http://www.w3.org/2005/Atomfoo')
»> foo_element
»> type(foo_element)
<class 'NoneType'>

  1. Línea 1: Viste esto en el ejemplo anterior. Encuentra todos los elementos atom:entry.

  2. Línea 4: El método find() toma una consulta y retorna el primer elemento que coincide.

  3. Línea 7: No existen elementos denominados foo por lo que retorna None.

Hay una complicación en el método find() que te pasará en algún momento. En un contexto booleano los objetos elemento de ElementTree se evalúan a False si no tienen hijos (si len(element) es cero). Esto significa que if element.find('...') no está comprobando si el método find() encontró un elemento coincidente; está comprobando si ¡el elemento coincidente tiene algún elemento hijo! Para comprobar si el método find() retornó algún elemento debes utilizar if element.find('...') is not None.

Existe una forma de buscar entre los elementos descendientes: hijos, nietos y niveles más profundos de anidamiento.

»> all_links = tree.findall('//http://www.w3.org/2005/Atomlink')
»> all_links
[<Element http://www.w3.org/2005/Atomlink at e181b0>,
 <Element http://www.w3.org/2005/Atomlink at e2b570>,
 <Element http://www.w3.org/2005/Atomlink at e2b480>,
 <Element http://www.w3.org/2005/Atomlink at e2b5a0>]
»> all_links[0].attrib
'href': 'http://diveintomark.org/',
 'type': 'text/html',
 'rel': 'alternate'
»> all_links[1].attrib
'href': 'http://diveintomark.org/archives/2009/03/27/
  dive-into-history-2009-edition',
 'type': 'text/html',
 'rel': 'alternate'
»> all_links[2].attrib
'href': 'http://diveintomark.org/archives/2009/03/21/
  accessibility-is-a-harsh-mistress',
 'type': 'text/html',
 'rel': 'alternate'
»> all_links[3].attrib
'href': 'http://diveintomark.org/archives/2008/12/18/
  give-part-1-container-formats',
 'type': 'text/html',
 'rel': 'alternate'

  1. Línea 1: Esta consulta --//http://www.w3.org/2005/Atomlink-- es muy similar a las anteriores, excepto por las dos barras inclinadas al comienzo de la consulta. Estas dos barras significan que ``no se busque únicamente entre los hijos directos; quiero cualquier elemento que coincida independientemente del nivel de anidamiento''. Por eso el resultado es una lista de cuatro elementos link, no únicamente uno.

  2. Línea 7: El primer resultado es hijo directo del elemento raíz. Como puedes observar por sus atributos, es el enlace alternativo que apunta a la versión HTML del sitio web que esta fuente describe.

  3. Línea 11: Los otros tres resultados son cada uno de los enlaces alternativos de cada entrada. Cada entry tiene un único elemento hijo link. Debido a la doble barra inclinada al comienzo de la consulta, se encuentran todos estos enlaces.

En general, el método findall() de ElementTree es una característica muy potente, pero el lenguaje de consulta puede ser un poco sorprendente. Está descrito oficialmente en http://effbot.org/zone/element-xpath.htm(Soporte limitado a expresiones XPath). XPath es un estándar del W3C para consultar documentos XML. El lenguaje de consulta de ElementTree es suficientemente parecido a XPath para poder hacer búsquedas básicas, pero también suficientemente diferente como para desconcertarte si ya conoces XPath.

Ahora vamos a ver una librería de terceros que extiende la API de ElementTree para proporcionar un soporte completo de XPath.

7.6 Ir más allá con LXML

lxml7.3 es una librería de terceros de código abierto que se desarrolla sobre el popular analizador libxml27.4. Proporciona una API que es 100% compatible con ElementTree, y la extiende con soporte completo a Xpath 1.0 y otras cuantas bondades. Existe un instalador disponible para Windows7.5; los usuarios de Linux siempre deberían intentar usar las herramientas específicas de la distribución como yum o apt-get para instalar los binarios precompilados desde sus repositorios. En otro caso, necesitarás instalar los binarios manualmente7.6.

»> from lxml import etree
»> tree = etree.parse('examples/feed.xml')
»> root = tree.getroot()
»> root.findall('http://www.w3.org/2005/Atomentry')
[<Element http://www.w3.org/2005/Atomentry at e2b4e0>,
 <Element http://www.w3.org/2005/Atomentry at e2b510>,
 <Element http://www.w3.org/2005/Atomentry at e2b540>]

  1. Línea 1: Una vez importado, lxml proporciona la misma API que la librería estándar ElementTree.

  2. Línea 2: La función parse(), igual que en ElementTree.

  3. Línea 3: El método getroot(), también igual.

  4. Línea 4: El método findall(), exactamente igual.

Para documentos XML grandes, lxml es significativamente más rápido que la librería ElementTree. Si solamente estás utilizando la API ElementTree y quieres usar la implementación más rápida existente, puedes intentar importar lxml y de no estar disponible, usar como segunda opción ElementTree.

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

Pero lxml proporciona algo más que el ser más rápido que ElementTree. Su método findall() incluye el soporte de expresiones más complicadas.

»> import lxml.etree
»> tree = lxml.etree.parse('examples/feed.xml')
»> tree.findall('//http://www.w3.org/2005/Atom*[@href]')
[<Element http://www.w3.org/2005/Atomlink at eeb8a0>,
 <Element http://www.w3.org/2005/Atomlink at eeb990>,
 <Element http://www.w3.org/2005/Atomlink at eeb960>,
 <Element http://www.w3.org/2005/Atomlink at eeb9c0>]
»> tree.findall("//http://www.w3.org/2005/Atom*"                   "[@href='http://diveintomark.org/']")
[<Element http://www.w3.org/2005/Atomlink at eeb930>]
»> NS = 'http://www.w3.org/2005/Atom'
»> tree.findall('//NSauthor[NSuri]'.format(NS=NS))
[<Element http://www.w3.org/2005/Atomauthor at eeba80>,
 <Element http://www.w3.org/2005/Atomauthor at eebba0>]

  1. Línea 1: En este ejemplo voy a importar lxml.tree en lugar de utilizar from lxml import etree, para destacar que estas características son específicas de lxml.

  2. Línea 3: Esta consulta encuentra todos los elementos del espacio de nombres Atom, en cualquier sitio del documento, que contengan el atributo href. Las // al comienzo de la consulta significa ``elementos en cualquier parte del documento (no únicamente los hijos del elemento raíz)''. {http://www.w3.org/2005/Atom} significa ``únicamente los elementos en el espacio de nombres de Atom''. * significa ``elementos con cualquier nombre local'' y @href significa ``tiene un atributo href''.

  3. Línea 8: La consulta encuentra todos los elementos Atom con el atributo href cuyo valor sea http://diveintomark.org/.

  4. Línea 11: Después de hacer un rápido formateo de las cadenas de texto (porque de otro modo estas consultas compuestas se vuelven ridículamente largas), esta consulta busca los elementos author que tienen un elemento uri como hijo. Solamente retorna dos elementos author, los de la primera y segunda entry. El author del último entry contiene únicamente el elemento name, no uri.

¿No es suficiente para ti? lxml tampoco integra soporte de expresiones XPath 1.0. No voy a entrar en profundidad en la sintaxis de XPath; se podría escribir un libro entero sobre ello. Pero te mostraré cómo se integra en lxml.

»> import lxml.etree
»> tree = lxml.etree.parse('examples/feed.xml')
»> NSMAP = 'atom': 'http://www.w3.org/2005/Atom'
»> entries = tree.xpath("//atom:category[@term='accessibility']/..",
...     namespaces=NSMAP)
»> entries 
[<Element http://www.w3.org/2005/Atomentry at e2b630>]
»> entry = entries[0]
»> entry.xpath('./atom:title/text()', namespaces=NSMAP)
['Accessibility is a harsh mistress']

  1. Línea 3: Para realizar consultas XPath en elementos de un espacio de nombres, necesitas definir dichos espacios de nombre con el mapeo a sus alias. Esto se realiza con un diccionario de Python.

  2. Línea 4: Esto es una consulta XPath. La expresión XPath busca elementos category (en el espacio de nombres de Atom) que contengan el atributo term con el valor accesibility. Pero ése no es el resultado real de la consulta. Observa el final de la cadena de texto de la consulta; ¿observaste el trozo /..? Significa que ``devuelve el elemento padre del elemento category que se acaba de encontrar''. Así esta consulta XPath encontrará todas las entradas que tengan un hijo <category term='accessibility'>.

  3. Línea 6: La función xpath() devuelve una lista de objetos ElementTree. En este documento, únicamente hay una entrada con un elemento category cuyo term sea accesibility.

  4. Línea 9: Las expresiones XPath no siempre devuelven una lista de elementos. Técnicamente, el modelo DOM de un documento XML no contiene elementos, contiene nodos. Dependiendo de su tipo, los nodos pueden ser elementos, atributos o incluso contenido de texto. El resultado de una consulta XPath es una lista de nodos. Esta consulta retorna una lista de nodos de texto: el contenido de texto (text()) del elemento title (atom:title) que sea hijo del elemento actual (./).

7.7 Generación de XML

El soporte a XML de Python no está limitado al análisis de documentos existentes. Puedes crear también documentos XML desde cero.

»> import xml.etree.ElementTree as etree
»> new_feed = etree.Element('http://www.w3.org/2005/Atomfeed',
...     attrib='http://www.w3.org/XML/1998/namespacelang': 'en')
»> print(etree.tostring(new_feed))
<ns0:feed xmlns:ns0='http://www.w3.org/2005/Atom' xml:lang='en'/>

  1. Línea 2: Para crear un elemento nuevo, se debe instanciar un objeto de la clase Element. Se le pasa el nombre del elemento (espacio de nombres + nombre local) como primer parámetro. Esta sentencia crear un elemento feed en el espacio de nombres Atom. Esta será nuestro elemento raíz del nuevo documento.

  2. Línea 3: Para añadir atributos al elemento, se puede pasar un diccionario de nombres y valores de atributos en el parámetro attrib. Observa que el nombre del atributo debe estar en el formato estándar de ElementTree, {espacio_de_nombres}nombre_local.

  3. Línea 4: En cualquier momento puedes serializar cualquier elemento (y sus hijos) con la función tostring() de ElementTree.

¿Te ha sorprendido el resultado de la serialización? La forma en la que ElementTree serializa los elementos con espacios de nombre XML es técnicamente precisa pero no óptima. El documento XML de ejemplo al comienzo del capítulo definió un espacio de nombres por defecto (xmlns='http://www.w3.org/2005/Atom'). La definición de un espacio de nombres por defecto es útil para documentos --como las fuentes Atom-- en los que todos, o la mayoría de, los elementos pertenecen al mismo espacio de nombres, porque puedes declarar el espacio de nombres una única vez y declarar cada elemento únicamente con su nombre local (<feed>, <link>, <entry>). No hay necesidad de utilizar prefijos a menos que quieras declarar elementos de otro espacio de nombres.

Un analizador XML no verá ninguna diferencia entre un documento XML con un espacio de nombres por defecto y un documento XML con un espacio de nombres con prefijo. El DOM resultante de esta serialización:

<ns0:feed xmlns:ns0='http://www.w3.org/2005/Atom' xml:lang='en'/>

Es idéntico al DOM de esta otra:

<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'/>

La única diferencia práctica es que la segunda serialización es varios caracteres más corta. Si tuviéramos que modificar nuestro ejemplo para añadirle el prefijo ns0: en cada etiqueta de inicio y fin, serían 4 caracteres por cada etiqueta de inicio x 79 etiquetas + 4 caracteres por la propia declaración del espacio de nombres, en total son 320 caracteres más. En el caso de que asumamos una codificación de caracteres UTF-8 se trata de 320 bytes extras (después de comprimir la diferencia se reduce a 21 bytes). Puede que no te importe mucho, pero para una fuente Atom, que puede descargarse miles de veces cada vez que cambia, una diferencia de unos cuantos bytes por petición puede suponer una cierta diferencia.

La librería ElementTree no ofrece un control fino sobre la serialización de los elementos con espacios de nombres, pero lxml sí:

»> import lxml.etree
»> NSMAP = None: 'http://www.w3.org/2005/Atom'
»> new_feed = lxml.etree.Element('feed', nsmap=NSMAP)
»> print(lxml.etree.tounicode(new_feed))
<feed xmlns='http://www.w3.org/2005/Atom'/>
»> new_feed.set('http://www.w3.org/XML/1998/namespacelang', 'en')
»> print(lxml.etree.tounicode(new_feed))
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'/>

  1. Línea 2: Para comenzar, se define el mapeo de los espacios de nombre como un diccionario. Los valores del diccionario son espacios de nombres; las claves son el prefijo deseado. Utilizar None como prefijo, sirve para declarar el espacio de nombres por defecto.

  2. Línea 3: Ahora puedes pasar el parámetro nsmap, que es específico de lxml, cuando vayas a crear un elemento, y lxml respectará los prefijos que hayas definido.

  3. Línea 4: Como se esperaba, esta serialización define el espacio de nombres Atom como el espacio de nombres por defecto y declara el elemento feed sin prefijo.

  4. Línea 6: ¡Subir as! Olvidamos añadir el atributo xml:lang. Siempre puedes añadir atributos a cualquier elemento con el método set(). Toma dos parámetros, el nombre del atributo en formato estándar de ElementTree y el valor del atributo. Este método no es específico de lxml, lo único específico de lxml en este ejemplo es la parte del parámetro nsmap para controlar los prefijos de la salida serializada.

¿Están los documentos XML limitados a un elemento por documento? Por supuesto que no. Puedes crear hijos de forma fácil.

»> title = lxml.etree.SubElement(new_feed, 'title',
...     attrib='type':'html')
»> print(lxml.etree.tounicode(new_feed))
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
<title type='html'/></feed>
»> title.text = 'dive into &hellip;'
»> print(lxml.etree.tounicode(new_feed))
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
<title type='html'>dive into &amp;hellip;</title></feed>
»> print(lxml.etree.tounicode(new_feed, pretty_print=True))
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
<title type='html'>dive into&amp;hellip;</title>
</feed>

  1. Línea 1: Para crear elementos hijo de un elemento existente, instancia objetos de la clase SubElement. Los parámetros necesarios son el elemento padre (new_feed en este caso) y el nombre del nuevo elemento. Puesto que los elementos hijo heredan el espacio de nombres de sus padres, no hay necesidad de redeclarar el espacio de nombres o sus prefijos.

  2. Línea 2: Puedes pasarle un diccionario de atributos. Las claves son los nombres de los atributos y los valores son los valores de los atributos.

  3. Línea 3: Como esperabas, el nuevo elemento title se ha creado en el espacio de nombres Atom y fue insertado como hijo del elemento feed. Puesto que el elemento title no tiene contenido de texto y no tiene hijos por sí mismo, lxml lo serializa como un elemento vacío (con />).

  4. Línea 6: Para establecer el contenido de texto de un elemento basta con asignarle valor a la propiedad .text.

  5. Línea 7: Ahora el elemento title se serializa con su contenido de texto. Cualquier contenido de texto que contenga símbolos 'menor que' o ampersands necesitan 'escaparse' al serializarse. lxml hace estas conversiones de forma automática.

  6. Línea 10: Puedes aplicar una impresión formateada a la serialización, lo que inserta los saltos de línea correspondientes al cambiar las etiquetas. En términos técnicos, lxml añade espacios en blanco no significativos para hacer más legible la salida resultante.

Podrías querer echarle un vistazo a xmlwitch7.7, otra librería de terceros para generar XML. Hace uso extensivo de la sentencia with para hacer la generación de código XML más legible.

7.8 Análisis de XML ``estropeado''

La especificación XML obliga a que todos los analizadores XML empleen un manejo de errores ``draconiano''. Esto es, deben parar tan pronto como detecten cualquier clase de error de ``malformado'' del documento. Errores de mala formación del documento son: que las etiquetas de inicio y fin no se encuentren bien balanceadas, entidades sin definir, caracteres unicode ilegales y otro número de reglas esotéricas. Esto es un contraste importante con otros formatos habituales como HTML --tu navegador no para de mostrar una página web si se te olvida cerrar una etiqueta HTML o aparece un escape o ampersand en el valor de un atributo (Es un concepto erróneo bastante extendido que HTML no tiene definida una forma de hacer manejo de errores. Sí que está bien definido, pero es significativamente más complejo que ``párate ante el primer error que encuentres''.

Algunas personas (yo mismo incluido) creen que fue un error para los inventores del XML obligar a este manejo de errores ``draconianos''. No me malinterpretes; puedo comprender el encanto de la simplificación de las reglas de manejo de errores. Pero en la práctica, el concepto de ``bien formado'' es más complejo de lo que suena, especialmente para aquellos documentos XML (como los documentos Atom) se publican en la web mediante un servidor HTTP. A pesar de la madurez de XML, cuyo manejo estandarizado de errores es de 1997, las encuestas muestran continuamente que una significativa fracción de fuentes Atom de la web están plagadas con errores de ``buena formación''.

Por eso, tengo razones teóricas y prácticas para analizar documentos XML a ``cualquier precio'', esto es, para no parar ante el primer error de formación. Si te encuentras tú mismo en esta situación, lxml puede ayudar.

Aquí hay un fragmento de un documento XML mal formado. El ampersand debería estar ``escapado''.

<?xml version='1.0' encoding='utf-8'?>
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
  <title>dive into &hellip;</title>
...
</feed>

Eso es un error, porque la entidad &hellip; no está definida en XML (está definida en HTML). Si intentas analizar este documento XML con los valores por defecto, lxml parará en la entidad sin definir.

»> import lxml.etree
»> tree = lxml.etree.parse('examples/feed-broken.xml')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "lxml.etree.pyx", line 2693, 
       in lxml.etree.parse (src/lxml/lxml.etree.c:52591)
  File "parser.pxi", line 1478, 
       in lxml.etree._parseDocument (src/lxml/lxml.etree.c:75665)
  File "parser.pxi", line 1507, 
       in lxml.etree._parseDocumentFromURL (src/lxml/lxml.etree.c:75993)
  File "parser.pxi", line 1407, 
       in lxml.etree._parseDocFromFile (src/lxml/lxml.etree.c:75002)
  File "parser.pxi", line 965, 
       in lxml.etree._BaseParser._parseDocFromFile 
       (src/lxml/lxml.etree.c:72023)
  File "parser.pxi", line 539, 
       in lxml.etree._ParserContext._handleParseResultDoc 
       (src/lxml/lxml.etree.c:67830)
  File "parser.pxi", line 625, 
       in lxml.etree._handleParseResult (src/lxml/lxml.etree.c:68877)
  File "parser.pxi", line 565, 
       in lxml.etree._raiseParseError (src/lxml/lxml.etree.c:68125)
lxml.etree.XMLSyntaxError: 
       Entity 'hellip' not defined, line 3, column 28

Para analizar este documento, a pesar de su error de buena formación, necesitas crear un analizador XML específico.

»> parser = lxml.etree.XMLParser(recover=True)
»> tree = lxml.etree.parse('examples/feed-broken.xml', parser)
»> parser.error_log 
examples/feed-broken.xml:3:28:FATAL:PARSER:ERR_UNDECLARED_ENTITY: 
    Entity 'hellip' not defined
»> tree.findall('http://www.w3.org/2005/Atomtitle')
[<Element http://www.w3.org/2005/Atomtitle at ead510>]
»> title = tree.findall('http://www.w3.org/2005/Atomtitle')[0]
»> title.text
'dive into '
»> print(lxml.etree.tounicode(tree.getroot()))
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
  <title>dive into </title>
.
. [resto de la serialización suprimido por brevedad]
.

  1. Línea 1: Para crear un analizador específico, se debe instanciar la clase lxml.etree.XMLParser. Puede recibir un número diferente de parámetros. Nos interesa ahora el parámetro recover. Cuando se establece a True, el analizador XML intentará ``recuperarse'' de este tipo de errores.

  2. Línea 2: Para analizar un documento XML con este analizador, basta con pasar este objeto parser como segundo parámetro de la función parse(). Observa que lxml no eleva ninguna excepción sobre la entidad no definida &hellip;.

  3. Línea 3: Aún así, el analizador mantiene un registro de los errores de formación que ha encontrado (Esto siempre es cierto independientemente de que esté activado para recuperarse de esos errores o no).

  4. Línea 9: Puesto que no supo que hacer con la entidad sin definir &hellip;, el analizador simplemente la descarta silenciosamente. El contenido de texto del elemento title se convierte en 'dive into '.

  5. Línea 11: Como puedes ver en la serialización, la entidad &hellip; ha sido suprimida.

Es importante reiterar que no existe garantía de interoperabilidad entre analizadores XML que se recuperan de los errores. Una analizador diferente podría decidir que reconoce la entidad &hellip; de HTML y reemplazarla por &amp;hellip; ¿Es esto mejor? Puede ser. ¿Es más correcto? No, ambas soluciones son igualmente erróneas. El comportamiento correcto (de acuerdo a la especificación XML) es pararse y elevar el error. Si has decidido que no es lo que quieres hacer, lo haces bajo tu propia responsabilidad.

7.9 Lecturas recomendadas

José Miguel González Aguilera 2016-08-18