Web Scraping con HTML Agility Pack

Hay veces que queremos procesar información que está disponible en internet, pero no está expuesta a través de ningún API que nos permita exportarla a un formato más manejable. En ese caso, una de las pocas alternativas que nos quedan es parsear el HTML y extraer de él la información que queremos.

Si tenemos mucha suerte, la web estará escrita en XHTML y podremos usar directamente las clases de System.Xml para procesar la información, pero lo más frecuente es que esto no sea así, por lo que tendremos que recurrir a un parser de HTML de verdad o, si queremos jugar con fuego, hacerlo al modo Cthulhu y usar expresiones regulares.

El mejor parser de HTML que conozco para .NET se llama HTML Agility Pack y tiene dos ventajas fundamentales:

  1. Es muy fácil de usar, con un API similar a XmlDocument e incluso soporte para Linq To Xml.
  2. Maneja bien el HTML mal formado que podemos encontrar en el mundo real, con sus etiquetas mal cerradas, mal anidadas, etc.

Vamos a ver un ejemplo sencillo de cómo usar esta librería.

Obteniendo los resultados de la Liga BBVA de Marca.com

En la web del diario Marca se puede acceder a bastantes estadísticas de la liga de fútbol española de los últimos años. Por ejemplo, podemos ver los resultados de la jornada 34 de la temporada 2008/2009.

Revisando el código fuente de la página, vemos que los resultados se presentan de esta forma:

<div class="resultados borde-caja">
   <h4 class="cintillodatos">Resultados</h4>
   <table>
       <tr onclick="DoNav('/estadisticas/futbol/primera/2000_01/jornada_15/espanyol_realmadrid/');">
           <td class="equipo-local"><a href=/estadisticas/futbol/primera/2000_01/espanyol/>Espanyol</a></td>
           <td class="resultado"><a href=/estadisticas/futbol/primera/2000_01/jornada_15/espanyol_realmadrid/>1-2</a></td>
           <td class="equipo-visitante"><a href=/estadisticas/futbol/primera/2000_01/realmadrid/>R. Madrid</a></td>
           <td class="ampliacion"><span>Ampliar</span></td>
       </tr>
       ...
   </table>
</div>

Algo bastante simple, una tabla en la que cada fila contiene el equipo local, el equipo visitante y el resultado del partido. Para poder convertir este HTML en información útil, necesitamos crear una instancia de HtmlDocument:

private HtmlDocument DownloadDocument(string season, int round)
{
    var url = string.Format("http://www.marca.com/estadisticas/futbol/primera/{0}/jornada_{1}", season, round);
    var html = new WebClient().DownloadString(url);

    var doc = new HtmlDocument();
    doc.LoadHtml(html);
    return doc;
}

Una vez que tenemos el HtmlDocument, podemos usar consultas XPath para acceder a las partes que necesitamos:

public IEnumerable<Match> GetMatches()
{
    var doc = DownloadDocument();

    return doc.DocumentNode
           .SelectNodes("//div[@class='resultados borde-caja']/table/tr")
           .Select(node => GetMatch(node));
}

private Match GetMatch(HtmlNode node)
{
    // El resultado tiene el formato goles_local - goles_visitante
    // Ejemplo: 1-3
    var scoreParts = node.SelectSingleNode("td[@class='resultado']").InnerText.Split('-');

    return new Match
    {
       HomeTeam = node.SelectSingleNode("td[@class='equipo-local']").InnerText,
       AwayTeam = node.SelectSingleNode("td[@class='equipo-visitante']").InnerText,
       HomeTeamScore = int.Parse(scoreParts[0]),
       AwayTeamScore = int.Parse(scoreParts[1]),
    }
}

El código es bastante sencillo, pero ganaría mucho si en lugar de utilizar XPath se pudieran usar selectores css, como se hace con jQuery.

Limitaciones

Si la web que contiene la información está generada dinámicamente con javascript (cosa que cada vez es más frecuente), este método no nos servirá de mucho, ya que en ningún momento estamos ejecutando el código javascript.

Para solventarlo, se puede usar el control WebBrowser para cargar la página y, una vez cargada, poder jugar con ella a través de la propiedad Document. Podéis ver un ejemplo de cómo hacer esto en este post de Javier Torrecilla. Es un método más lento y, desde mi punto de vista, más sucio, pero tiene la ventaja de que al renderizar la página se ejecutará el javascript que pueda tener y podremos acceder al contenido generado dinámicamente.

10 comentarios en “Web Scraping con HTML Agility Pack

  1. Nunca lo había oído, habrá que echarle un vistazo. ¡Gracias por la aportación!

  2. pregunton dijo:

    Web scrapping es un tema muy amplio, en codeproject había algún buen artículo. Me suena que en Java había alguna herramienta muy potente al respecto. Y scripts en Python o Ruby aparecen por github o gist para aspectos puntuales.

    No sé ahora cómo está el estado del arte en .NET respecto a web scrapping.

  3. Muy interesante este artículo! Llevo tiempo queriendo automatizar unas búsquedas sobre temas legales que se hacían en mi empresa y me he topado muchas veces con el problemas de las partes de código que se ejecutan por JavaScript! Echaré un vistazo al tuto de Javier Torrecilla a ver si no me explota la cabeza con mi poco conocimiento del tema. Un saludo!

  4. Hola Juan,

    Una buena alternativa que no conocía en ese momento para extraer información de webs que usen javascript, es emplear phantomjs, un navegador sin interfaz de usuario que puedes controlar desde javascript.

    Saludos.

  5. Gracias, lo he descargado y empezado a trastear un poco con él. La verdad es que aún no he conseguido ni lanzar uno de los ejemplos que trae incluidos jaja. Voy a leer algo sobre el tema por ahí. Un saludo.

Comentarios cerrados.