Selenium — это инструмент, предназначенный для запуска автоматизированных тестов в веб-приложениях. Он доступен на нескольких разных языках программирования.

Хотя это не его основная цель, Selenium также используется в Python для парсинга веб-страниц, поскольку он может получить доступ к контенту, отрисованному с помощью JavaScript (чего не могут сделать обычные инструменты парсинга, такие как BeautifulSoup).

Selenium также полезен, когда вам нужно как-то взаимодействовать со страницей перед сбором данных, например, нажимать кнопки или заполнять поля. Это вариант использования, который будет рассмотрен в этой статье.

Например, мы проанализируем investing.com, чтобы извлечь исторические данные об обменных курсах доллара по отношению к одной или нескольким валютам.

Если вы выполните поиск в Интернете, вы можете найти API-интерфейсы и пакеты Python, которые значительно упрощают сбор финансовых данных (вместо того, чтобы очищать их вручную). Однако идея здесь состоит в том, чтобы изучить, как Selenium может помочь вам в общем извлечении данных.

Веб-сайт, который мы собираемся спарсить

Во-первых, нам нужно понять веб-сайт. Этот сайт содержит исторические данные по обменному курсу доллара по отношению к евро.

На этой странице вы можете увидеть таблицу с данными и возможностью установить желаемый диапазон дат. Вот что мы собираемся использовать.

Чтобы просмотреть данные для других валют по отношению к доллару, просто замените «eur» на код другой валюты в URL-адресе.

Также предполагается, что вам нужен только обменный курс валюты по отношению к доллару. Если это не так, просто замените «usd» в URL-адресе.

Листинг:

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

Затем мы напишем функцию для очистки данных. Функция получит:

  • Список курсов валют
  • Дата начала
  • Дата окончания
  • Логическое значение, указывающее, хотим ли мы экспортировать данные в виде файла .csv. Я буду использовать значение по умолчанию False.

Кроме того, поскольку идея состоит в том, чтобы создать парсер, способный собирать данные о нескольких валютах, мы также инициализируем пустой список для хранения данных по каждой валюте.

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

Поскольку функция теперь имеет список валют, вы, вероятно, можете представить, что мы перебираем этот список и получаем валюту данных по валютам. План таков.

Итак, для каждой валюты в списке валют мы создадим URL-адрес, создадим экземпляр объекта драйвера и будем использовать его для получения страницы. Затем мы развернем окно, но оно будет видно только в том случае, если для параметра option.headless установлено значение False. В противном случае Selenium выполнит всю работу, ничего вам не показывая.

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

На этом этапе мы уже просматриваем исторические данные и можем просто получить таблицу с данными. Однако по умолчанию мы видим данные только за последние 20 дней. Мы хотим получить эти данные за любой период времени.

Для этого мы будем использовать некоторые интересные функции Selenium для взаимодействия с веб-сайтом.

Здесь мы будем нажимать на даты, заполнять поля Start Date и End Date желаемыми датами и нажимать Apply.

Для этого мы будем использовать WebDriverWait, ExpectedConditions и By, чтобы убедиться, что веб-драйвер будет ждать, пока элементы, с которыми мы хотим взаимодействовать, станут активными.

Это важно, потому что, если дайвер попытается взаимодействовать с чем-либо до того, как это станет доступным для кликов, возникнет исключение.

Время ожидания составит двадцать секунд, но вы можете установить его по своему усмотрению. Сначала выберите кнопку даты по ее Xpath, а затем щелкните по ней.

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

Теперь нам нужно заполнить поле Дата начала. Давайте сначала выберем его, а затем используем clear, чтобы удалить дату по умолчанию, и send_keys, чтобы заполнить ее желаемой датой.

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) 

А теперь повторяем процесс для поля 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)

Когда это будет сделано, мы выберем кнопку «Применить» и нажмем на нее. Затем мы используем режим сна, чтобы приостановить код на несколько секунд и убедиться, что новая страница полностью загружена.

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)

Если для параметра option.headless задано значение False, вы увидите, что весь этот процесс происходит перед вами, как если бы кто-то действительно нажимал на страницу. Когда Selenium нажимает «Применить», вы увидите, что таблица перезагружается и отображаются данные за указанный вами период времени.

Теперь мы используем функцию pandas.read_html, чтобы выбрать все таблицы на странице. Эта функция получит исходный код страницы. Наконец, мы можем выйти из драйвера.

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

Как обрабатывать исключения в Selenium

Процесс сбора данных завершен. Но мы должны учитывать, что Selenium иногда может быть немного нестабильным и в конечном итоге может не загрузить страницу в какой-то момент во время всех действий, которые мы здесь выполняем.

Чтобы предотвратить это, у нас будет весь код внутри предложения try, которое будет внутри бесконечного цикла. Как только Selenium удастся собрать данные, как я описал выше, цикл будет разорван. Но каждый раз, когда обнаруживается проблема, активируется условие ожидания.

В этом сценарии код будет:

  • Закройте драйвер — это всегда важно, чтобы у нас не было десятков работающих веб-драйверов, потребляющих много памяти.
  • Распечатать сообщение с указанием ошибки
  • Спи тридцать секунд
  • Перейти в начало цикла еще раз

Этот процесс будет повторяться до тех пор, пока данные для каждой валюты не будут правильно собраны. И вот код всего этого:

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

Но последний шаг. Если вы помните, у нас пока есть список, содержащий все таблицы на странице, хранящиеся как DataFrames. Нам нужно выбрать одну таблицу, содержащую нужные нам исторические данные.

Для каждого фрейма данных в этом списке фреймов данных мы проверим, соответствует ли имя его столбцов ожидаемому. Если да, то это наш фрейм, и мы разорвем цикл. И теперь мы, наконец, готовы добавить этот DataFrame в список, который был инициализирован в начале.

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

frames.append(df)

И да, если для параметра export_csv установлено значение True, нам нужно будет экспортировать файл .csv. Но это далеко не проблема, поскольку метод DataFrame.to_csv может легко это сделать.

И тогда мы можем просто завершить эту функцию, вернув список DataFrames. Этот последний шаг, конечно же, выполняется после того, как цикл по списку валют закончился.

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

# Outside of the loop
return frames

Вот и все! Вот полный код этих двух последних шагов вместе:

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

Следующие шаги и подведение итогов

Пока этот код получает исторические данные по обменному курсу списка валют по отношению к доллару и возвращает список DataFrames и несколько файлов .csv.

Но всегда есть возможности для улучшения. Имея еще несколько строк кода, несложно заставить функцию возвращать и экспортировать один DataFrame, содержащий данные для каждой валюты в списке.

Другое предложение — написать функцию обновления с использованием тех же функций Selenium, которые получают существующий фрейм данных и обновляют исторические данные до текущей даты.

Кроме того, та же самая логика, которая используется для очистки валют, может использоваться для очистки акций, индексов, товаров, фьючерсов и многого другого. Есть так много страниц, которые можно очистить.

Однако, если это цель, важно вставлять в код больше пауз, чтобы избежать перегрузки сервера. Вы также должны воспользоваться преимуществами прокси-провайдера, такого как Infatica, чтобы убедиться, что ваш код будет работать до тех пор, пока есть страницы, которые нужно очистить, и что вы и ваше соединение защищены.

Наконец, Selenium может быть полезен в нескольких других ситуациях, таких как вход на веб-сайты, заполнение форм, выбор элементов в раскрывающемся списке и многое другое. Конечно, это не единственное решение таких проблем, но оно определенно может быть полезно в зависимости от варианта использования.