Часть 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 часто достается за медлительность. Но когда дело касается сложных сайтов-агрегаторов или, как в моём случае, сайтов провайдеров, где данные «размазаны» по кнопкам и табам - это надежный путь.

Мы получили:

  • Сбор всех вариаций скоростей одного тарифа.

  • Обход скрытых фильтров.

  • Надежное хранение с защитой от дублей в БД.

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

А как вы боретесь с динамическим контентом? Пишите в комментариях!