6  Web scraping y manipulación básica de texto

En esta sección vamos a realizar un pequeño desvío para tratar dos temas auxiliares pero muy importantes en la práctica. Lo hacemos precisamente ahora porque contamos con los conocimientos sufiencientes para sacarles partido y porque, además, nos serán útiles más adelante: el rascado de páginas web (web scraping) y la manipulación básica de texto.

6.1 Web scraping

En ocasiones interesa descargar datos directamente de páginas de internet recorriendo una, varias o, incluso, muchas de ellas. A eso, a falta de un nombre de consenso en español (¿rascado?), se lo denomina web scraping.

Para descargar datos de páginas web usaremos el paquete rvest.

library(rvest)

Con él podemos descargar, por ejemplo, la lista de países ordenada según su PIB (PPA) per cápita:

my_url <- "https://es.wikipedia.org/wiki/Anexo:Pa%C3%ADses_por_PIB_(PPA)_per_c%C3%A1pita"
tmp <- read_html(my_url)
tmp <- html_nodes(tmp, "table")

La segunda línea descarga y preprocesa una página descargada de internet. El objeto tmp contiene una representación interna de la página. Una página web, en el fondo, no es otra cosa que un árbol del que penden nodos que son párrafos, imágenes, enlaces, tablas, etc. Sobre este árbol se pueden realizar distintos tipos de consultas, i.e., extraer distintos tipos de nodos.

La función html_nodes captura los nodos que tienen determinadas características. En este ejemplo, como ocurre con mucha frecuencia, nos interesan los identificados como tablas (tables). De hecho, las tablas son tan interesantes que el paquete rvest proporciona una función auxiliar para convertir los nodos de tipo table en tablas de R: html_table.

La página contiene varias tablas. Como consecuencia, tmp es una lista de nodos:

length(tmp)
[1] 4
sapply(tmp, class)
[1] "xml_node" "xml_node" "xml_node" "xml_node"

Aparentemente, si se visita la página, parecería que tiene solo dos tablas. Sin embargo, nuestro código ha encontrado cuatro. Eso se debe a que en HTML las tablas se utilizan en ocasiones, por abuso, no para almacenar datos tabulares sino para dar formato a las páginas. Sin embargo, es fácil detectar estas seudotablas por inspección. Para identificar la tabla de interés, la que contiene la lista de países, podemos examinarlas todas ejecutando la función html_table sobre tmp[[1]], tmp[[2]], etc. hasta dar con la de interés: es la tercera.

Alternativamente, para evitar tener que examinar las tablas una a una se puede hacer

sapply(tmp, function(x) dim(html_table(x, fill = TRUE)))
     [,1] [,2] [,3] [,4]
[1,]    1  393  195  194
[2,]    2 1566    4    4

que nos indica que la tercer tabla tiene 195 filas, un indicio sólido de que es la que va a contener la lista de países buscada.

Podemos entonces transformar este último nodo en una tabla de R:

paises_por_pib <- html_table(tmp[[3]])

Ejercicio 6.1 Inspecciona la tabla recién cargada. Presta atención al tipo de las columnas. ¿Observas algo raro?

La información colgada en internet está pensada para ser consumida por humanos, no máquinas. Como consecuencia, los números están decorados con separadores de miles, unidades, porcentajes, etc.; los nombres de columnas tienen espacios y otros caracteres extraños, etc. Por eso, la información directamente descargada mediante técnicas de rascado raramente se puede utilizar directamente: es necesario someterla aun proceso sencillo pero laborioso de limpieza.

Ejercicio 6.2 Dales nombres razonables a las columnas de paises_por_pib. Nota: usa colnames.

Es habitual tener que cambiar los nombres de columnas: utilizar nombres con caracteres no estándar es garantía de problemas en los análisis subsiguientes. También es habitual tener que manipular los valores de esas columnas; es frecuente que, por culpa de los separadores de miles, el uso de la coma como separador de los decimales o el uso de unidades, R no reconozca las columnas numéricas como tales y que las trate como texto. Entonces no queda otro remedio que tratar esas columnas para darles el formato correcto y convertirlas en numéricas usando as.numeric. De ahí la sección siguiente, que es una introducción a la manipulación básica de texto con R.

Además de las tablas, hay otros tipos de nodos que puede interesar extraer de una página web. La función html_nodes permite hacerlo utilizando XPath. Puedes echarle un vistazo a esta página, donde se muestra un ejemplo de web scraping en el que se descarga información no contenida en tablas de una serie de páginas.

Ejercicio 6.3 Examina la documentación del paquete rvest y busca aplicaciones a sus funciones.

6.2 Manipulación básica de texto

Las tablas bajadas de internet (y datos procedentes de otras fuentes) exigen frecuentemente un proceso de limpieza de datos. Por ejemplo, es típico que, al importar datos, las columnas que contienen números se interpreten como cadenas de caracteres por incluir símbolos no estándar como separadores de miles, etc. O tener que reinterpretar determinados campos como, p.e., fechas.

La función gsub se usa muy a menudo para dicha limpieza de datos. Una llamada a gsub tiene la forma

gsub("h", "H", c("hola", "búho"))
[1] "Hola" "búHo"

donde el primer argumento, "h" es una expresión regular; la función gsub modifica las ocurrencias de esta expresión regular por el segundo argumento, "H" en este caso. El tercer argumento es un vector que contiene cadenas de texto en las que se realiza la sustitución.

Las expresiones regulares son muy útiles para manipular texto. Conviene aprender algunas de las más frecuentes, como por ejemplo, las que identifican caracteres que aparecen al principio de un texto,

gsub("^h", "H", c("hola", "búho"))
[1] "Hola" "búho"

o al final del mismo,

gsub("o$", "os", c("hola", "búho"))
[1] "hola"  "búhos"

Una función emparentada con gsub es grep, que busca cadenas en las que aparece una determinada expresión regular:

grep("^h", c("hola", "búho"))
[1] 1

La salida de la expresión anterior nos indica que el patrón cadena de texto que comienza con la letra h aparece solo en la posición número 1 del vector.

Ejercicio 6.4 colors() es una función que devuelve el nombre de más de 600 colores en R. Usándolo,

  • encuentra quellos cuyo nombre contenga un número (posiblemente tengas que investigar cómo se expresa cualquier número como expresión regular)
  • encuentra aquellos que comiencen con yellow
  • encuentra aquellos que contengan blue
  • reemplaza los números por x (p.e., blue10 quedaría como bluexx)
  • reemplaza secuencias de números por x (por ejemplo, blue10 quedaría como bluex)

Ejercicio 6.5 Los números que aparecen en la tabla descargada en la sección anterior (y contenidos en paises_por_pib) no tienen formato numérico. Para convertirlos en números de verdad, transfórmalos adecuadamente. En particular,

  • Elimina los espacios usados como separadores de miles.
  • Convierte el texto a número

El ejercicio anterior es relativamente sencillo comparado con el proceso de limpieza de datos en tablas descargadas de la web. En particular, si aparecen columnas con valores como 12.345,67, hay que realizar operaciones tales como:

  • Usar gsub para cambiar “.” por “” (i.e., nada) en las columnas de interés teniendo en cuenta que . es el comodín de las expresiones regulares y que el punto es \\..
  • Usar gsub para cambiar , por . en las columnas de interés.
  • Finalmente, usar as.numeric para cambiar texto resultante por valores numéricos.

Ejercicio 6.6 Toma la tabla

dat <- data.frame(
    a = 1:3,
    b = c("20,34", "1.345,42", "1.234.5678,90"))

y transforma la segunda columna en una función numérica.

Si, además, aparecen fechas, es necesario usar as.Date para transformar el texto en un objeto específico para fechas.

Otra función muy útil para procesar texto es paste, que tiene un comportamiento distinto según se use con el argumento sep o collapse.

paste("A", 1:6, sep = ",")
[1] "A,1" "A,2" "A,3" "A,4" "A,5" "A,6"
paste("Hoy es ", date(), " y tengo clase de R", sep = "")
[1] "Hoy es Fri Feb 10 18:19:08 2023 y tengo clase de R"
paste("A", 1:6, collapse = ",")
[1] "A 1,A 2,A 3,A 4,A 5,A 6"

sep y collapse pueden combinarse:

paste("A", 1:6, sep = "_", collapse = ",")
[1] "A_1,A_2,A_3,A_4,A_5,A_6"

Para la operación inversa, la de partir cadenas de texto, se usa la función strsplit:

strsplit("Hoy es martes", split = " ")
[[1]]
[1] "Hoy"    "es"     "martes"
strsplit(c("hoy es martes", "mañana es miércoles"), split = " ")
[[1]]
[1] "hoy"    "es"     "martes"

[[2]]
[1] "mañana"    "es"        "miércoles"

Advierte que esta función devuelve una lista de cadenas de texto (¿podría ser de otra manera?).

Ejercicio 6.7 Crea una función que tome los nombres de ficheros

ficheros <- c("ventas_20160522_zaragoza.csv",
               "pedidos-firmes_20160422_soria.csv")

y genere una tabla con una fila por fichero y tres columnas: el nombre del fichero, la fecha y y la provincia. Nota: puedes crear una función que procese solo un nombre de fichero y aplicársela convenientemente al vector de nombres.

Esas son las funciones fundamentales para la manipulación básica de texto en R. Existen otras que es encuentran también en otros lenguajes de programación (p.e., sprintf, substr, etc.) u otros paquetes, como separate y unite del paquete tidyr1. No obstante, con las descritas en esta sección, se cubren la mayor parte de las necesidades de manipulación de datos corrientes.

6.3 JSON y XML

Existe una web para humanos (el mundo del HTML) y otra para máquinas. Se puede bajar información programáticamente de las primeras usando las técnicas de rascado (o scraping) discutidas más arriba y después limpiarla para su postproceso como hemos hecho en la sección anterior.

Pero cada vez son más populares los servicios web que proporcionan información a través de APIs que son consultadas directamente por ordenadores. Estos servicios suelen proporcionar resultados en formato JSON, XML o ambos. Los dos formatos son similares: organizan la información en forma de árbol.

Por ejemplo, el INE proporciona un API JSON del que se puede bajar información de interés estadístico. Tiene, además, un servicio que permite construir la consulta, i.e., obtener la URL con la que consultar una serie de datos en concreto. Usándola, encontramos que para obtener la población de cada provincia española por sexos durante los últimos cinco años tenemos que consultar esta.

Pero podemos realizar la consulta programáticamente así:

library(rjson)

pob <- readLines("http://servicios.ine.es/wstempus/js/ES/DATOS_TABLA/2852?nult=5&tip=AM")
pob <- paste(pob, collapse = " ")
pob <- fromJSON(pob)

En la primera línea cargamos el paquete necesario, rjson. En las dos siguientes leemos la URL y colapsamos todas las líneas en una única cadena de texto, una exigencia de fromJSON. Esta es la función que llamamos en última instancia para convertir el fichero JSON en una estructura arborescente en R, i.e., una lista que contiene, a su vez, otras listas.

class(pob)
[1] "list"
length(pob)
[1] 159

pob tiene longitud 159. Son tres elementos por cada provincia (más Ceuta, Melilla y el total nacional), los correspondientes a los dos sexos y el total. Cada uno de estos elementos tiene una serie de atributos y una sublista de longitud 5, que almacena los datos anuales. Así,

pob[[89]]$Data[[5]]$Valor
[1] 161775

es el valor (i.e, la población) correspondiente al quinto periodo (o año) del elemento 85 de la primera lista, i.e.,

pob[[89]]$Nombre
[1] "Lugo. Hombres. Total habitantes. Personas. "

Idealmente, querríamos convertir nuestros datos (o la parte más relevante de ellos) en una tabla para su posproceso. Podremos hacerlo más adelante, cuando aprendamos más sobre manipulación de datos y programación en R.

Otras APIs proporcionan información en formato XML. Por ejemplo, la del Banco Mundial2:

library(xml2)

bm  <- read_xml("http://api.worldbank.org/countries/all/indicators/NY.GDP.MKTP.CD?date=2009:2010&per_page=500&page=1")
esp <- xml_find_all(bm, "//*/wb:data[wb:country[@id='ES']]/wb:value")
as.numeric(xml_text(esp))
[1] 1.422108e+12 1.491473e+12

El código anterior proporciona el PIB español de los años 2009 y 2010 en dólares. La primera línea carga el paquete xml2, que es el que vamos a usar para leer ficheros XML. La función read_xml lee una URL de la API del Banco Mundial que extrae el indicador NY.GDP.MKTP.CD (PIB según la documentación de la API) y la procesa. A diferencia de lo que ocurría con JSON, el objeto bm no es una lista de R (aunque la podemos convertir en una usando la función as_list), sino un objeto de la clase xml_document. Sobre este documento se pueden realizar consultas usando XPath, como más arriba, y obtener los valores corespondientes al país con etiqueta ES, es decir, España.

El objeto esp contiene dos nodos y la función xml_text permite extraer su contenido (en formato de texto, que tenemos que convertir en números). En general, los nodos tienen atributos y el texto es uno más de ellos.

6.4 Resumen y referencias

Hemos aprendido técnicas para descargar información disponible en internet en varios formatos. Para el más joven de todos ellos, JSON, hemos usado el paquete rjson. Este paquete tiene dos funciones: la que hemos usado, fromJSON, para leer ficheros JSON en R (y convertirlos en listas de listas de listas…) y toJSON, para realizar el proceso inverso.

Existen otros paquetes similares en R, como RJSONIO o jsonlite muy similares a rjson. Curiosamente, los tres paquetes contienen las dos mismas funciones, fromJSON y toJSON, con pequeñas diferencias de implementación entre ellos. La mayor diferencia entre jsonlite y el resto es que su función fromJSON proporciona la opción de simplificar la lista que produce en algunos casos para construir una tabla u otras estructuras de datos cuando detecta que es posible. Esto puede ser ventajoso determinadas circunstancias.

También se pueden descargar datos de ficheros XML o HTML. Tradicionalmente, esto se hacía con el paquete XML de R, que aprovechaba el hecho de que HTML es un dialecto3 de XML. XML, sin embargo, era algo complicado. Así que con el tiempo apareció una alternativa, xml2, un tanto más amigable. En el fondo, tanto XML como xml2 son interfaces de R para una librería desarrollada en C, libxml2. El paquete que hemos usado para rascar páginas, rvest, es a su vez, una interfaz para el paquete subyacente, xml2, adaptada al web scraping.

XML y xml2 (y, como consecuencia, rvest) hacen uso de XPath, un lenguaje de consulta para documentos XML (de la misma manera que SQL es un lenguaje de consulta para datos relacionales). Es un lenguaje que pocos dominan con soltura y que, casi seguro, solo vas a necesitar esporádicamente. Es convieniente que sepas que existe y cuatro cosas básicas sobre él. Es fácil encontrar en internet ejemplos de consultas sencillas que pueden modificarse para un fin determinado. Lo más típico suele ser la extracción de valores encerrados en etiquetas div o span con determinados atributos class o id.

Una alternativa moderna a algunos de esos paquetes es httr, una implementación en R del protocolo HTTP que permite hacer llamadas a determinados servicios web con un mayor control de las operaciones a bajo nivel.

Finalmente, acerca de la manipulación de texto —que es un campo, por supuesto, mucho más extenso que el contexto con el que lo hemos motivado, i.e., el arreglo de datos rascados de internet— es conveniente hacer estas dos recomendaciones:

  • Aprende bien los fundamentos de las expresiones regulares. Son muy útiles y se emplean en muchos más lenguajes de programación.
  • Que aunque las funciones discutidas en este capítulo son suficientes para un uso esporádico, si vas a estar trabajando seriamente con texto, deberías familiarizarte con el paquete stringr, que contiene una selección amplia de funciones avanzadas de manipulación de texto con una interfaz homogénea y moderna.

6.5 Ejercicios adicionales

Ejercicio 6.8 Toma el vector c("41°39'00''N","0°53'00''O") y conviértelo en coordenadas decimales. Recuerda que también tienes que asignar el signo (es menos para latitudes en el hemisferio sur y al oeste del meridiano de Greenwich).


  1. Véase esto↩︎

  2. Que también la ofrece en JSON.↩︎

  3. En sentido amplio, por supuesto.↩︎