Selenium es una herramienta diseñada para ayudarle a ejecutar pruebas automatizadas en aplicaciones web. Está disponible en varios lenguajes de programación.

Aunque no es su propósito principal, Selenium también se usa en Python para web scraping, porque puede acceder a contenido renderizado en JavaScript (lo que las herramientas de scraping normales como BeautifulSoup no pueden hacer).

Selenium también es útil cuando necesita interactuar con la página de alguna manera antes de recopilar los datos, como hacer clic en botones o completar campos. Este es el caso de uso que se cubrirá en este artículo.

Como ejemplo, haremos scraping a investing.com para extraer datos históricos de los tipos de cambio del dólar frente a una o más monedas.

Si busca en la web, puede encontrar APIs y paquetes de Python que facilitan mucho la recopilación de datos financieros (en lugar de hacer scraping manualmente). Sin embargo, la idea aquí es explorar cómo Selenium puede ayudarle con la extracción general de datos .

El sitio web que vamos a hacer scraping

En primer lugar, necesitamos entender el sitio web. Este sitio contiene los datos históricos del tipo de cambio del dólar frente al euro.

En esta página, puede ver una tabla con los datos y la opción de establecer el rango de fechas que queremos. Eso es lo que vamos a usar.

Para ver los datos de otras monedas frente al dólar, simplemente reemplace “eur” por el otro código de divisa en la URL.

Además, esto supone que solo querrá el tipo de cambio de la moneda frente al dólar. Si ese no es el caso, simplemente reemplace el “usd” en la URL.

El código scraper

Empezaremos con las importaciones, y no necesitamos mucho. Vamos a importar algunos elementos útiles de Selenium: la función sleep para insertar algunas pausas en el código, y Pandas para manipular la fecha cuando sea necesario.

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from time import sleep
import pandas as pd

A continuación, escribiremos una función para hacer scraping a los datos. La función recibirá:

  • Una lista de códigos de divisas
  • Una fecha de inicio
  • Una fecha de finalización
  • Un booleano que informa si queremos exportar los datos como un archivo .csv. Usaré False como predeterminado.

Además, como la idea aquí es construir un scraper capaz de recopilar datos sobre múltiples monedas, también inicializaremos una lista vacía para almacenar los datos de cada moneda.

def get_currencies(currencies, start, end, export_csv=False):
    frames = []

Como la función ahora tiene una lista de monedas, probablemente imaginará que iteraremos sobre esta lista y obtendremos los datos por moneda. Ese es precisamente el plan.

Entonces, para cada moneda en la lista de monedas, crearemos una URL, instanciaremos un objeto driver , y lo usaremos para obtener la página. Luego maximizaremos la ventana, pero eso solo es visible si mantiene option.headless como False. De lo contrario, Selenium hará todo el trabajo sin mostrarte nada.

for currency in currencies:
    my_url = f'https://br.investing.com/currencies/usd-{currency.lower()}-historical-data'
    option = Options()
    option.headless = False
    driver = webdriver.Chrome(options=option)
    driver.get(my_url)
    driver.maximize_window()

Ya estamos viendo los datos históricos en este momento, y podríamos obtener la tabla con los datos. Sin embargo, de forma predeterminada, solo vemos los datos de los últimos 20 días. Queremos obtener estos datos para cualquier período de tiempo.

Para ello, usaremos algunas funcionalidades interesantes de Selenium para interactuar con el sitio web. ¡Aquí es cuando Selenium brilla!

Lo que haremos aquí es hacer clic en las fechas y completar los campos Fecha de Inicio y Fecha de Finalización con las fechas que queremos y presionar Aplicar.

Para ello, usaremos WebDriverWait, ExpectedConditions, y By  para asegurarnos de que el controlador web esperará a que se pueda hacer clic en los elementos con los que queremos interactuar.

Esto es importante porque si el driver intenta interactuar con algo antes de que se pueda hacer clic, se generará una excepción.

El tiempo de espera será de veinte segundos, pero depende de ti configurarlo como considere apropiado. Primero, seleccionemos el botón de fecha por su XPath y luego hagamos clic en él.

date_button = WebDriverWait(driver, 20).until(
              EC.element_to_be_clickable((By.XPATH,
              "/html/body/div[5]/section/div[8]/div[3]/div/div[2]/span")))

date_button.click()

Ahora, necesitamos llenar el campo Fecha de Inicio. Primero seleccionémoslo y luego usemos clear para eliminar la fecha predeterminada y send_keys para llenarlo con la fecha que queremos.

start_bar = WebDriverWait(driver, 20).until(
            EC.element_to_be_clickable((By.XPATH, 
	        "/html/body/div[7]/div[1]/input[1]")))

start_bar.clear()
start_bar.send_keys(start) 

Y ahora repetimos el proceso para el campo Fecha de Finalización.

end_bar = WebDriverWait(driver, 20).until(
          EC.element_to_be_clickable((By.XPATH, 
          "/html/body/div[7]/div[1]/input[2]")))

end_bar.clear()
end_bar.send_keys(end)

Una vez hecho esto, seleccionaremos el botón Aplicar y haremos clic en él. Luego usamos sleep para pausar el código durante unos segundos y asegurarnos de que la nueva página esté completamente cargada.

apply_button = WebDriverWait(driver, 20).until(
	           EC.element_to_be_clickable((By.XPATH,  
               "/html/body/div[7]/div[5]/a")))

apply_button.click()
sleep(5)

Si tenías option.headless como False, verás todo este proceso sucediendo frente a ti como si alguien estuviera haciendo clic en la página. Cuando Selenium haga clic en Aplicar, verá que la tabla se recarga para mostrar los datos para el período de tiempo que especifico.

Ahora usamos la función pandas.read_html para seleccionar todas las tablas de la página. Esta función recibirá el código fuente de la página. Finalmente, podemos salir del controlador.

dataframes = pd.read_html(driver.page_source)
driver.quit()
print(f'{currency} scraped.')

Cómo manejar excepciones en Selenium

El proceso de recopilación de datos está hecho. Pero tenemos que considerar que Selenium a veces puede ser un poco inestable y eventualmente podría no cargar la página en algún momento durante todas las acciones que estamos realizando aquí.

Para evitar eso, tendremos el código completo dentro de una cláusula try que estará dentro de un bucle infinito. Una vez que Selenium logre recopilar los datos como describí anteriormente, el ciclo se romperá. Pero cada vez que encuentre un problema, se activará una cláusula de expect.

En este escenario, el código:

  • Salir del controlador – siempre es importante hacer esto para que no terminemos con docenas de controladores web que consumen memoria en ejecución.
  • Imprime un mensaje indicando el error
  • Dormir por treinta segundos
  • Ir al inicio del bucle una vez más

Este proceso se repetirá hasta que los datos de cada moneda se recopilen correctamente. Y este es el código para todo esto:

 for currency in currencies:
        while True:
            try:
                # Opening the connection and grabbing the page
                my_url = f'https://br.investing.com/currencies/usd-{currency.lower()}-historical-data'
                option = Options()
                option.headless = False
                driver = webdriver.Chrome(options=option)
                driver.get(my_url)
                driver.maximize_window()
                   
                # Clicking on the date button
                date_button = WebDriverWait(driver, 20).until(
                            EC.element_to_be_clickable((By.XPATH,
                            "/html/body/div[5]/section/div[8]/div[3]/div/div[2]/span")))
                
                date_button.click()
                
                # Sending the start date
                start_bar = WebDriverWait(driver, 20).until(
                            EC.element_to_be_clickable((By.XPATH,
                            "/html/body/div[7]/div[1]/input[1]")))
                            
                start_bar.clear()
                start_bar.send_keys(start)

                # Sending the end date
                end_bar = WebDriverWait(driver, 20).until(
                            EC.element_to_be_clickable((By.XPATH,
                            "/html/body/div[7]/div[1]/input[2]")))
                            
                end_bar.clear()
                end_bar.send_keys(end)
               
                # Clicking on the apply button
                apply_button = WebDriverWait(driver,20).until(
                		EC.element_to_be_clickable((By.XPATH,
                		"/html/body/div[7]/div[5]/a")))
                
                apply_button.click()
                sleep(5)
                
                # Getting the tables on the page and quiting
                dataframes = pd.read_html(driver.page_source)
                driver.quit()
                print(f'{currency} scraped.')
                break
            
            except:
                driver.quit()
                print(f'Failed to scrape {currency}. Trying again in 30 seconds.')
                sleep(30)
                continue

Un último paso, sin embargo. Si recuerda, lo que tenemos hasta ahora es una lista que contiene todas las tablas de la página almacenadas como DataFrames. Necesitamos seleccionar la única tabla que contiene los datos históricos que queremos.

Para cada DataFrame en esta lista de marcos de datos, verificaremos si el nombre de sus columnas coincide con lo que esperamos. Si lo hacen, entonces ese es nuestro marco y rompemos el bucle. Y ahora finalmente estamos listos para agregar este DataFrame a la lista que se inicializó al principio.

for dataframe in dataframes:
    if dataframe.columns.tolist() == ['Date', 'Price', 'Open', 'High', 'Low', 'Change%']:
        df = dataframe
        break

frames.append(df)

Y sí, si el parámetro export_csv se estableció en True, necesitaríamos exportar un archivo .csv. Pero eso está lejos de ser un problema, ya que el método DataFrame.to_csv puede hacer esto fácilmente.

Y luego podemos terminar esta función devolviendo la lista de DataFrames. Este último paso está hecho después de que el bucle a través de la lista de monedas ha terminado, por supuesto.

if export_csv:
        df.to_csv('currency.csv', index=False)
        print(f'{currency}.csv exported.')

# Outside of the loop
return frames

¡Y eso es todo! Aquí está el código completo para estos dos últimos pasos combinados:

		# Selecting the correct table            
        for dataframe in dataframes:
            if dataframe.columns.tolist() == ['Date', 'Price', 'Open', 'High', 'Low', 'Change%']:
                df = dataframe
                break
        frames.append(df)

        # Exporting the .csv file
        if export_csv:
            df.to_csv('currency.csv', index=False)
            print(f'{currency}.csv exported.')
                  
  return frames

Próximos pasos y conclusión

Hasta ahora, este código obtiene los datos históricos del tipo de cambio de una lista de monedas frente al dólar y devuelve una lista de DataFrames y varios archivos .csv.

Pero siempre hay margen de mejora. Con algunas líneas más de código, no es difícil hacer que la función devuelva y exporte un único DataFrame que contenga los datos de cada moneda en la lista.

Otra sugerencia es escribir una función update utilizando las mismas funcionalidades de Selenium que recibe un dataframe existente y actualizar los datos históricos a la fecha actual.

Además, la misma lógica utilizada para hacer scraping las monedas se puede utilizar para hacer scraping acciones, índices, materias primas, futuros y mucho más. Hay tantas páginas que puedes hacer scraping.

Sin embargo, si ese es el objetivo, entonces es importante insertar más pausas en el código para evitar sobrecargar el servidor. También debes aprovechar un proveedor de proxy, como Infatica, para asegurarte de que tu código seguirá funcionando mientras queden páginas por hacer scraping y que tú y tu conexión estén protegidos.

Finalmente, Selenium puede ser útil en varias otras situaciones, como iniciar sesión en sitios web, completar formularios, seleccionar elementos en una lista desplegable y mucho más. Por supuesto, no es la única solución para estos problemas, pero definitivamente puede ser útil según el caso de uso.

Espero que hayas disfrutado este artículo y te haya ayudado. Si tiene alguna pregunta o sugerencia, no dude en ponerse en contacto.

Traducido del artículo de Otávio Simões Silveira - How to Code a Scraping Bot with Selenium and Python