Introducción

El presente artículo tiene dos objetivos: uno individual y otro colectivo. El objetivo individual es sistematizar en un solo documento algunos conocimientos que he adquirido sobre web scraping, acceso a APIs y minería de textos. El segundo objetivo es compartir este conocimiento con cualquier persona que lo requiera.

Este artículo no aspira a ser una investigación completa y exhaustiva, ni mucho menos metodológicamente coherente. Su finalidad es exponer algunas ideas de código para el seguimiento de discusiones políticas en entornos virtuales, como páginas de noticias, blogs y redes sociales.

Utilizaré herramientas de minería de datos, como la API de Google Trends y el paquete rvest, para recuperar información de varios medios digitales sobre un tema en particular, en este caso, sobre el anuncio de la posibilidad de llevar a referendo un conjunto de proyectos de ley. Solo me enfoco en la recuperación de texto y no en el análisis del mismo; eso lo abarcaré en una próxima publicación.

Métodos de recolección de información virtual: web scrapping y APIs

La información en Internet crece a un ritmo exponencial cada día. Mientras que en el siglo XX la ciencia política enfrentaba la falta de datos para probar sus hipótesis, en el siglo XXI el desafío es distinto: aunque la información es abundante y fácilmente accesible, es necesario saber cómo recopilarla y analizarla adecuadamente. La mayoría de estos datos están disponibles de manera no estructurada, por lo que el reto principal es extraerlos y organizarlos de manera adecuada (Urdinez y Cruz, 2021).

Scrapping con rvest

Lastimosamente los datos anteriores no nos permiten, más allá de patrones y tendencias, conocer en profundidad la orientación de las discusiones políticas entorno a un tema en particular, en este caso el anuncio del referendo. Una opción para profundizar en estas discusiones es analizar la producción de noticias y titulares que han generado los medios de comunicación en una coyuntura específica.

El paquete rvest en R es una herramienta poderosa para realizar web scraping. Fue creado por Hadley Wickham y está diseñado para facilitar la extracción de datos de sitios web. Cargamos la librería y veeamos que contiene:

library(rvest)
?rvest #profundizar documentación

A continuación un resumen de sus funciones principales:

  • read_html(): Lee y parsea HTML desde una URL o archivo.
  • html_nodes(): Selecciona nodos de un documento HTML usando selectores CSS.
  • html_node(): Similar a html_nodes(), pero selecciona solo el primer nodo que coincide.
  • html_text(): Extrae el texto de los nodos seleccionados.
  • html_attr(): Extrae atributos de los nodos seleccionados.
  • html_table(): Convierte tablas HTML en data frames.

Por suerte, no necesitamos ser expertos programadores en HTML para poder obtener información de este tipo de archivo. Típicamente, las páginas web por las que navegamos se escriben en este formato. Cada elemento con el que podemos interactuar en una página de internet está “anidado” como un elemento dentro de la misma página. Para obtener información, solo basta con conocer el nombre dentro del archivo HTML para extraerlo con las funciones de rvest.

Scrapping La Nación

Existen varias herramientas para navegar por los elementos de una página web. SelectorGadget es quizá la herramienta más sencilla y práctica para nuestros fines. Les dejo un tutorial de guía. Ciertamente no es la herramienta más potente; con el aumento de la ciberseguridad, algunas páginas se han vuelto con el tiempo menos amistosas para el web scraping. Así que no todos los métodos funcionan para todos los casos.

Como primer ejemplo sencillo, vamos a extraer algunos elementos de la sección de política en la página web del periódico La Nación.

# Guardamos el link
url <- "https://www.nacion.com/el-pais/politica/"

# Leer la página web como html 
web_nacion <- read_html(url)

# extraemos los títulos de las primeras 3 filas 
titulos1 <- web_nacion %>% 
  html_nodes(".lg-promo-headline") %>% 
  html_text()

## extrajo títulos repetidos, nos quedamos solo con las posiciones de filas unicas
titulos1 <- titulos1[c(1, 3, 5)]

En este caso en particular, por la forma en que fue programada la página los primeros 3 títulos se anidaron con un nombre distinto a los restantes, vamos a obtener los titulos que nos faltan, la descripción y la fecha:

# extraemos los títulos restantes 
titulos2 <- web_nacion %>% 
  html_nodes(".headline-text") %>% 
  html_text()

# extraemos la descripción 
descripcion <-  web_nacion %>% 
  html_nodes(".description-text") %>% 
  html_text()

# extraemos la fecha de las 3 primeras noticias
fechas1 <-  web_nacion %>% 
  html_nodes(".promo-date") %>% 
  html_text()
# extraemos las fechas del resto
fechas2 <-  web_nacion %>% 
  html_nodes(".story-date") %>% 
  html_text()


# almacenamos los titulares y las fechas en un solo marco de datos
titulos <- rbind(as_data_frame(titulos1), as_data_frame(titulos2))
## Warning: `as_data_frame()` was deprecated in tibble 2.0.0.
## ℹ Please use `as_tibble()` (with slightly different semantics) to convert to a
##   tibble, or `as.data.frame()` to convert to a data frame.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
fechas <- rbind(as_data_frame(fechas1), as_data_frame(fechas2))

descripcion <- as.tibble(descripcion)
## Warning: `as.tibble()` was deprecated in tibble 2.0.0.
## ℹ Please use `as_tibble()` instead.
## ℹ The signature and semantics have changed, see `?as_tibble`.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
titulos <- as.tibble(titulos)
fechas <- as.tibble(fechas)

Ya con estos elementos podemos generar un solo marco de datos que incluya toda la información de pudimos extraer:

datos_nacion <- bind_cols(titulos, fechas, descripcion) |> 
  rename(
    "titulos" = `value...1`,
    "fechas" = `value...2`,
    "descripcion" = `value...3`
  )

Funciones e Iteraciones

Todo lo anterior parece demasiado trabajo para haber extraido tan poca información, podemos programar una función para agilizar el proceso.

scrap_nacion <- function(url){
  # Leer la página web como html 
web_nacion <- read_html(url)

# extraemos los títulos de las primeras 3 filas 
titulos1 <- web_nacion %>% 
  html_nodes(".lg-promo-headline") %>% 
  html_text()

## extrajo títulos repetidos, nos quedamos solo con las posiciones de filas unicas
titulos1 <- titulos1[c(1, 3, 5)]

titulos2 <- web_nacion %>% 
  html_nodes(".headline-text") %>% 
  html_text()

# extraemos la descripción 
descripcion <-  web_nacion %>% 
  html_nodes(".description-text") %>% 
  html_text()

# extraemos la fecha de las 3 primeras noticias
fechas1 <-  web_nacion %>% 
  html_nodes(".promo-date") %>% 
  html_text()
# extraemos las fechas del resto
fechas2 <-  web_nacion %>% 
  html_nodes(".story-date") %>% 
  html_text()


# almacenamos los titulares y las fechas en un solo marco de datos
titulos <- rbind(as_data_frame(titulos1), as_data_frame(titulos2))
fechas <- rbind(as_data_frame(fechas1), as_data_frame(fechas2))

descripcion <- as.tibble(descripcion)
titulos <- as.tibble(titulos)
fechas <- as.tibble(fechas)

datos_nacion <- bind_cols(titulos, fechas, descripcion) |> 
  rename(
    "titulos" = `value...1`,
    "fechas" = `value...2`,
    "descripcion" = `value...3`
  )
  
  return(datos_nacion) # what returns/delivers the function
}

# Probamos la función
scrap_nacion(url = url)
## # A tibble: 8 × 3
##   titulos                                                     fechas descripcion
##   <chr>                                                       <chr>  <chr>      
## 1 "‘Voto de censura contra Anna Katharina Müller no prospera… 15 de… El fabrici…
## 2 "Nogui Acosta propondrá cambio a ley de eurobonos para emi… 15 de… Hacienda r…
## 3 "Presidencia confirma que otorgó declaratoria a Marcha de … 15 de… La declara…
## 4 "Rodrigo Arias pide a la población vacunarse contra la inf… 15 de… Rodrigo Ar…
## 5 "Defensoría: Incumplimiento salarial en MEP podría ser más… 15 de… Institució…
## 6 "Gabinete de Rodrigo Chaves acumula ocho salidas en el últ… 15 de… Gobierno d…
## 7 "Diputados fijarían un solo método para declarar ganancias… 15 de… Reforma so…
## 8 "Comisión legislativa rechaza proyecto para trasladar seis… 14 de… En la disc…

Nota: A la fecha, el autor no ha encontrado otra forma de hacer scrapping en esta página en particular, para obtener más volumen de datos con un menor gasto de recursos (tiempo y energía). Cualquier sugerencia es más que bienvenida.

Scrapping Semanario

Como señalé antes, existen algunas páginas más amigables con los métodos tradicionales para realizar scraping. Uno de esos sitios web es el Semanario Universidad, medio de comunicación de la Universidad de Costa Rica. Algunas estrategias más eficientes nos permiten capturar datos con un menor esfuerzo.

url2 <- "https://semanariouniversidad.com/pais/"

web_semanario <- read_html(url2)

titulos_semario <- web_semanario %>% 
  html_nodes("a") %>% 
  html_text()

## extrae más texto basura, limpiamos y nos quedamos solo con las filas de titulares
titulos_semario <- titulos_semario[seq(24, 60, by = 4)]

Ahora tambien podemos extraer las fechas y generar un marco de datos con esto:

fechas_semario <- web_semanario %>% 
  html_nodes(".updated") %>% 
  html_text()

# marco de datos
semanario <- tibble(
  "titulo" = titulos_semario,
  "fechas" = fechas_semario
)

Nuevamente, esto sigue siendo muy poca información con la cual trabajar. Notemos que, por la forma en que se diseñó este sitio web, cuando navegamos entre páginas, lo único que cambia en el enlace es el número que señala cada página. Por ejemplo: “https://semanariouniversidad.com/pais/page/2/”.

Para nuestros propósitos, esto es sumamente útil, ya que nos permitiría programar una iteración. Una iteración se refiere a una misma acción que se ejecuta repetidamente por n cantidad de veces. En el siguiente código, vamos a extraer de la página 2 a la 10 los elementos que nos interesan, unificarlos en un solo marco de datos y, además, unirlos con el primero que realizamos.

for(i in 2:10){
  print(i)
  www <- paste0("https://semanariouniversidad.com/pais/page/", i, "/")
  ultra <- read_html(www)
  # extraer titulo
  aux1 <- html_text(html_nodes(ultra, "a"))
  aux1 <- aux1[seq(24, 60, by = 4)]
  aux2 <- html_text(html_nodes(ultra, ".updated"))
  semanario2  <- tibble(
  "titulo" = aux1,
  "fechas" = aux2)
  semanario <- rbind(semanario, semanario2)
}

De acuerdo con lo esperado, obtuvimos un marco de datos con 100 observaciones que se refieren a los titulares del Semanario. Esto ya nos empieza a dar materia prima con la cual trabajar y realizar nuestros análisis. ¿Qué pasa si incrementamos el número a 200?

for(i in 2:200){
  print(i)
  www <- paste0("https://semanariouniversidad.com/pais/page/", i, "/")
  ultra <- read_html(www)
  # extraer titulo
  aux1 <- html_text(html_nodes(ultra, "a"))
  aux1 <- aux1[seq(24, 60, by = 4)]
  aux2 <- html_text(html_nodes(ultra, ".updated"))
  semanario2  <- tibble(
  "titulo" = aux1,
  "fechas" = aux2)
  semanario <- rbind(semanario, semanario2)
}

Ahora sí, tenemos más de 2000 mil titulares. (Se nos colaron algunos textos basura, lo que implica que el código podría mejorarse en un futuro.)

Scrapping AM prensa

¿Qué otros medios nos permiten obtener información de forma similar? El medio digital AM Prensa es otro buen lugar para practicar nuestras habilidades de web scraping. Después de estudiar la estructura de la página, obtenemos el nombre de los elementos que queremos extraer en formato HTML:

# leemos el link en formato html 
web_amprensa <- read_html("https://amprensa.com/category/nacionales/")

#obtenemos los titulos y limpiamos el texto básura
titulos_amprensa <- web_amprensa %>% 
  html_nodes("a") %>% 
  html_text()
titulos_amprensa <- titulos_amprensa[seq(75, 370, by = 5)]

#obtenemos las fechas y limpiamos el texto basura 
fechas_amprensa <- web_amprensa %>% 
  html_nodes(".td-module-date") %>% 
  html_text()
fechas_amprensa <- fechas_amprensa[(c(2:61))]

# armamos nuestro nuevo marco de datos
amprensa <- tibble(
  "titulo" = titulos_amprensa,
  "fechas" = fechas_amprensa
)

Como aprendimos antes, este proceso puede ser automatizado de la misma forma, añadiendo un par de ajustes:

for(i in 3:100){
  print(i)
  www <- paste0("https://amprensa.com/category/nacionales/page/", i, "/")
  ultra <- read_html(www)
  # extraer titulo
  aux1 <- html_text(html_nodes(ultra, "a"))
  aux1 <- aux1[seq(76, 371, by = 5)]
  aux1 <- aux1[(c(1:60))]
  aux2 <- html_text(html_nodes(ultra, ".td-module-date"))
  aux2 <- aux2[(c(2:61))]
  amprensa2  <- tibble(
  "titulo" = aux1,
  "fechas" = aux2)
  amprensa <- rbind(amprensa, amprensa2)
}

Con nuestras dos bases de datos, ahora podemos armar un dataset único con un gran volumen de titulares de noticias de dos fuentes de información, con su respectiva fecha.

datost <- bind_rows(semanario, amprensa)

A partir de acá, solo será necesario filtrar nuestros datos por los términos o palabras clave que nos interesen para nuestros objetivos de análisis. En este caso, lo relacionado al anuncio del referendo.

# quitamos mayusculas y tildes
library(stringi)
datost$titulo <-  tolower(stri_trans_general(str = datost$titulo, id = "Latin-ASCII"))

# filtramos los datos 
filtered_datos <- subset(datost, grepl("referendo", titulo, ignore.case = TRUE))
filtered_datos2 <- subset(datost, grepl("referendum", titulo, ignore.case = TRUE))
filtered_datos3 <- subset(datost, grepl("ley Jaguar", titulo, ignore.case = TRUE))
filtered_datos <- rbind(filtered_datos, filtered_datos2, filtered_datos3)

# eliminamos duplicados 
filtered_datos <- filtered_datos %>%
  distinct(titulo, .keep_all = TRUE)

Scrapping google news

Otra opción para conseguir textos de noticias de una variedad de fuentes es realizar una consulta directamente desde la sección especial de Google News. Esto se puede realizar rápidamente aplicando el procedimiento correcto. La debilidad de este método es que no nos permite obtener una cantidad de observaciones mayor a 100.

El código no es propiamente de mi autoría, solo se realizarón los ajustes para adaptarse a mis necesidades. Aquí pueden encontrar la fuente original

#Definir la consulta
query <- 'ley jaguar costa rica'

# Función para codificar caracteres especiales
encode_special_characters <- function(text) {
  encoded_text <- ''
  special_characters <- list('&' = '%26', '=' = '%3D', '+' = '%2B', ' ' = '%20')
  for (char in strsplit(text, '')[[1]]) {
    encoded_text <- paste0(encoded_text, ifelse(is.null(special_characters[[char]]), char, special_characters[[char]]))
  }
  return(tolower(encoded_text))
}
query2 <- encode_special_characters(query)


# Leer la página de Google News
html_dat <- read_html(paste0("https://news.google.com/search?for=", query2, "&hl=es-419&gl=US&ceid=US%3Aes-419"))

# Extraer enlaces de los artículos
dat <- data.frame(Link = html_dat %>% html_nodes("article") %>% html_node("a") %>% html_attr('href')) %>%
  mutate(Link = gsub("./articles/", "https://news.google.com/articles/", Link))

# Extraer texto de las noticias
news_text <- html_dat %>% html_nodes("article") %>% html_text2()
x <- strsplit(news_text, "\n")

# Crear un data frame con columnas relevantes
google_df <- data.frame(
  Title = sapply(x, function(item) item[3]),
  Source = sapply(x, function(item) item[1]),
  Time = sapply(x, function(item) item[4]),
  Author = gsub("By\\s+.*", "", sapply(x, function(item) item[5])),
  Link = dat$Link
)

Podemos gráficar los resultados que obtuvimos:

google_df |> 
  group_by(Source) |> 
  summarise(freq = n()) |> 
  slice_max(freq, n = 10) |> 
  ggplot(aes(y =  reorder(Source, freq), x = freq)) +
  geom_col(aes(fill = Source), show.legend = F)