Selenium: пишем парсер для меняющегося сайта

Аватарка пользователя Елена Капаца

Разобрали на примере, как работает Python selenium и настроили бота, который будет отсылать находки в Telegram.

Обложка поста Selenium: пишем парсер для меняющегося сайта

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

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

Выбираем сайт

Для гайда я выбрала ресурс otzivisotrudnikov.ru, поскольку он позволяет не только провести парсинг, но и попробовать сопутствующие действия selenium вроде ожидания появления страницы и нажатия кнопки «Загрузить еще».

Если вы только осваиваете скрэйперы, к сложным порталам, сопротивляющимся парсингу, пока подступаться не стоит. Среди них — все продукты Яндекса, а также Авито. Некоторые популярные площадки вроде HeadHunter, «ощутив пауков на себе», поступили демократично и создали API для выгрузки данных.

Подготовка среды разработки

Для начала импортируем необходимые библиотеки:

			from selenium import webdriver;
from selenium.webdriver.common.by import By
import os
import pandas as pd
import re
import telegram
		

Загоняем инструментарий в файл requirements.txt:

			selenium
pandas
python-telegram-bot
		

Создаем виртуальное окружение и устанавливаем инструменты:

			python3 -m venv selenium_env
source selenium_env/bin/activate
pip3 install -r requirements.txt
		

Я хочу наладить коллекцию пауков таким образом, чтобы новые отзывы отправлялись мне в Telegram. Для этого предстоит создать бота. Чтобы получить ключ, перейдите по адресу my.telegram.org/apps, авторизуйтесь и создайте приложение (документация). API Key лежит в поле App api_hash:

			TELEGRAM_API_KEY = "API Key"
		

Чтобы получить ID чата, добавим туда бота @RawDataBot и запустим командой /start. В ответ он отдаст массив, среди которых есть и идентификатор:

			"chat": {
    "id": -1001200247335,
    ...
},
		

Это целочисленная переменная, знак минус тоже оставляем:

			TELEGRAM_CHAT_ID = -1001200247335
		

Чтобы получить токен, нужно пообщаться с @BotFather — утилитой для создания и настройки ботов. Если у вас пока нет ботов в TG, следуйте этой краткой инструкции.

			TELEGRAM_BOT_TOKEN = "Bot Token"
		
Когда скрейперов становится слишком много, оптимальным решением будет .env-файл. О работе с этим инструментом можно узнать больше в статье “Using .env Files for Environment Variables in Python Applications”.

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

			DELAY = 5
		

Зададим полную ссылку на страницу:

			URL = "https://otzivisotrudnikov.ru/company/moskva/lamoda_ru_internet_magazin/"
		

Отзывов много, потому нам потребуется нажать кнопку «Больше». Чтобы это сделать, укажем число нажатий (число страниц пока проверяется вручную):

			MAX_LOAD_MORE_CLICKS = 5
		

Инициируем бота для отправки новых отзывов:

			bot = telegram.Bot(token=TELEGRAM_BOT_TOKEN)
		

Теперь настал через самого Selenium. Инициируем экземпляр веб-драйвера и передадим ему целевую ссылку:

			driver = webdriver.Chrome()
driver.get(URL)
		

Объявим временные списки, в которых будем хранить тексты отзывов, ссылки на них и дату публикации:

			reviews_lst = []
urls_lst = []
dates_lst = []
		

Отдадим команду selenium выполнять поиск элементов на каждой странице:

			count = 0
while count <= MAX_LOAD_MORE_CLICKS:
    try:
        WebDriverWait(driver, 100)
        # Код извлечения и обработки элементов (будет дальше)
        count += 1
    except TimeoutException:
        break
		

Как быстрее писать селекторы

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

  • нажимаем на любом свободном месте на веб-странице «Просмотреть код» (или комбинацией Ctrl + Shift + C / Cmd + Shift + C);
  • выбираем наведением мыши один отзыв со всеми интересующими элементами и копируем название класса (col-xs-10);
  • нажимаем комбинацию Ctrl + F / Cmd + F прямо в панели разработчика. Откроется поле поиска по селектору;
  • набираем //* (отсылает к всей странице на языке XPath), затем [@class=’col-xs-10′];

Посмотреть, как проверяется селектор, можно на видео.

Записей несколько, потому я использую метод find_elements(). Укажем класс, который предстоит найти:

			full_reviews = driver.find_elements(By.XPATH, "//*[@class='col-xs-10']")
		

Теперь результат хранится в специальном объекте selenium-webdriver.WebElement, и чтобы извлечь из него текст, используется метод text().

Звездочка, кстати, помогает справляться с меняющимися названиями классов: вы можете подставлять ее аналогично сочетанию (.*) в регулярных выражениях, и в случае "//*[@class='col-*']" она будет цеплять все объекты классов, начинающихся с col-.

В сниппете ниже я удаляю фразу, которая не нужна в выгрузке заказчикам проекта:

			reviews_lst = []
for element in full_reviews:
    text = element.text.replace('Читать полностью отзыв и комментарии', '')
    reviews_lst.append({'text': text})
		

Теперь выделим тем же образом ссылки, они зашиты в кнопки «Читать полностью отзыв и комментарии» (класс ‘read-more-serm‘):

			urls = driver.find_elements(By.XPATH, "//*[@class='read-more-serm']/a")
for element in urls:
    review_absolute_path = element.get_attribute("href")
    urls_lst.append(review_absolute_path)
		

Осталось вычленить время создания отзыва:

			dates = driver.find_elements(By.XPATH, "//*[@class='divh1 red']")
for element in dates:
    element = element.text
    when = re.sub(' \|(.*)', '', element)
    dates_lst.append(when)
		

Теперь троицу выше превратим в словарь:

			i = 0
while i < len(reviews_lst):
    review = {'text': reviews_lst[i]['text'], 'when': dates_lst[i], 'url': urls_lst[i]}
    reviews.append(review)
    i += 1
		

Преобразуем словарь в таблицу методом pd.DataFrame():

			scrapedReviews = pd.DataFrame(reviews)
		

Добавим столбец с автоматическим индексом:

			scrapedReviews.insert(0, 'id', range(0, 0 + len(scrapedReviews)))
		

Сохраним результат в файл.csv:

			scrapedReviews.to_csv('scrapedReviews.csv', index=False)
		
Selenium: пишем парсер для меняющегося сайта 1

Отправим отзывы по одному в чат Telegram:

			i = 0
while i < len(scrapedReviews):
    bot.send_message(chat_id=TELEGRAM_CHAT_ID, text=f'\n{scrapedReviews["text"][i][:-4]}\n {scrapedReviews["url"][i]} \n-------------------\n', disable_web_page_preview=True,   parse_mode='html')
    i += 1
		

Кстати, параметр parse_mode позволяет подключить HTML-разметку и улучшить читаемость отзыва. Разделы «Список льгот», «Что мне нравится в работодателе» и «Что можно было бы улучшить» были «обернуты» полужирным шрифтом ():

Selenium: пишем парсер для меняющегося сайта 2

Заключение

Конечно, существуют и low-code решения для сбора данных с веба, однако полную управляемость по-прежнему обеспечивают лишь самописные инструменты вроде selenium и beautifulsoup4. Подспорьем новичкам в этой нелегкой задаче, где структура HTML-документа то и дело меняется, может стать подборка пауков на GitHub. Добросовестная часть авторов их даже обернула в Docker, а это значит, что процесс деплоя и дотяжки селекторов до актуального состояния займет у вас минимум времени.

Полный код можно посмотреть по ссылке.

Какую библиотеку для парсинга вы предпочитаете?
selenium
beautifulsoup4
Low=-code-инструменты
API
Парсинг
Инструменты
Опрос
Python
Telegram
Библиотеки
3641