Веб-парсинг с использованием Selenium и Python

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

\r\n\r\n

Selenium API использует протокол WebDriver для управления веб-браузерами, такими как Chrome, Firefox или Safari. Selenium может управлять как локально установленным экземпляром браузера, так и запущенным на удаленной машине по сети.

\r\n\r\n

Изначально (а это уже около 20 лет!) Selenium предназначался для кросс-браузерного сквозного тестирования (приемочных тестов). Однако со временем он стал использоваться в основном как платформа для автоматизации работы с браузерами общего назначения (например, для снятия скриншотов), что, конечно же, включает в себя задачи веб-парсинга и. Редко что может быть лучше в "общении" с веб-сайтом, чем настоящий, правильный браузер, не так ли?

\r\n\r\n

Selenium предоставляет широкий спектр способов взаимодействия с сайтами, таких как:

\r\n\r\n
    \r\n\t
  • нажатие кнопок
  • \r\n\t
  • заполнение форм данными
  • \r\n\t
  • прокрутка страницы
  • \r\n\t
  • создание скриншотов
  • \r\n\t
  • выполнение собственного, пользовательского JavaScript-кода.
  • \r\n
\r\n\r\n

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

\r\n\r\n

Установка

\r\n\r\n

Хотя Selenium поддерживает несколько браузерных движков, для примера мы будем использовать Chrome, поэтому убедитесь, что у вас установлены следующие пакеты:

\r\n\r\n
    \r\n\t
  • Страница загрузки Chrome
  • \r\n\t
  • Двоичный драйвер ChromeDriver, соответствующий вашей версии Chrome
  • \r\n\t
  • Пакет Selenium Python Binding.
  • \r\n
\r\n\r\n

Для установки пакета Selenium, как обычно, я рекомендую создать виртуальное окружение (например, с помощью virtualenv), а затем:

\r\n\r\n
\r\n

pip install selenium

\r\n
\r\n\r\n

\"\"

\r\n\r\n

Быстрый запуск

\r\n\r\n

После загрузки Chrome и ChromeDriver и установки пакета Selenium вы должны быть готовы к запуску браузера:

\r\n\r\n
\r\n

from selenium import webdriver

\r\n\r\n

DRIVER_PATH = '\/path\/to\/chromedriver'
\r\ndriver = webdriver.Chrome(executable_path=DRIVER_PATH)
\r\ndriver.get('https:\/\/google.com')

\r\n
\r\n\r\n

Так как мы не настраивали режим headless, то на экране появится обычное окно Chrome с дополнительным сообщением о том, что Chrome управляется Selenium.

\r\n\r\n

Безголовый режим Chrome

\r\n\r\n

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

\r\n\r\n

В этом режиме Selenium будет запускать Chrome в "фоновом режиме" без каких-либо визуальных выводов или окон. Представьте себе рабочий сервер, на котором одновременно работает несколько экземпляров Chrome со всеми открытыми окнами. Что ж, серверы, как правило, пренебрегают тем, насколько "внимательны" люди к их пользовательскому интерфейсу - бедняжки, - но если серьезно, то нет смысла тратить ресурсы графического интерфейса без причины.

\r\n\r\n

К счастью, включение безголового режима требует всего нескольких флагов.

\r\n\r\n
\r\n

from selenium import webdriver
\r\nfrom selenium.webdriver.chrome.options import Options

\r\n\r\n

options = Options()
\r\noptions.headless = True
\r\noptions.add_argument("--window-size=1920,1200")

\r\n\r\n

driver = webdriver.Chrome(options=options, executable_path=DRIVER_PATH)

\r\n
\r\n\r\n

Нам осталось только инстанцировать объект Options, установить для его поля headless значение True и передать его в конструктор WebDriver. Готово.

\r\n\r\n

Свойства страницы WebDriver

\r\n\r\n

Развивая наш пример с режимом headless, давайте превратимся в настоящего Марио и заглянем на сайт Nintendo.

\r\n\r\n
\r\n

from selenium import webdriver
\r\nfrom selenium.webdriver.chrome.options import Options

\r\n\r\n

options = Options()
\r\noptions.headless = True
\r\noptions.add_argument("--window-size=1920,1200")

\r\n\r\n

driver = webdriver.Chrome(options=options, executable_path=DRIVER_PATH)
\r\ndriver.get("https:\/\/www.nintendo.com\/")
\r\nprint(driver.page_source)
\r\ndriver.quit()

\r\n
\r\n\r\n

При запуске этого скрипта вы получите несколько отладочных сообщений, связанных с браузером, и, в конце концов, HTML-код сайта nintendo.com. Это связано с тем, что наш вызов print обращается к полю page_source драйвера, которое содержит тот самый HTML-документ сайта, который мы запрашивали последним.

\r\n\r\n

Еще два интересных поля WebDriver:

\r\n\r\n
    \r\n\t
  • driver.title - для получения заголовка страницы
  • \r\n\t
  • driver.current_url - для получения текущего URL (это может быть полезно, когда на сайте происходят перенаправления и нужен конечный URL)
  • \r\n
\r\n\r\n

Определение местоположения элементов

\r\n\r\n

Для того чтобы соскрести\/извлечь данные, необходимо знать, где они находятся. Поэтому определение местоположения элементов сайта является одной из ключевых особенностей веб-парсинга. Естественно, Selenium предоставляет такую возможность из коробки (например, в тестовых сценариях необходимо убедиться в наличии\/отсутствии определенного элемента на странице).

\r\n\r\n

Существует достаточно много стандартных способов поиска определенного элемента на странице. Например, можно

\r\n\r\n
    \r\n\t
  • поиск по имени тега
  • \r\n\t
  • фильтр по определенному классу HTML или идентификатору HTML
  • \r\n\t
  • либо использовать CSS-селекторы или выражения XPath
  • \r\n
\r\n\r\n

Как обычно, самый простой способ найти элемент - открыть инструменты разработчика Chrome и проинспектировать нужный элемент. Удобное сокращение для этого - выделить нужный элемент мышью, а затем нажать Ctrl + Shift + C или на macOS Cmd + Shift + C вместо того, чтобы каждый раз щелкать правой кнопкой мыши и выбирать Inspect.

\r\n\r\n

Найдя элемент в дереве DOM, можно определить, каким способом лучше всего программно обратиться к этому элементу. Например, можно щелкнуть правой кнопкой мыши на элементе в инспекторе и скопировать его абсолютное XPath-выражение или CSS-селектор.

\r\n\r\n

\"\"

\r\n\r\n

Методы find_element

\r\n\r\n

WebDriver предоставляет два основных метода для поиска элементов.

\r\n\r\n
    \r\n\t
  • find_element
  • \r\n\t
  • find_elements
  • \r\n
\r\n\r\n

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

\r\n\r\n

Оба метода поддерживают восемь различных типов поиска, обозначаемых классом By.с

\r\n\r\n

Примеры find_element
\r\nДопустим, у нас есть следующий HTML-документ .....

\r\n\r\n
\r\n

<html>
\r\n    <head>
\r\n        ... some stuff
\r\n    <\/head>
\r\n    <body>
\r\n        <h1 class="someclass" id="greatID">Super title<\/h1>
\r\n    <\/body>
\r\n<\/html>

\r\n
\r\n\r\n

.... и мы хотим выбрать элемент <h1>. В данном случае следующие пять примеров будут идентичны по возвращаемой информации

\r\n\r\n
\r\n

h1 = driver.find_element(By.NAME, 'h1')
\r\nh1 = driver.find_element(By.CLASS_NAME, 'someclass')
\r\nh1 = driver.find_element(By.XPATH, '\/\/h1')
\r\nh1 = driver.find_element(By.XPATH, '\/html\/body\/h1')
\r\nh1 = driver.find_element(By.ID, 'greatID')

\r\n
\r\n\r\n

Другим примером может быть выборка всех тегов якорей\/ссылки на странице. Поскольку нам нужно несколько элементов, мы используем здесь find_elements (обратите внимание на множественное число)

\r\n\r\n
\r\n

all_links = driver.find_elements(By.TAG_NAME, 'a')

\r\n
\r\n\r\n

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

\r\n\r\n

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

\r\n\r\n

Selenium WebElement

\r\n\r\n

WebElement - это объект Selenium, представляющий элемент HTML.

\r\n\r\n

Существует множество действий, которые можно выполнять с этими объектами, вот наиболее полезные из них:

\r\n\r\n
    \r\n\t
  • Доступ к тексту элемента с помощью свойства element.text
  • \r\n\t
  • Щелчок по элементу с помощью element.click()
  • \r\n\t
  • Доступ к атрибуту с помощью element.get_attribute('class')
  • \r\n\t
  • Отправка текста на вход с помощью element.send_keys('mypassword').
  • \r\n
\r\n\r\n

Есть и другие интересные методы, например is_displayed(). Он возвращает значение True, если элемент виден пользователю, и может оказаться полезным для предотвращения "медовых ботов" (например, намеренно скрытых элементов ввода). Honeypots - это механизмы, используемые владельцами сайтов для обнаружения ботов. Например, если HTML-элемент ввода имеет атрибут type=hidden, как показано ниже:

\r\n\r\n
\r\n

<input type="hidden" id="custId" name="custId" value="">.

\r\n
\r\n\r\n

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

\r\n\r\n

Полный пример

\r\n\r\n

Приведем пример с использованием методов Selenium API, которые мы только что рассмотрели.

\r\n\r\n

Мы собираемся войти в систему Hacker News:

\r\n\r\n

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

\r\n\r\n

Для того чтобы пройти аутентификацию, нам необходимо:

\r\n\r\n
    \r\n\t
  1. Перейти на страницу входа в систему с помощью driver.get()
  2. \r\n\t
  3. Выбрать поле ввода имени пользователя с помощью driver.find_element и вызвать element.send_keys() для отправки текста в это поле.
  4. \r\n\t
  5. Проделайте аналогичные действия с полем ввода пароля
  6. \r\n\t
  7. Выберите кнопку входа в систему (конечно же, с помощью элемента find_element) и нажмите ее с помощью element.click().
  8. \r\n
\r\n\r\n

Все должно быть просто, верно? Давайте посмотрим на код:

\r\n\r\n
\r\n

driver.get("https:\/\/news.ycombinator.com\/login")

\r\n\r\n

login = driver.find_element_by_xpath("\/\/input").send_keys(USERNAME)
\r\npassword = driver.find_element_by_xpath("\/\/input[@type='password']").send_keys(PASSWORD)
\r\nsubmit = driver.find_element_by_xpath("\/\/input[@value='login']").click()

\r\n
\r\n\r\n

Легко, правда? Но здесь не хватает одного важного момента. Как мы узнаем, вошли ли мы в систему?

\r\n\r\n

Мы можем попробовать несколько вариантов:

\r\n\r\n
    \r\n\t
  • Проверить наличие сообщения об ошибке (например, "Неверный пароль").
  • \r\n\t
  • Проверить наличие на странице элемента, который отображается только после входа в систему.
  • \r\n
\r\n\r\n

Итак, мы проверим кнопку выхода из системы. Кнопка выхода из системы имеет идентификатор logout (легко)!

\r\n\r\n

Мы не можем просто проверить, является ли элемент None, поскольку find_element вызывает исключение, если элемент не найден в DOM. Поэтому мы должны использовать блок try\/except и перехватить исключение NoSuchElementException:

\r\n\r\n
\r\n

# dont forget from selenium.common.exceptions import NoSuchElementException
\r\ntry:
\r\n    logout_button = driver.find_element_by_id("logout")
\r\n    print('Успешный вход в систему')
\r\nexcept NoSuchElementException:
\r\n    print('Неверный логин\/пароль')

\r\n
\r\n\r\n

Снятие скриншотов

\r\n\r\n

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

\r\n\r\n
\r\n

driver.save_screenshot('screenshot.png')

\r\n
\r\n\r\n

Один-единственный вызов - и у нас есть скриншот нашей страницы. Ну, разве это не круто!

\r\n\r\n

Обратите внимание, что при создании скриншота с помощью Selenium некоторые вещи могут пойти не так или потребовать доработки. Во-первых, необходимо убедиться, что размер окна установлен правильно. Затем необходимо убедиться, что все асинхронные HTTP-вызовы, выполняемые фронтенд-кодом JavaScript, завершены, и страница полностью отрисована.

\r\n\r\n

В нашем случае с Hacker News все просто, и нам не нужно беспокоиться об этих проблемах.

\r\n\r\n

Ожидание присутствия элемента

\r\n\r\n

Работа с сайтом, использующим большое количество JavaScript для отображения содержимого, может оказаться непростой задачей. В настоящее время все больше сайтов используют такие фреймворки, как Angular, React и Vue.js, для своих фронт-эндов. Работа с этими фреймворками сложна тем, что они не просто предоставляют HTML-код, а содержат довольно сложный набор JavaScript-кода, который изменяет дерево DOM "на лету" и отправляет множество информации асинхронно в фоновом режиме через AJAX.

\r\n\r\n

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

\r\n\r\n
    \r\n\t
  • Использовать time.sleep() перед тем, как сделать снимок экрана.
  • \r\n\t
  • Использовать объект WebDriverWait.
  • \r\n
\r\n\r\n

Если вы используете time.sleep(), то вам придется использовать наиболее разумную задержку для вашего случая. Проблема в том, что вы либо ждете слишком долго, либо недостаточно долго, и ни то, ни другое не является идеальным вариантом. Кроме того, сайт может загружаться медленнее на вашем домашнем ISP-соединении, чем когда ваш код работает в производстве в центре обработки данных. При использовании WebDriverWait это не нужно принимать во внимание. Он будет ждать только столько, сколько необходимо, пока не появится нужный элемент (или пока не наступит таймаут).

\r\n\r\n
\r\n

try:
\r\n    element = WebDriverWait(driver, 5).until(
\r\n        EC.presence_of_element_located((By.ID, "mySuperId"))
\r\n    )
\r\nfinally:
\r\n    driver.quit()

\r\n
\r\n\r\n

Это позволит дождаться появления элемента с HTML-идентификатором mySuperId или истечения пятисекундного таймаута.
\r\nПолный список Waits и их ожидаемых условий можно, конечно, найти в документации Selenium. Подробнее о веб-парсинге читайте в руководстве по веб-парсингу для получения финансовых данных.

\r\n\r\n

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

\r\n\r\n

\"\"

\r\n\r\n

Выполнение JavaScript

\r\n\r\n

Как и в случае со скриншотами, мы также можем в полной мере использовать JavaScript-движок нашего браузера. Это означает, что мы можем внедрять и выполнять произвольный код в контексте сайта. Хотите сделать скриншот фрагмента, расположенного немного ниже по странице? Легко, window.scrollBy() и execute_script() помогут вам в этом.

\r\n\r\n
\r\n

javaScript = "window.scrollBy(0, 1000);"
\r\ndriver.execute_script(javaScript)

\r\n
\r\n\r\n

Или вы хотите выделить все теги якорей рамкой? Это проще простого.

\r\n\r\n
\r\n

javaScript = "document.querySelectorAll('a').forEach(e => e.style.border='red 2px solid')"
\r\ndriver.execute_script(javaScript)

\r\n
\r\n\r\n

Дополнительным преимуществом функции execute_script() является то,