Скрапинг с помощью Python и Selenium

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

Еще один случай использования Selenium для скрапинга возникает, когда необходимо взаимодействовать с веб-страницей перед сбором данных, например, кликать по кнопкам или заполнять поля. Этот случай использования и будет рассмотрен в данной статье. В качестве примера мы будем парсить investing.com для извлечения исторических данных о курсах обмена доллара на одну или несколько валют.

Скачивайте книги ТОЛЬКО на русском языке у нас в телеграм канале: PythonBooksRU

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

Скрапинг

Для начала необходимо понять, что это за сайт. Следующий URL ведет к историческим данным по курсу доллара по отношению к евро:

https://investing.com/currencies/usd-eur-historical-data

На этой странице можно увидеть таблицу с данными и возможность задать нужный нам диапазон дат. Этим мы и воспользуемся. Чтобы посмотреть данные по другим валютам по отношению к доллару, просто замените в URL “eur” на код другой валюты.

Кроме того, в данном случае предполагается, что вам нужен только курс валюты по отношению к доллару. Если это не так, просто замените “usd” в URL.

Код

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

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 для взаимодействия с сайтом. Вот тут-то 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()

Теперь необходимо заполнить поле Start Date. Сначала выделим его, затем с помощью 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 и щелкнем на ней. Затем с помощью функции sleep мы приостановим выполнение кода на несколько секунд и убедимся, что новая страница полностью загружена.

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

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

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

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

Обработка исключений

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

Чтобы предотвратить неожиданное завершение программы, мы поместим весь код в блок try, который будет находиться внутри бесконечного цикла. После того как Selenium выполнит все описанные выше процессы сбора данных, цикл будет прерван, но каждый раз, когда будет происходить ошибка парсинга, будет выполняться блок except. В этом сценарии код будет выполнять следующие действия:

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

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

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 = WebDriverWait(driver, 20).until(EC.element_to_be_clickable((By.XPATH, "/html/body/div[7]/div[5]/a")))
            apply.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

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

Для каждого DataFrame в списке 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() легко справится с этой задачей. А затем мы можем просто завершить эту функцию, вернув список DataFrame. Этот последний шаг выполняется после завершения цикла по списку валют.

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

# Outside of the loop
    return frames

Вот и все! Вот полный код для всего, что мы только что сделали:

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

def get_currencies(currencies, start, end, export_csv=False):
    frames = []
    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 = WebDriverWait(driver, 20).until(EC.element_to_be_clickable((By.XPATH,
                                                        "/html/body/div[7]/div[5]/a")))
                apply.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
                
        # 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

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

На данный момент мы написали код, который позволяет получить исторические данные о курсе обмена валют по отношению к доллару и возвращает список датафреймов или несколько csv-файлов по вашему запросу.

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

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

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

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

Надеюсь, статья вам понравилась, и, возможно, окажется полезной. Если у вас есть вопросы или предложения, не стесняйтесь, обращайтесь.

Перевод статьи «Writing a Scraping Bot with Python and Selenium».

2 комментария к “Скрапинг с помощью Python и Selenium”

  1. Пингбэк: Библиотека urllib в Python

  2. Пингбэк: Как заработать на Python: превращаем код в золото

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *