45
Automatizando la extracción de datos con Selenium
Contenido
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í.
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
O, si usas conda
para administrar tus paquetes (como yo):
conda install -c conda-forge selenium
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.
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:
- Que acceda a la página "Otros indicadores de precios mensuales" del SIE de Banxico.
- Que desmarque todas las casillas, excepto la de "Indicador subyacente fundamental".
- Que despliegue el cuadro "Exportar series".
- Que seleccione como periodo inicial "Todo" y como periodo final "Todo".
- Que seleccione como formato de exportación "XLS" y guarde el archivo descargado.
Visualmente se vería de la siguiente forma:
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
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'
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)
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)
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")
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"]
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()
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()
Así hemos llegado a este punto:
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")
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()
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):
- El selector del periodo inicial es un elemento
<select>...</select>
con IDanoInicial
. - El selector del periodo final es un elemento
<select>...</select>
con IDanoFinal
. - 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")
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")
Para la fecha final también elegimos la opción cuyo texto es "Todo":
periodo_final.select_by_visible_text("Todo")
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()
Y, finalmente, damos clic en el botón para descargar nuestro archivo:
boton_xls.click()
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()
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()
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);
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.
45