DEV Community

Cover image for Automatizando la extracción de datos con Selenium
Rodrigo Leo
Rodrigo Leo

Posted on

Automatizando la extracción de datos con Selenium

Contenido

  1. Los requisitos
  2. El guión
  3. El script
  4. Poniendo todo junto
  5. Haciéndolo fácil: usando la API

Hace unos días necesitaba automatizar la extracción de cierta serie de tiempo del Sistema de Información Económica (SIE) del Banco de México y no me di cuenta que tienen una API diseñada específicamente para eso, por lo que escribí un script de Python para descargar datos usando Selenium.

Cuando vi que existía la API en cuestión terminé usándola (con Google Apps Script, que por cierto es muy bueno), pero para que las horas invertidas con Python no se vayan a la basura, aquí comparto esta pequeña guía sobre cómo usar Selenium, por si a alguien le es de utilidad alguna vez.

Pero antes de empezar, ¿qué es Selenium? Básicamente es un entorno de pruebas (testing) de software diseñado para aplicaciones web. En palabras sencillas, es una especie de "robot" que nos permite automatizar tareas rutinarias, y aunque se usa principalmente para hacer pruebas que resultarían tediosas manualmente, también se emplea para diseñar sistemas de automatización, que es lo que haremos aquí.

1. Los requisitos

Lo primero que necesitamos (evidentemente) es Python instalado en la computadora. Además, necesitamos instalar Selenium. Con pip es muy sencillo hacerlo:

pip install selenium
Enter fullscreen mode Exit fullscreen mode

O, si usas conda para administrar tus paquetes (como yo):

conda install -c conda-forge selenium 
Enter fullscreen mode Exit fullscreen mode

Una vez que tenemos Selenium instalado, es necesario descargar un driver: Selenium simula la interacción con una página web que nosotros haríamos a través de un navegador, por lo que tenemos que proporcionarle tal navegador: eso es el driver. El ejecutable del driver debe estar en el PATH del sistema o bien en el mismo directorio que nuestro script de Python, de lo contrario obtendremos un error del tipo: selenium.common.exceptions.WebDriverException: Message: ‘X’ executable needs to be in PATH, donde X es el driver en cuestión.

Aquí los links para descargar drivers (yo uso el de Google Chrome):

Una vez que el driver esté en el directorio del script o bien en la variable PATH, estamos listos para comenzar a construir nuestro script.

2. El guión

Primero que nada, es necesario tener muy claro qué es lo que queremos que haga nuestro programa. En mi caso, quiero que haga lo siguiente:

  1. Que acceda a la página "Otros indicadores de precios mensuales" del SIE de Banxico.
  2. Que desmarque todas las casillas, excepto la de "Indicador subyacente fundamental".
  3. Que despliegue el cuadro "Exportar series".
  4. Que seleccione como periodo inicial "Todo" y como periodo final "Todo".
  5. Que seleccione como formato de exportación "XLS" y guarde el archivo descargado.

Visualmente se vería de la siguiente forma:

Proceso animado de descarga

3. El script

Primero que nada importamos las librerías de Python necesarias:

  • webdriver que es la clase fundamental para interactuar con Selenium;
  • Select que permite que Selenium trabaje eficientemente con elementos HTML de tipo <select>...</select>;
  • sleep, que requeriremos por las características de la página con la que trabajaremos (más sobre esto adelante); y
  • Path para obtener el directorio de trabajo del script.
from selenium import webdriver
from selenium.webdriver.support.ui import Select
from time import sleep
from pathlib import Path
Enter fullscreen mode Exit fullscreen mode

A continuación almacenamos el directorio de trabajo actual en la variable cwd, y el URL de la página a la que el driver debe dirigirse en la variable url:

cwd = str(Path().resolve())
url = 'https://www.banxico.org.mx/SieInternet/consultarDirectorioInternetAction.do?sector=8&accion=consultarCuadro&idCuadro=CP194&locale=es'
Enter fullscreen mode Exit fullscreen mode

Guardamos como preferencia del driver que el directorio para almacenar las descargas sea el directorio del script (el que está en la variable cwd):

opciones = webdriver.ChromeOptions()
prefs = {'download.default_directory': cwd}
opciones.add_experimental_option('prefs', prefs)
Enter fullscreen mode Exit fullscreen mode

Y con eso podemos finalmente arrancar el driver. Al ejecutar el siguiente código se abrirá una ventana nueva de Google Chrome y se dirigirá a la URL almacenada en la variable url:

driver = webdriver.Chrome(options = opciones)
driver.get(url)
Enter fullscreen mode Exit fullscreen mode

Ahora obtenemos todos los elementos <input>...</input> de la página. Los guardaremos en la variable inputs. La forma de encontrarlos es mediante el método find_elements_by_tag_name():

inputs = driver.find_elements_by_tag_name("input")
Enter fullscreen mode Exit fullscreen mode

A continuación los filtramos para quedarnos únicamente con aquellos cuyo tipo (type) es checkbox. Para eso usamos el método get_attribute() (pues el tipo es un atributo de la etiqueta HTML; existe un método análogo para obtener propiedades, se llama get_property()):

checkboxes = [x for x in inputs if x.get_attribute("type") == "checkbox"]
Enter fullscreen mode Exit fullscreen mode

Ahora iteramos sobre la lista checkboxes para asegurarnos de que todos queden sin seleccionar. Para ello usamos el método is_selected(), que devuelve True si el elemento está seleccionado y False en caso contrario. En caso de que esté seleccionado, invocamos el método click() para anular la selección:

for checkbox in checkboxes:
    if checkbox.is_selected():
        checkbox.click()
Enter fullscreen mode Exit fullscreen mode

Lo que queremos es que únicamente el checkbox de la serie "Indicador subyacente fundamental" quede seleccionado. Usando el inspector de elementos de Chrome, nos damos cuenta que dicho elemento tiene el ID nodo_0_1_0_SP74825. Así, podemos usar el método find_element_by_id() para encontrarlo y darle click:

checkbox_target = driver.find_element_by_id("nodo_0_1_0_SP74825")
checkbox_target.click()
Enter fullscreen mode Exit fullscreen mode

Así hemos llegado a este punto:

Todos los elementos sin seleccionar

Ahora necesitamos abrir la caja de opciones de "Exportar series". Nuevamente empleamos el inspector de Chrome para descubrir que el ID del botón es consultaSeriesCarritoToggleAcc. Lo guardaremos en la variable botón_expandir:

boton_expandir = driver.find_element_by_id("consultaSeriesCarritoToggleAcc")
Enter fullscreen mode Exit fullscreen mode

Para verificar si está abierto o no, usamos una propiedad llamada aria-expanded. Si su valor es "true", quiere decir que la caja de opciones está abierta, en caso contrario es "false". Evidentemente en este caso está cerrada, pero por seguridad incluiremos esta verificación extra en nuestro código:

if boton_expandir.get_property("aria-expanded") == "false":
    boton_expandir.click()
Enter fullscreen mode Exit fullscreen mode

Listo, la caja de opciones para exportar las series está abierta. Ahora identificamos los elementos que necesitaremos manipular dentro de esta caja (para obtener los IDs seguimos usando el inspector de Chrome):

Caja de opciones de descarga de series

  • El selector del periodo inicial es un elemento <select>...</select> con ID anoInicial.
  • El selector del periodo final es un elemento <select>...</select> con ID anoFinal.
  • El botón para seleccionar el formato de exportación de datos tiene ID seleccionFormato.
  • El elemento del menú desplegable que se abre al dar clic en el botón para seleccionar formato que corresponde al archivo XLS tiene ID seleccionFormatoXLS.

Vamos a asociar a diferentes variables cada uno de estos elementos para poder manipularlos. En el caso de los elementos <select>...</select> usaremos la clase Select que importamos al inicio:

periodo_inicial = Select(driver.find_element_by_id("anoInicial"))
periodo_final = Select(driver.find_element_by_id("anoFinal"))
boton_formato = driver.find_element_by_id("seleccionFormato")
boton_xls = driver.find_element_by_id("seleccionFormatoXLS")
Enter fullscreen mode Exit fullscreen mode

Ahora elegimos como fecha inicial "Todo". Una de las ventajas de usar la clase Select es que permite buscar los elementos <option> del tag por ID, por valor o por el texto mostrado. En nuestro caso buscaremos por texto mostrado. Para ello usamos el método select_by_visible_text():

periodo_inicial.select_by_visible_text("Todo")
Enter fullscreen mode Exit fullscreen mode

Para la fecha final también elegimos la opción cuyo texto es "Todo":

periodo_final.select_by_visible_text("Todo")
Enter fullscreen mode Exit fullscreen mode

A continuación verificamos que el menú desplegable de los diferentes formatos de exportación esté cerrado, en cuyo caso lo abrimos. Para ello volvemos a verificar la propiedad aria-expanded:

if boton_formato.get_property("aria-expanded") == "false":
    boton_formato.click()
Enter fullscreen mode Exit fullscreen mode

Y, finalmente, damos clic en el botón para descargar nuestro archivo:

boton_xls.click()
Enter fullscreen mode Exit fullscreen mode

En este punto el driver de Google Chrome debe descargar el archivo de Excel con las características que seleccionamos en el mismo directorio donde se encuentra nuestro script de Python. Solamente resta cerrar el driver:

driver.quit()
Enter fullscreen mode Exit fullscreen mode

4. Poniendo todo junto

Si hemos ejecutado los comandos anteriores uno a uno, de forma pausada, no debimos tener ningún problema en llegar hasta el paso final. Pero si ponemos todos los comandos juntos en un archivo .py y lo ejecutamos, probablemente nos encontremos con errores que nos indican que ciertos elementos (como los selectores de periodos o el botón de descargar como XLS) no están disponibles o no se encuentran. Esto es debido a una particularidad de la página del SIE: tiene animaciones. Al dar clic para expandir la caja de opciones para descargar las series, ésta no se muestra de inmediato, sino que se despliega lentamente, de arriba hacia abajo, como una persiana. Por lo tanto, como la velocidad de ejecución del script de Python es más rápida que la animación, busca los elementos dentro de la caja de opciones antes de que estén completamente cargados en el DOM de la página.

Para resolver este inconveniente es que importamos la función sleep() al inicio. Esta función pausa la ejecución del programa durante el número de segundos indicado, y así nos permite garantizar que al momento de avanzar, los elementos del DOM estén completamente cargados y el script no devuelva errores.

Así es como se ve la implementación completa de nuestro script de automatización, con las llamadas a sleep() en lugares estratégicos:

# Importamos las librerías necesarias
from selenium import webdriver
from selenium.webdriver.support.ui import Select
from time import sleep
from pathlib import Path

# Guardamos el directorio del script y la URL a la que queremos que se dirija el driver
cwd = str(Path().resolve())
url = 'https://www.banxico.org.mx/SieInternet/consultarDirectorioInternetAction.do?sector=8&accion=consultarCuadro&idCuadro=CP194&locale=es'

# Establecemos como directorio de descarga el mismo que el del script
opciones = webdriver.ChromeOptions()
prefs = {'download.default_directory': cwd}
opciones.add_experimental_option('prefs', prefs)

# Lanzamos el driver
driver = webdriver.Chrome(options = opciones)
driver.get(url)

# Buscamos todos los elementos <input>...
inputs = driver.find_elements_by_tag_name("input")

# ... y los filtramos para quedarnos sólo con los que son de tipo "checkbox"
checkboxes = [x for x in inputs if x.get_attribute("type") == "checkbox"]

# Nos aseguramos de que ninguno esté seleccionado
for checkbox in checkboxes:
    if checkbox.is_selected():
        checkbox.click()

# Elegimos el checkbox de la serie que queremos y nos aseguramos que sí esté seleccionado
checkbox_target = driver.find_element_by_id("nodo_0_1_0_SP74825")
checkbox_target.click()

# Identificamos el botón para expandir la caja de opciones de descarga de las series
boton_expandir = driver.find_element_by_id("consultaSeriesCarritoToggleAcc")

# Si la caja no está abierta, la abrimos dándole clic
if boton_expandir.get_property("aria-expanded") == "false":
    boton_expandir.click()

# Aquí conviene pausar el script, pues la caja de opciones tarda en abrirse debido a la animación
sleep(2)

# Identificamos los controles a manipular dentro de la caja
periodo_inicial = Select(driver.find_element_by_id("anoInicial"))
periodo_final = Select(driver.find_element_by_id("anoFinal"))
boton_formato = driver.find_element_by_id("seleccionFormato")
boton_xls = driver.find_element_by_id("seleccionFormatoXLS")

# Elegimos los periodos inicial y final
# Ambos con la opción que dice "Todos"
periodo_inicial.select_by_visible_text("Todo")
periodo_final.select_by_visible_text("Todo")

# Desplegamos la lista de opciones de formatos de descarga
if boton_formato.get_property("aria-expanded") == "false":
    boton_formato.click()

# Nuevamente pausamos un par de segundos para dar tiempo a la animación
sleep(2)

# Damos clic en el botón que corresponde a "XLS"
boton_xls.click()

# Cerramos el driver
driver.quit()
Enter fullscreen mode Exit fullscreen mode

5. Haciéndolo fácil: usando la API

Como dije al inicio, el SIE dispone de una API para facilitar la automatización de la descarga de datos, por lo que no es realmente necesario usar Selenium. Solamente como una muestra, aquí está el código de Google Apps Script para obtener los mismos datos:

var url = 'https://www.banxico.org.mx/SieAPIRest/service/v1/series/SP74825/datos/?token=' + token;
var respuesta = UrlFetchApp(url, {'muteHtpExceptions': true});
var respuestaTexto = respuesta.getContentText();
var respuestaJson = JSON.parse(respuestaTexto);
Enter fullscreen mode Exit fullscreen mode

Donde la variable token es un string que contiene el token provisto por Banxico para acceder a su API. Como se puede ver, el camino es mucho más corto y al final se obtienen los mismos datos en formato JSON. Desgraciadamente, muchos sitios no cuentan con APIs o con vínculos permanentes que faciliten el acceso programado a los datos, por lo que el conocimiento de herramientas como Selenium se vuelve esencial para cualquiera que desee automatizar flujos de trabajo que involucren el acceso a recursos web.

Top comments (0)