Часть 1: Парсинг тарифов интернета и ТВ: Архитектура БД и бэкенд на SQL
На этапе тестирования я отобрал 6 городов (Москва, Санкт-Петербург, Новосибирск, Екатеринбург, Казань, Красноярск) и двух крупнейших провайдеров России - Ростелеком и Дом.ру. В планах масштабирование на большее количество городов и операторов.
Для парсинга тарифов у провайдеров применял связку Python + Selenium + BeautifulSoup, через хранимую процедуру складывал полученные данные в базу PostgreSQL.
Основные трудности, с которыми столкнулся при парсинге тарифов у провайдеров
Сайт Ростелекома оказался более «дружелюбным» к парсингу: все тарифы расположены на одной странице, каждый описан в отдельном HTML-блоке с понятной структурой, что оказалось удобным не только для парсинга, но и для пользователей которые хотят ознакомится с тарифами. Достаточно было один раз загрузить страницу и можно собирать данные. С Дом.ру ситуация сложилась иначе. Во-первых, сайт активно сопротивляется автоматическому сбору данных, без имитации поведения реального пользователя парсер моментально получал блокировку. Во-вторых, на карточке тарифа присутствует селектор выбора скорости, и цена динамически подгружается только после клика. Если просто взять исходный HTML, в нем останется цена за базовый вариант скорости, например, за 300 Мбит/с, даже если в интерфейсе выбрано 600 Мбит/с.
Шаг 1: Настройка Selenium Stealth
Чтобы сайт не заблокировал нас на второй секунде, используем selenium-stealth. Это маскирует автоматизированный браузер под обычного пользователя.
python
from selenium import webdriver from selenium_stealth import stealth options = webdriver.ChromeOptions() options.add_argument("--headless=new") # Работаем в фоновом режиме driver = webdriver.Chrome(options=options) stealth(driver, languages=["ru-RU", "ru"], vendor="Google Inc.", platform="Win32", fix_hairline=True, )
Шаг 2: Интерактивный парсинг (Кликаем и собираем)
Главная хитрость: нам нужно не просто собрать карточки, а проитерироваться по каждой кнопке внутри каждой карточки.
Важный нюанс: После клика через Selenium DOM обновляется. Чтобы BeautifulSoup увидел новые цены, объект супа нужно пересоздавать внутри цикла.
python
# Находим все карточки тарифов cards_count = len(driver.find_elements(By.CSS_SELECTOR, 'article[aria-label="package-card"]')) for card_idx in range(cards_count): # Находим кнопки переключения скоростей внутри карточки card_el = driver.find_elements(By.CSS_SELECTOR, 'article[aria-label="package-card"]')[card_idx] speed_btns = card_el.find_elements(By.CSS_SELECTOR, '.speed-selector li span') for btn_idx in range(len(speed_btns)): # Кликаем по кнопке (используем JS-клик для надежности) btn = driver.find_elements(By.CSS_SELECTOR, '...')[btn_idx] driver.execute_script("arguments[0].click();", btn) time.sleep(2) # Ждем асинхронного обновления цены # Магия: берем обновленный HTML и скармливаем его BeautifulSoup full_soup = BeautifulSoup(driver.page_source, 'html.parser') target_card = full_soup.find_all('article')[card_idx] # Теперь парсим актуальную цену и название price = target_card.find('span', class_='price').text
Шаг 3: Фильтры «Только интернет»
Часто тарифы без ТВ скрыты за отдельным чекбоксом. Чтобы не раздувать логику, мы просто оборачиваем наш парсинг в цикл по типам фильтров (bundle - с ТВ, mono - только интернет), принудительно кликая по чекбоксу перед началом сбора.
Шаг 4: Сохранение в PostgreSQL через процедуру
Зачем использовать хранимые процедуры (Stored Procedures) вместо простого INSERT?
Атомарность: Мы можем в одной транзакции пометить старые тарифы как архивные и добавить новые.
Чистота данных: Процедура может сама проверять дубликаты или обновлять цены, если тариф уже существует.
sql
CREATE OR REPLACE PROCEDURE upsert_tariff( p_city_id INT, p_provider_id INT, p_name TEXT, p_price INT, -- ... другие параметры ) LANGUAGE plpgsql AS $$ BEGIN -- Обновляем, если такой тариф уже есть в этом городе у этого провайдера INSERT INTO tariffs (city_id, provider_id, name, price, last_updated) VALUES (p_city_id, p_provider_id, p_name, p_price, NOW()) ON CONFLICT (city_id, provider_id, name) DO UPDATE SET price = EXCLUDED.price, last_updated = NOW(); END; $$;
Итоги
Связке Selenium + BeautifulSoup часто достается за медлительность. Но когда дело касается сложных сайтов-агрегаторов или, как в моём случае, сайтов провайдеров, где данные «размазаны» по кнопкам и табам - это надежный путь.
Мы получили:
Сбор всех вариаций скоростей одного тарифа.
Обход скрытых фильтров.
Надежное хранение с защитой от дублей в БД.
Собранные данные уже позволяют строить интересную аналитику по тарифам в разрезе городов и провайдеров. В следующей статье я планирую поделиться получившейся статистикой, а также рассказать об оптимизациях парсинга и работы с базой данных .
А как вы боретесь с динамическим контентом? Пишите в комментариях!
