Lo que he aprendido: scraping con Julia

Tengo la intuición de que los artículos de los periódicos son cada vez más sencillos, de que se usan frases más cortas y palabras más comunes. Es posible que se deba a que yo me culturizo con la edad y que, por lo tanto, no tengo la misma percepción de la complejidad que tenía hace unos años. Como cuando veo Saber y ganar y me sé las respuestas, bueno, las es igual exagerar un poco. O puede ser que ocurra como con Willy Fogg, que hizo que una generación entera se criara oyendo a un león en traje decir “detrás de usted, estimado caballero” o frases del estilo, mientras que los dibujos de ahora hablan más en la línea de la chavalada.

Como navego entre la fina línea que separa la cordura de la sinrazón, he pensado que podría analizar diferentes periódicos en diferentes épocas y ver si mi hipótesis (ha evolucionado de intuición a hipótesis ¿habéis visto?) es correcta. Soy una científica, qué leches. Y de ahí el título de esta entrada: cuando una quiere analizar textos, necesita primero conseguirlos y qué mejor manera que usar una técnica que no puede estar más de moda.

Antes de contar qué he hecho va un disclaimer: esto es el resultado de un par de tardes de investigación por parte de alguien que no sabe programar en general y menos en Julia en particular pero que, imbuida por el espíritu hacker, se ha puesto a darle a todos los botones hasta que ha conseguido algo que más o menos funciona. Así funciona la ingeniería, hermanos.

Lo que he entendido sobre el scrapeo

Veamos, si yo me he empanado de algo, scrapear consiste en crear un robot que navegue por una web como si fuera un humano y se descargue algún tipo de dato para luego analizarlo. Para ello, básicamente hay que localizar en el código fuente de la página el elemento en cuestión para decirle a nuestro robot a dónde debe ir y qué debe filtrar.

En mi caso, fui a la hemeroteca de El País para un día determinado, localicé la url de las noticias de ese día y, para cada una de ellas, descargué el texto. Todo ello lo hice con Julia, que se note que estoy aprendiendo.

Scrapear con Julia

Utilicé los siguiente paquetes:

  • HTTP: nos da funcionalidad cliente/servidor HTTP, lo usé para descargar el HTML de la página.
  • Gumbo: un parser HTML, convierte el HTML en un árbol formado por nodos y elementos.
  • Cascadia: un selector CSS, permite filtrar el output de Gumbo por clase o tipo de elemento, por ejemplo.
  • AbstractTrees: sirve para manejar datos de tipo árbol, útil para pasear por los árboles que genera Gumbo.

Seguramente se podría hacer con otros, pero los ejemplos que fusilé y remezclé utilizaban estos, así que para qué complicarse. Los instalé con pkg desde la terminal de Julia.

El proceso tuvo dos partes: localizar las noticias y extraer el texto de cada una de ellas.

Localizar las noticias

Para localizar las noticias, fui a la hemeroteca, elegí una fecha y me fijé en que la url a la que me mandaba era https://elpais.com/tag/fecha/AAAAMMDD donde AAAAMMDD es la fecha con los cuatro dígitos del año (AAAA), seguido de los dos dígitos del mes (MM) y los dos del día (DD).

Una vez ahí, vi que cada una de las noticias estaba dentro de un elemento cuya clase era articulo-titulo y que contenía la dirección de la noticia en cuestión. Así, si conseguía extraer la url de cada noticia, podría hacer que mi robot fuera a esa dirección y descargase el texto.

Hice lo siguiente:

date = 19851010 # Elegir una fecha
url = "https://elpais.com/tag/fecha/"*date # Montar la url
res = HTTP.get(url) # Descargar HTML
body = String(res.body) # Convertir a texto
html = parsehtml(body) # Parsear HTML
articles = eachmatch(sel".articulo-titulo", html.root); # Seleccionar los elementos que contienen las noticias

# Para cada noticia
for f in PreOrderDFS(articles) # Pasea por los elementos
   if f isa HTMLElement{:a} # Si es un enlace
       url = getattr(f,"href") # Extrae la url
       # TODO: extraer texto de cada noticia
   end
end

Faltaba extraer el texto de cada noticia.

Extraer el texto

Despues, fui a varias noticias y vi que el texto de la noticia en sí estaba dentro de un elemento de clase articulo-cuerpo. Ese elemento estaba formado por otros elementos que estaban formados por otros hasta llegar a los elementos HTMLText que contenían el texto. Solo tenía que pasearme por las hojas (los elementos del árbol que no tienen a su vez elementos) extraer su texto y empalmar los trozos, saltando una línea en el caso de que el texto viniese de un párrafo, sin saltar si no.

Escribí este código y lo introduje en el TODO anterior:

html = parsed(url) # Parsear HTML
content = eachmatch(sel".articulo-cuerpo", html.root) # Seleccionar cuerpo
texto = "" # Inicialización
for elem in Leaves(content) # Pasear por las hojas
   if elem isa HTMLText # Si el elemento es texto
   # Si el texto viene de un párrafo saltar línea
       if elem.parent isa HTMLElement{:p}
           texto = texto*text(elem)*"\n"
       else
           texto = texto*text(elem) # Unir a lo anterior
       end
    end
end

Juntando todo, extrayendo el código repetido en un par de funciones y metiendo los textos de las noticias en un array, me quedó:

using HTTP, Gumbo, Cascadia, AbstractTrees

"""
    getText(HTMLNode)
Extrae texto de objeto HTMLNode
"""
function getText(content)
texto = ""
for elem in Leaves(content)
    if elem isa HTMLText
        # Si el texto viene de un párrafo saltar línea
        if elem.parent isa HTMLElement{:p}
           texto = texto*text(elem)*"\n"
        else
           texto = texto*text(elem)
        end
    end
end
return texto
end

"""
   parsed(url)
Devuelve HTML parseado a partir de URL
"""
function parsed(url)
res = HTTP.get(url)
body = String(res.body)
html = parsehtml(body)
return html
end

"""
   scrapElPais(date)
Descarga las noticias de El País para una fecha dada como AAAAMMDD
"""
function scrapElPais(date)
url = "https://elpais.com/tag/fecha/"*date
html = parsed(url)
articles = eachmatch(sel".articulo-titulo", html.root);
# Inicializar matriz que contendrá textos
raw = String[]
for f in PreOrderDFS(articles)
    if f isa HTMLElement{:a}
       url = getattr(f,"href")
       html = parsed(url)
       content = eachmatch(sel".articulo-cuerpo", html.root)
       articleText = getText(content)
       append!(raw, [articleText])
   end
end
return raw
end
################# MAIN #######################
date = "19760504" # Fecha de las noticias
news = scrapElPais(date)

Y así es cómo se scrapea un periódico sin saber scrapear ni programar ni entender cómo funciona la web. Evidentemente, esto se puede afinar, lo iré haciendo y os contaré. Ahora voy a tratar los datos: contaré el número de palabras total, por párrafo y por frase y miraré cuántas palabras aparecen en la lista de palabras más comunes. Será divertido.

Por cierto, WordPress me revienta la indentación y no tiene resaltador de sintaxis para Julia (estoy usando la de Python), para algo más decente tenéis el snippet de Gitlab.

Referencias

Julia: Introduction to Web Scraping (PHIVOLCS’ Seismic Events)

Web scraping with Julia

Web scraping en diferentes lenguajes en Rosetta Code


Os dejo con rap en galego:

2 pensamientos en “Lo que he aprendido: scraping con Julia

  1. Giorgio Grappa

    Estás analizando textos estadísticamente?

    Bueno, a ver si lo he entendido: todo lo que explicas aquí se refiere a como has automatizado la descarga de textos para una fecha concreta de una fuente concreta (mediante un guión en Julia?), pero el análisis de los textos extraídos sería el siguiente paso. Es así? Yo quiero aprender! Eso mola!

    Responder
    1. Ondiz Autor de la entrada

      Quiero analizar textos estadísticamente, exacto.

      He puesto un ejemplo de cómo descargarme los textos en un caso, tengo variaciones para otros medios.

      Ahora me toca tratar el texto, leer sobre el tema y pintar unos gráficos, os mantengo informados.

      Responder

¡Opina sin miedo! (Puedes usar Markdown)

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s