Большая часть данных, которые стоит собирать, никогда не появляется в сыром HTML. Сетки товаров, ветки комментариев, бесконечно прокручиваемые ленты и виджеты панели управления появляются только после того, как JavaScript отработает в браузере, поэтому обычный HTTP-запрос возвращает скелет, в котором отсутствуют самые интересные части. Классическое решение, отрисовать страницу в реальном браузере, а затем разобрать готовую разметку. Самая распространённая связка для этого в Python, Selenium и BeautifulSoup.

Это руководство показывает, как парсить динамический контент с помощью Selenium и BeautifulSoup: Selenium управляет headless-экземпляром Chrome для выполнения JavaScript, ожидания элементов, прокрутки для запуска ленивой загрузки и кликов по элементам, тогда как BeautifulSoup парсит отрисованный page_source в чистые структурированные данные. Мы создадим небольшой рабочий пример, а затем честно рассмотрим, где этот стек становится затратным и где серверный API отрисовки является более лёгким выбором.

Почему обычный запрос упускает динамический контент

Когда страница отрисовывается на сервере, скачиваемый HTML уже содержит данные. Когда она отрисовывается на клиенте, сервер отправляет практически пустую оболочку плюс пакет JavaScript; браузер выполняет этот JavaScript, обращается к API и впоследствии вставляет реальный контент в DOM. Такая библиотека, как requests, видит только первый ответ и никогда не видит контент, который добавляет JavaScript позже.

В этом и состоит вся проблема, которую решает динамический парсинг: нужно что-то, что действительно выполнит JavaScript страницы, прежде чем вы читаете DOM. Selenium делает именно это, автоматизируя реальный браузер. После того как браузер всё отрисовал, получившийся HTML, это просто HTML, а BeautifulSoup, это быстрый и эргономичный способ извлекать из него поля. Два инструмента чётко разделяют работу: Selenium занимается взаимодействием и отрисовкой, BeautifulSoup, извлечением.

Render first, parse second

BeautifulSoup не выполняет JavaScript. Самостоятельно он разбирает любой HTML, который вы ему передаёте, поэтому передача сырого ответа страницы с клиентской отрисовкой даёт ту же пустую оболочку, что и обычный запрос. Шаг отрисовки должен произойти сначала, будь то локальный браузер через Selenium или API отрисовки, возвращающий готовый HTML.

Настройка окружения

Вам потребуется Python 3.8 или выше и pip. Создайте виртуальное окружение, чтобы зависимости оставались изолированными, затем установите Selenium и BeautifulSoup.

bash
python -m venv scraper_env
source scraper_env/bin/activate

pip install selenium beautifulsoup4

В Windows активируйте окружение командой scraper_env\Scripts\activate вместо строки с source. Загружать бинарный драйвер вручную не нужно: начиная с Selenium 4.10, Selenium Manager автоматически находит и загружает соответствующий ChromeDriver при первом запуске Chrome, поэтому достаточно актуальной установки Chrome и пакета selenium.

Шаг 1: Запуск headless Chrome WebDriver

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

python
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

def build_driver():
    options = Options()
    options.add_argument("--headless=new")
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--window-size=1920,1080")
    return webdriver.Chrome(options=options)

Фиксированный размер окна важнее, чем кажется: многие сайты отрисовывают разные макеты при разной ширине области просмотра, поэтому закрепление размера обеспечивает стабильность селекторов между запусками. Чтобы отладить то, что реально видит браузер, уберите строку --headless=new и наблюдайте за загрузкой страницы в реальном времени.

Шаг 2: Навигация и явное ожидание элементов

Самая распространённая ошибка при динамическом парсинге, читать DOM до того, как контент появился. Фиксированный time.sleep(), неправильный способ решения: слишком короткий, и вы упустите данные, слишком длинный, и каждый запуск будет медленным. Правильный инструмент, явное ожидание, которое опрашивает страницу до выполнения конкретного условия (или срабатывания таймаута) и немедленно возвращается, как только условие выполнено. В Selenium это реализуется через WebDriverWait в связке с expected_conditions.

python
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

def load_page(driver, url, wait_for):
    driver.get(url)
    WebDriverWait(driver, 15).until(
        EC.presence_of_element_located((By.CSS_SELECTOR, wait_for))
    )

Здесь load_page переходит по URL и блокируется только до тех пор, пока не появится хотя бы один элемент, соответствующий wait_for, но не более 15 секунд. Используйте visibility_of_element_located, когда вам также нужно, чтобы элемент был отрисован (а не просто присутствовал в DOM), и element_to_be_clickable перед кликом. Привязка ожидания к элементу, который вас реально интересует, делает запуск Selenium одновременно быстрым и надёжным.

Шаг 3: Прокрутка для запуска ленивой загрузки

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

python
import time

def scroll_to_bottom(driver, pause=2.0, max_rounds=10):
    last_height = driver.execute_script("return document.body.scrollHeight")
    for _ in range(max_rounds):
        driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
        time.sleep(pause)
        new_height = driver.execute_script("return document.body.scrollHeight")
        if new_height == last_height:
            break
        last_height = new_height

Короткий sleep здесь, это прагматичная пауза для загрузки и отрисовки следующего пакета; это единственное место, где фиксированная задержка трудноизбежна, поскольку триггером является сеть, а не известный элемент. Ограничьте цикл параметром max_rounds, чтобы страница, которая никогда не перестаёт расти, не запускала его бесконечно. Если сайт использует кнопку «Загрузить ещё» вместо бесконечной прокрутки, эквивалентом является поиск кнопки, клик по ней, ожидание новых строк и повторение до тех пор, пока кнопка не исчезнет.

Шаг 4: Передача отрисованного HTML в BeautifulSoup

После того как страница отрисована и полностью прокручена, остаётся обычный парсинг. Читайте driver.page_source, живой DOM, сериализованный в HTML, и загружайте его в BeautifulSoup. Отсюда вы выбираете элементы по CSS-селектору точно так же, как при работе с любой статичной страницей.

python
from bs4 import BeautifulSoup

def parse_items(html):
    soup = BeautifulSoup(html, "html.parser")
    items = []
    for card in soup.select("div.product-card"):
        title = card.select_one("h2.title")
        price = card.select_one("span.price")
        items.append({
            "title": title.get_text(strip=True) if title else None,
            "price": price.get_text(strip=True) if price else None,
        })
    return items

Защитные проверки для каждого поля (title.get_text(...) if title else None) предотвращают сбой всего запуска из-за одного отсутствующего элемента, что стоит делать с самого начала, потому что реальные листинги непоследовательны. Можно было бы запрашивать элементы через собственный find_elements Selenium, но BeautifulSoup быстрее для массового извлечения, а его API для работы с селекторами и навигации удобнее, когда DOM устоялся.

Шаг 5: Сборка всех частей

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

python
import json

def main():
    url = "https://example.com/products"
    driver = build_driver()
    try:
        load_page(driver, url, "div.product-card")
        scroll_to_bottom(driver)
        data = parse_items(driver.page_source)
    finally:
        driver.quit()
    print(json.dumps(data, indent=2))

if __name__ == "__main__":
    main()

Блок try/finally в продакшене обязателен. Цикл прокрутки или ожидание может вызвать исключение, и если driver.quit() так и не выполнится, после каждого сбоя остаётся процесс-зомби Chrome. При длительной работе это быстро исчерпывает память. Для более детального изучения работы браузеров таким образом читайте статью о headless-браузере для веб-парсинга, а для широкого обзора отрисовки JavaScript с помощью Python, статью «Как парсить JavaScript-страницы с Python».

Честные издержки подхода с Selenium

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

  • Накладные расходы браузера. Каждая страница запускает полный экземпляр Chrome с его потреблением памяти и ЦП. Дюжина параллельных драйверов может насытить небольшой сервер, поэтому пропускная способность ограничена железом, а не только сетью.
  • Нестабильные ожидания. Явные ожидания значительно лучше sleep, но они всё равно ломаются при изменении разметки или тайминга сайта, а ожидание, которое проходит локально, может истечь по таймауту на более медленной машине. Логика ожидания превращается в постоянное обслуживание.
  • Обнаружение ботов. Headless-браузер всё равно оставляет сигналы (отпечатки драйвера, отсутствующие заголовки, IP-адреса дата-центров), которые современные системы защиты распознают. При увеличении объёма вы столкнётесь с CAPTCHA и блокировками IP, которые никакое ожидание не устранит, а это означает добавление ротации прокси и патчей для отпечатков поверх всего вышеперечисленного.

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

Crawlbase Crawling API

Если вы хотите получить отрисованный HTML без запуска флота браузеров, Crawling API отрисовывает страницу в реальном браузере на стороне сервера и ротирует жилые IP-адреса, а затем возвращает готовый HTML, который вы парсите тем же кодом BeautifulSoup. Вы передаёте JS-токен и параметры ожидания вместо управления драйверами, циклами прокрутки и пулом прокси. Начните с бесплатного тарифа, направив его на одну динамическую страницу.

Более лёгкая альтернатива: отрисовка на сервере, парсинг локально

Crawling API сохраняет ту половину этого рабочего процесса, которая вам нравится (извлечение через BeautifulSoup), и убирает ту, которая доставляет неудобства (запуск и разблокировка браузеров). Вы отправляете URL с JavaScript-токеном, API отрисовывает его за доверенным IP, и вы парсите возвращаемый HTML точно так же, как раньше. Та же функция parse_items из шага 4 работает без изменений.

python
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup
import json

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"})

def fetch_rendered(url):
    options = {"ajax_wait": "true", "page_wait": 5000}
    response = api.get(url, options)
    if response["status_code"] == 200:
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['status_code']}")
    return None

html = fetch_rendered("https://example.com/products")
if html:
    print(json.dumps(parse_items(html), indent=2))

Опция ajax_wait указывает API дождаться завершения асинхронного контента, а page_wait удерживает фиксированное количество миллисекунд после загрузки, чтобы элементы с отложенной отрисовкой появились до захвата. Увеличьте page_wait, если поля возвращаются пустыми. Обратите внимание на то, что исчезло: никакого жизненного цикла драйвера, никаких циклов прокрутки, никакой очистки в try/finally и никакого управления прокси. Отрисовка и ротация IP происходят на стороне сервера, поэтому тысяча страниц, это тысяча HTTP-вызовов, а не тысяча запусков браузера.

When to keep Selenium

Если ваша задача требует подлинного многошагового взаимодействия (вход в систему, заполнение и отправка форм, прохождение мастера настройки, чтение состояния, зависящего от этих действий), сессия браузера с состоянием в Selenium, правильный инструмент. Crawling API выгоден, когда цель, «отрисовать этот URL и вернуть готовый HTML» в большом объёме. Многие проекты используют оба: браузер для немногочисленных интерактивных сценариев, API для массовой загрузки.

Для сравнения других стеков автоматизации браузеров статья о Playwright для веб-парсинга охватывает современную альтернативу Selenium со схожими компромиссами. Если вы предпочитаете маршрутизировать трафик собственного браузера через ротируемые IP вместо использования управляемого API, Smart AI Proxy предоставляет жилую ротацию как прокси-эндпоинт с подстановкой, а для заданий в режиме «отправил и забыл» асинхронный Crawler отправляет отрисованные результаты на колбэк вместо блокировки на каждом запросе.

Итоги

Ключевые выводы

  • Динамический контент требует отрисовки. JavaScript вставляет данные после первого ответа, поэтому страницу нужно выполнить перед парсингом; обычный запрос возвращает оболочку.
  • Selenium отрисовывает, BeautifulSoup извлекает. Управляйте headless Chrome WebDriver для отрисовки и взаимодействия, затем передавайте driver.page_source в BeautifulSoup для быстрого извлечения на основе селекторов.
  • Используйте явные ожидания, а не sleep. WebDriverWait с expected_conditions, привязанный к нужному элементу, быстрее и надёжнее фиксированной задержки.
  • Прокручивайте для запуска ленивой загрузки. Организуйте цикл прокрутка-ожидание-измерение до тех пор, пока высота страницы не перестанет расти, с ограничением числа итераций, чтобы цикл не был бесконечным.
  • Selenium тяжёл при масштабировании. Накладные расходы браузера, нестабильные ожидания и блокировки ботами накапливаются; API отрисовки, такой как Crawling API, возвращает готовый HTML с управляемой ротацией IP, а код BeautifulSoup остаётся прежним.

Часто задаваемые вопросы

Почему BeautifulSoup не может самостоятельно парсить динамический контент?

BeautifulSoup, это парсер, а не браузер: он читает HTML, который вы ему передаёте, но никогда не выполняет JavaScript. На странице с клиентской отрисовкой сырой HTML является пустой оболочкой, поэтому BeautifulSoup нечего извлекать, пока что-нибудь не отрисует страницу первым. Этот шаг отрисовки обеспечивает Selenium локально или API отрисовки с JS-токеном на стороне сервера, до того, как запустится BeautifulSoup.

Как ждать динамические элементы вместо того, чтобы угадывать со sleep?

Используйте явное ожидание. WebDriverWait(driver, timeout).until(EC.presence_of_element_located((By.CSS_SELECTOR, sel))) опрашивает страницу и возвращается сразу, как только элемент появляется, но не позже таймаута. Это быстрее фиксированного time.sleep(), потому что не ждёт дольше необходимого, и надёжнее, потому что привязано к реальному нужному элементу, а не к угаданной длительности.

Нужно ли по-прежнему загружать ChromeDriver вручную?

Нет, начиная с Selenium 4.10. Selenium Manager автоматически определяет и загружает версию ChromeDriver, соответствующую установленному Chrome, при первом запуске браузера. Управлять драйвером вручную нужно только в закрытых окружениях, где автоматическая загрузка заблокирована; в этом случае вы указываете Selenium на предоставленный вами бинарный драйвер.

Поможет ли Selenium обойти системы защиты от ботов?

Самостоятельно при масштабировании, нет. Headless-браузер всё равно выдаёт сигналы (отпечатки автоматизации, IP-адреса дата-центров, отсутствующие или непоследовательные заголовки), которые обнаруживают современные системы защиты, поэтому при росте объёма вы будете сталкиваться с CAPTCHA и блокировками IP. Для их устранения нужно добавить ротацию прокси и патчи для отпечатков, именно поэтому управляемый Crawling API, обрабатывающий отрисовку и ротацию IP вместе, зачастую требует меньше усилий, чем укрепление флота браузеров.

Когда использовать Crawling API вместо Selenium и BeautifulSoup?

Используйте Crawling API, когда задача, надёжно отрисовывать множество страниц с сервера и вы не хотите запускать или разблокировать флот браузеров. Он отрисовывает на стороне сервера, ротирует жилые IP-адреса и возвращает готовый HTML, который ваш существующий код BeautifulSoup парсит без изменений. Оставьте Selenium, когда нужно подлинное взаимодействие с состоянием: вход в систему, отправка форм или прохождение многошагового сценария.

Можно ли повторно использовать код парсинга BeautifulSoup с Crawling API?

Да, и в этом основная привлекательность. Crawling API возвращает отрисованный HTML в виде строки, поэтому тот же вызов BeautifulSoup(html, "html.parser") и те же селекторы работают без изменений. Вы меняете только способ получения HTML: вместо driver.page_source из локального браузера вы читаете response["body"] из вызова API.

Начать создавать

Обходите любой сайт в масштабе, без борьбы с инфраструктурой.

Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.

Самообслуживание · Звонок отдела продаж не требуется · Доступны корпоративные объёмы краулинга