Laboratorio de carga y procesado de datos: XML

En este laboratorio práctico, presentaremos los fundamentos básicos para descargar, procesar, transformar y almacenar localmente datos disponibles de diversas fuentes en la Web. Para ello, nos basaremos en algunas de las bibliotecas más utilizadas en Python, tales como requests o lxml. En la sección final se incluyen referencias adicionales para aquellos interesados en profundizar más en estos aspectos.

Este laboratorio está basado en recuperación y procesado de datos en formato XML. El siguiente laboratorio tendrá por objeto presentar un ejmplo de flujo de trabajo similar con datos en formato JSON, utilizado por numerosas APIs de datos abiertos en Internet, así como datos en formato CSV.

Content

  1. El proceso ETL (Extraer-Transformar-Almacenar).
  2. Fuentes de datos.
  3. Bibliotecas Python.
  4. Recopilación y carga de datos.
  5. Conclusiones.
  6. Referencias.

1. El proceso ETL

Todo proyecto de ingeniería y análisis de datos comienza por la recopilación, preparación y almacenamiento de datos en un repositorio (local, distribuido o en la nube) para su análisis posterior. En computación, este proceso se suele denominar ETL, por sus siglas en inglés Extract-Transform-Load (Extraer-Transformar-Almacenar).

Así pues, este proceso involucra tres fases:

  1. Extracción de datos.
  2. Preparación y transformación de datos.
  3. Carga de datos.

En este laboratorio, abordaremos el primer paso, aprendiendo cómo recopilar datos disponibles en la Web, representados en formato XML.

2. Fuentes de datos: XML

Existen multitud de fuentes de datos disponibles públicamente en la Web. Los tres tipos más usuales de fuentes de datos que podemos encontrar son:

  1. Recursos web: tales como páginas web o enlaces a archivos con datos representados según un formato estándar (XML, JSON, CSV, YAML, etc.). En este caso, se recupera directamente la información del recurso para almacenarla localmente, efectuando a veces un procesado o interpretación inicial de los datos descargados. Este proceso se denomina web scrapping. Los programas automáticos creados para descargar información de esta forma se denominan arañas web (web crawlers).
  2. Repositorios de datos: ofrecen un servicio de almacenamiento, clasificación y publicación de ficheros y conjuntos de datos. Esto facilita mucho la tarea del analista, a la hora de encontrar datos sobre un tema en particular. Algunos ejemplos son DataHub (patrocinado por la OKFN) o figshare. También existen diversos servicios de búsqueda de repositorios de datos (tales como OAD, Databib o re3data.
  3. APIs de datos: ofrecen una interfaz estructurada para acceder a los datos mediante un servicio público o de pago en la Web. Suelen estar asociadas a plataformas de empresas como Twitter, Facebook, Google+, LinkedIn, etc. A veces, permiten efectuar consultas para recopilar solo una fracción de los datos que cumplan ciertas condiciones, o modificar el formato de representación de los datos que devuelve en respuesta a nuestras peticiones.

Existen muchos tipos de formatos de representación de datos (ya hemos mencionado algunos en la lista anterior). En este laboratorio, nos vamos a concentrar en un formato muy popular, los documentos XML. Para el siguiente ejemplo, vamos a utilizar el fichero feed.xml, disponible en la web de Dive into Python 3 [2]. Asumiremos que el archivo ya lo hemos descargado y almacenado localmente.

Una forma sencilla de automatizar la descarga sería utilizar el módulo urllib, o si el archivo es muy grande usar el módulo requests para tener un mejor control del proceso de descarga.


In [58]:
import urllib
urllib.urlretrieve ("http://www.diveintopython3.net/examples/feed.xml", "feed_down.xml")


Out[58]:
('feed_down.xml', <httplib.HTTPMessage instance at 0x7f4c0df6b050>)

El formato XML

A continuación se muestra una fracción del contenido inicial del fichero de ejemplo XML que vamos a emplear.

<?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/'/>
  <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>
  <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/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/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>

Como podemos ver, XML es un lenguaje de marcado de documentos similar a HTML. Tenemos etiquetas de apertura y cierre, tales como <entry></entry>. Las etiquetas delimitan campos de datos, cuyo nombre corresponde con el identificador de la entiqueta (por ejemplo, en el caso anterior el campo es entry). A su vez, las etiquetas pueden contener atributos:

<link rel='alternate' type='text/html'
      href='http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition'/>

Finalmente, el contenido situado entre cada etiqueta de apertura y cierre es el texto de ese campo:

<author>
    <name>Mark</name>
</author>

A las herramientas o funciones que pueden leer e interpretar datos en un formato de representación documental específico (como XML, HTML, etc.) se les de denomina analyzadores sintáticos o parsers. Las bibliotecas que automatizan la lectura en interpretación de datos en formatos específicos siempre proporcionan funciones con este cometido.

Existen además, mecanismos para validar el contenido de un documento en formato XML, tales como la especificación de un documento DTD o de un esquema XML. En determinados entornos, esto forzará a la función que interpreta los datos compruebe si se sigue estrictamente el esquema de representación, devolviendo errores en caso contrario.

3. Bibliotecas Python

Para este laboratorio, utilizaremos la biblioteca Python lxml, una de las más extendidas y que mejor rendimiento proporciona para lectura y escritura de datos en formato XML o XHTML.

Esencialmente, existen dos aproximaciones para cargar y procesar datos en formato XML con esta biblioteca (al igual que ocurre con otras similares):

  • Carga en memoria: Cargar todo el arbol de elementos XML en memoria, para después procesarlo. Tiene la ventaja de ser muy rápido al trabajar con datos en memoria. En el caso de HTML, XML y SVG los analizadores generan una estructura denominada árbol de elementos, que permite navegar por el contenido del documento. La API DOM suele ser la referencia utilizada para acceder a dicha información de manera estándar.
  • Análisis dirigido por eventos: Leer iterativamente un flujo de datos de la fuente XML y analizar el contenido dinámicamente. Esta opción se denomina análisis dirigido por eventos. Es útil cuando no podemos cargar el documento en memoria porque ocuparía demasiado espacio. La API SAX suele ser la opción más popular para definir los métodos que permiten acceder a la información interpretada.

4. Recopilación y carga de datos

4.1 Carga en memoria

En primer lugar, debemos cargar la biblioteca la clase lxml.etree que permite crear árboles de elementos XML en memoria.


In [20]:
from lxml import etree

El siguiente paso será leer nuestro fichero de datos, que ya hemos descargado.


In [51]:
with open('feed.xml') as f:
    tree = etree.parse(f)
    print "XML version is: %s" % tree.docinfo.xml_version
    print "XML encoding used is: %s" % tree.docinfo.encoding
    # Obtener elemento root del documento
    root_elem = tree.getroot()
    # Get copy of dict of namespace prefixes in document
    ns_map = tree.getroot().nsmap.copy()
    print "Diccionario de prefijos espacio de nombres:"
    print ns_map


XML version is: 1.0
XML encoding used is: utf-8
Diccionario de prefijos espacio de nombres:
{None: 'http://www.w3.org/2005/Atom'}

In [38]:
# Algunas operaciones básicas
# Iterar sobre todos los hijos de un elemento en el árbol
for child in root_elem.iterchildren():
    print child.tag  # Como vemos, los espacios de nombres también se analizan


{http://www.w3.org/2005/Atom}title
{http://www.w3.org/2005/Atom}subtitle
{http://www.w3.org/2005/Atom}id
{http://www.w3.org/2005/Atom}updated
{http://www.w3.org/2005/Atom}link
{http://www.w3.org/2005/Atom}entry
{http://www.w3.org/2005/Atom}entry
{http://www.w3.org/2005/Atom}entry

In [49]:
# Búsqueda directa de elementos
for child in root_elem.iterfind(".//{%s}author" % ns_map[None]):
    for gchildren in child.getchildren():
        print "%s: %s" % (gchildren.tag, gchildren.text)


{http://www.w3.org/2005/Atom}name: Mark
{http://www.w3.org/2005/Atom}uri: http://diveintomark.org/
{http://www.w3.org/2005/Atom}name: Mark
{http://www.w3.org/2005/Atom}uri: http://diveintomark.org/
{http://www.w3.org/2005/Atom}name: Mark

In [53]:
# Encontrar todos los elementos con cierto atributo
for elem in tree.findall('//{%s}*[@href]' % ns_map[None]):
    print "%s href: %s" % (elem.tag, elem.get('href'))


{http://www.w3.org/2005/Atom}link href: http://diveintomark.org/
{http://www.w3.org/2005/Atom}link href: http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition
{http://www.w3.org/2005/Atom}link href: http://diveintomark.org/archives/2009/03/21/accessibility-is-a-harsh-mistress
{http://www.w3.org/2005/Atom}link href: http://diveintomark.org/archives/2008/12/18/give-part-1-container-formats

4.2 Análisis dirigido por eventos

Si la opción de cargar el árbol del documento en memoria no es viable, entonces podemos utilizar el análisis de código dirigido por eventos. En este caso, el iterador producirá un nuevo evento cada vez que encuentre una etiqueta de apertura o cierre del tipo especificado. Veamos un ejemplo.


In [57]:
with open('feed.xml') as f:
    # Si no pasamos el argumento 'events', por defecto solo se generan eventos al cierre
    # de cada etiqueta (eventos "end")
    for event, element in etree.iterparse(f, events=("start", "end")):
        print("%5s, %4s, %s" % (event, element.tag, element.text))  # Podemos acceder a la API de cada elemento


start, {http://www.w3.org/2005/Atom}feed, 
  
start, {http://www.w3.org/2005/Atom}title, dive into mark
  end, {http://www.w3.org/2005/Atom}title, dive into mark
start, {http://www.w3.org/2005/Atom}subtitle, currently between addictions
  end, {http://www.w3.org/2005/Atom}subtitle, currently between addictions
start, {http://www.w3.org/2005/Atom}id, tag:diveintomark.org,2001-07-29:/
  end, {http://www.w3.org/2005/Atom}id, tag:diveintomark.org,2001-07-29:/
start, {http://www.w3.org/2005/Atom}updated, 2009-03-27T21:56:07Z
  end, {http://www.w3.org/2005/Atom}updated, 2009-03-27T21:56:07Z
start, {http://www.w3.org/2005/Atom}link, None
  end, {http://www.w3.org/2005/Atom}link, None
start, {http://www.w3.org/2005/Atom}entry, 
    
start, {http://www.w3.org/2005/Atom}author, 
      
start, {http://www.w3.org/2005/Atom}name, Mark
  end, {http://www.w3.org/2005/Atom}name, Mark
start, {http://www.w3.org/2005/Atom}uri, http://diveintomark.org/
  end, {http://www.w3.org/2005/Atom}uri, http://diveintomark.org/
  end, {http://www.w3.org/2005/Atom}author, 
      
start, {http://www.w3.org/2005/Atom}title, Dive into history, 2009 edition
  end, {http://www.w3.org/2005/Atom}title, Dive into history, 2009 edition
start, {http://www.w3.org/2005/Atom}link, None
  end, {http://www.w3.org/2005/Atom}link, None
start, {http://www.w3.org/2005/Atom}id, tag:diveintomark.org,2009-03-27:/archives/20090327172042
  end, {http://www.w3.org/2005/Atom}id, tag:diveintomark.org,2009-03-27:/archives/20090327172042
start, {http://www.w3.org/2005/Atom}updated, 2009-03-27T21:56:07Z
  end, {http://www.w3.org/2005/Atom}updated, 2009-03-27T21:56:07Z
start, {http://www.w3.org/2005/Atom}published, 2009-03-27T17:20:42Z
  end, {http://www.w3.org/2005/Atom}published, 2009-03-27T17:20:42Z
start, {http://www.w3.org/2005/Atom}category, None
  end, {http://www.w3.org/2005/Atom}category, None
start, {http://www.w3.org/2005/Atom}category, None
  end, {http://www.w3.org/2005/Atom}category, None
start, {http://www.w3.org/2005/Atom}category, None
  end, {http://www.w3.org/2005/Atom}category, None
start, {http://www.w3.org/2005/Atom}summary, Putting an entire chapter on one page sounds
      bloated, but consider this &mdash; my longest chapter so far
      would be 75 printed pages, and it loads in under 5 seconds&hellip;
      On dialup.
  end, {http://www.w3.org/2005/Atom}summary, Putting an entire chapter on one page sounds
      bloated, but consider this &mdash; my longest chapter so far
      would be 75 printed pages, and it loads in under 5 seconds&hellip;
      On dialup.
  end, {http://www.w3.org/2005/Atom}entry, 
    
start, {http://www.w3.org/2005/Atom}entry, 
    
start, {http://www.w3.org/2005/Atom}author, 
      
start, {http://www.w3.org/2005/Atom}name, Mark
  end, {http://www.w3.org/2005/Atom}name, Mark
start, {http://www.w3.org/2005/Atom}uri, http://diveintomark.org/
  end, {http://www.w3.org/2005/Atom}uri, http://diveintomark.org/
  end, {http://www.w3.org/2005/Atom}author, 
      
start, {http://www.w3.org/2005/Atom}title, Accessibility is a harsh mistress
  end, {http://www.w3.org/2005/Atom}title, Accessibility is a harsh mistress
start, {http://www.w3.org/2005/Atom}link, None
  end, {http://www.w3.org/2005/Atom}link, None
start, {http://www.w3.org/2005/Atom}id, tag:diveintomark.org,2009-03-21:/archives/20090321200928
  end, {http://www.w3.org/2005/Atom}id, tag:diveintomark.org,2009-03-21:/archives/20090321200928
start, {http://www.w3.org/2005/Atom}updated, 2009-03-22T01:05:37Z
  end, {http://www.w3.org/2005/Atom}updated, 2009-03-22T01:05:37Z
start, {http://www.w3.org/2005/Atom}published, 2009-03-21T20:09:28Z
  end, {http://www.w3.org/2005/Atom}published, 2009-03-21T20:09:28Z
start, {http://www.w3.org/2005/Atom}category, None
  end, {http://www.w3.org/2005/Atom}category, None
start, {http://www.w3.org/2005/Atom}summary, The accessibility orthodoxy does not permit people to
      question the value of features that are rarely useful and rarely used.
  end, {http://www.w3.org/2005/Atom}summary, The accessibility orthodoxy does not permit people to
      question the value of features that are rarely useful and rarely used.
  end, {http://www.w3.org/2005/Atom}entry, 
    
start, {http://www.w3.org/2005/Atom}entry, 
    
start, {http://www.w3.org/2005/Atom}author, 
      
start, {http://www.w3.org/2005/Atom}name, Mark
  end, {http://www.w3.org/2005/Atom}name, Mark
  end, {http://www.w3.org/2005/Atom}author, 
      
start, {http://www.w3.org/2005/Atom}title, A gentle introduction to video encoding, part 1: container formats
  end, {http://www.w3.org/2005/Atom}title, A gentle introduction to video encoding, part 1: container formats
start, {http://www.w3.org/2005/Atom}link, None
  end, {http://www.w3.org/2005/Atom}link, None
start, {http://www.w3.org/2005/Atom}id, tag:diveintomark.org,2008-12-18:/archives/20081218155422
  end, {http://www.w3.org/2005/Atom}id, tag:diveintomark.org,2008-12-18:/archives/20081218155422
start, {http://www.w3.org/2005/Atom}updated, 2009-01-11T19:39:22Z
  end, {http://www.w3.org/2005/Atom}updated, 2009-01-11T19:39:22Z
start, {http://www.w3.org/2005/Atom}published, 2008-12-18T15:54:22Z
  end, {http://www.w3.org/2005/Atom}published, 2008-12-18T15:54:22Z
start, {http://www.w3.org/2005/Atom}category, None
  end, {http://www.w3.org/2005/Atom}category, None
start, {http://www.w3.org/2005/Atom}category, None
  end, {http://www.w3.org/2005/Atom}category, None
start, {http://www.w3.org/2005/Atom}category, None
  end, {http://www.w3.org/2005/Atom}category, None
start, {http://www.w3.org/2005/Atom}category, None
  end, {http://www.w3.org/2005/Atom}category, None
start, {http://www.w3.org/2005/Atom}category, None
  end, {http://www.w3.org/2005/Atom}category, None
start, {http://www.w3.org/2005/Atom}category, None
  end, {http://www.w3.org/2005/Atom}category, None
start, {http://www.w3.org/2005/Atom}category, None
  end, {http://www.w3.org/2005/Atom}category, None
start, {http://www.w3.org/2005/Atom}category, None
  end, {http://www.w3.org/2005/Atom}category, None
start, {http://www.w3.org/2005/Atom}summary, These notes will eventually become part of a
      tech talk on video encoding.
  end, {http://www.w3.org/2005/Atom}summary, These notes will eventually become part of a
      tech talk on video encoding.
  end, {http://www.w3.org/2005/Atom}entry, 
    
  end, {http://www.w3.org/2005/Atom}feed, 
  

Es importante recalcar que en el momento de recibir un evento de tipo start, puede que el texto, y los nodos hijos del elemento aún no hayan sido analizados. Solo el evento de tipo end garantiza que el elemento ha sido analizado por completo.

Sin embargo, este procedimiento va cargando datos de todos los elementos del árbol en memoria, para mantenerlos accesibles. Por lo tanto, si queremos realmente ahorrar memoria hay que ir borrando los elementos que ya no necesitemos, llamando al método clear() sobre dicho elemento. Esto borrará ese elemento y todos el subárbol que cuelga por debajo del mismo.

Ejemplo [5]:

elem.clear()
while elem.getprevious() is not None:
    del elem.getparent()[0]

5. Conclusiones

En este laboratorio hemos presentado los fundamentos para la descarga y extracción de datos de documentos XML en la Web. En particular, se han cubierto los siguientes aspectos:

  1. Descargar un fichero desde una URL.
  2. Configurar un parser para cargar el árbol del documento en memoria.
  3. Cómo recorrer el árbol y efectuar peraciones básicas de busqueda.
  4. Análizar un documento XML sin cargarlo por completo en memoria, mediante análisis dirigido por eventos.

En el próximo laboratorio, abordaremos la carga de datos representados con otros formatos populares: CSV y JSON.

6. Referencias

  1. Python XML processing with lxml. http://infohost.nmt.edu/tcc/help/pubs/pylxml/web/index.html#intro (último acceso Mar. 2015).
  2. Mark Pilgrim. Dive into Python 3. Chap. 12 XML. http://www.diveintopython3.net/xml.html (último acceso Mar. 2015).
  3. Russell, Matthew A. Mining the Social Web: Data Mining Facebook, Twitter, LinkedIn, Google+, GitHub, and More. 2nd Ed. O'Reilly Media, Inc., 2013.
  4. McKinney, Wes. Python for data analysis: Data wrangling with Pandas, NumPy, and IPython. O'Reilly Media, Inc., 2012.
  5. Liza Daly. High-performance XML parsing in Python with lxml. IBM developerWorks. http://www.ibm.com/developerworks/library/x-hiperfparse/ (último acceso Mar. 2015).

In [ ]: