Automatizando la extracción de datos con Selenium
Tags: python, spanish
Published: 2021-11-10 19:06:14

**Contenido**
1. [Los requisitos](#chapter-1)
1. [El guión](#chapter-2)
1. [El script](#chapter-3)
1. [Poniendo todo junto](#chapter-4)
1. [Haciéndolo fácil: usando la API](#chapter-5)

---

Hace unos días necesitaba automatizar la extracción de cierta serie de tiempo del [Sistema de Información Económica](https://www.banxico.org.mx/SieInternet/) (SIE) del Banco de México y no me di cuenta que tienen una [API diseñada específicamente para eso](https://www.banxico.org.mx/SieAPIRest/service/v1/;jsessionid=73ad9a988670d19d6424e3944775), 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 <a name="chapter-1"></a>

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):

| Driver  | Link |
| ------- | ---- |
| Chrome  | https://sites.google.com/chromium.org/driver/ |
| Edge    | https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/ |
| Firefox | https://github.com/mozilla/geckodriver/releases |
| Safari  | https://webkit.org/blog/6900/webdriver-support-in-safari-10/ |

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 <a name="chapter-2"></a>

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](https://www.banxico.org.mx/SieInternet/consultarDirectorioInternetAction.do?sector=8&accion=consultarCuadro&idCuadro=CP194&locale=es
)" del SIE de Banxico.
1. Que desmarque todas las casillas, excepto la de "Indicador subyacente fundamental".
1. Que despliegue el cuadro "Exportar series".
1. Que seleccione como periodo inicial "Todo" y como periodo final "Todo".
1. 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](https://i.imgur.com/DY7M3qg.gif)


## 3. El script <a name="chapter-3"></a>

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.

```python
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`:

```python
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`):

```python
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`:

```python
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()`:

```python
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()`):

```python
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:

```python
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:

```python
checkbox_target = driver.find_element_by_id("nodo_0_1_0_SP74825")
checkbox_target.click()
```

Así hemos llegado a este punto:

![Todos los elementos sin seleccionar](https://i.imgur.com/P7mpavG.png)

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`:

```python
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:

```python
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):

![Caja de opciones de descarga de series](https://i.imgur.com/80FC13B.png)

- 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:

```python
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()`:

```python
periodo_inicial.select_by_visible_text("Todo")
```

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

```python
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`:

```python
if boton_formato.get_property("aria-expanded") == "false":
    boton_formato.click()
```

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

```python
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:

```python
driver.quit()
```


## 4. Poniendo todo junto <a name="chapter-4"></a>

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:

```python
# 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()
```


## 5. Haciéndolo fácil: usando la API <a name="chapter-5"></a>

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:

```javascript
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.