Парсинг SPA-сайтов: Playwright для JavaScript-рендеринга

Современные сайты всё чаще рендерят контент на стороне клиента: React, Vue, Angular загружают данные через API и отрисовывают их в браузере. Обычный HTTP-запрос через httpx вернёт пустой HTML без нужных данных. Решение — Playwright: он запускает настоящий браузер, ждёт загрузки JavaScript и позволяет парсить готовую страницу.

Установка Playwright

pip install playwright
playwright install chromium

Команда playwright install chromium скачивает браузер Chromium (~150 МБ). Можно также установить Firefox и WebKit.

Первый запуск: загружаем SPA-страницу

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page()

        # Устанавливаем User-Agent
        await page.set_extra_http_headers({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                          "AppleWebKit/537.36 Chrome/131.0.0.0 Safari/537.36"
        })

        await page.goto("https://spa-example.com/products")

        # Ждём, пока появится нужный элемент
        await page.wait_for_selector(".product-card", timeout=10000)

        # Получаем HTML после рендеринга
        content = await page.content()
        print(f"Загружено {len(content)} символов HTML")

        await browser.close()

asyncio.run(main())

Ключевой момент — wait_for_selector(). Без него вы получите HTML до того, как JavaScript отрендерит контент.

Ожидание элементов: стратегии

Playwright предлагает несколько способов дождаться загрузки контента:

# Ждём конкретный CSS-селектор
await page.wait_for_selector(".product-card")

# Ждём, пока элемент станет видимым
await page.wait_for_selector(".product-card", state="visible")

# Ждём завершения сетевых запросов (SPA загружает данные через API)
await page.wait_for_load_state("networkidle")

# Ждём определённый URL в сетевых запросах
async with page.expect_response(
    lambda r: "/api/products" in r.url
) as response_info:
    await page.goto("https://spa-example.com/products")
response = await response_info.value
api_data = await response.json()  # данные напрямую из API

Перехват API-ответов через expect_response() — мощный приём. Вы получаете чистые JSON-данные, минуя парсинг HTML.

Парсинг элементов на странице

Playwright позволяет извлекать данные двумя способами: через встроенные локаторы или через BeautifulSoup.

import asyncio
from playwright.async_api import async_playwright

async def parse_products():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page()
        await page.goto("https://spa-example.com/products")
        await page.wait_for_selector(".product-card")

        # Способ 1: через Playwright locators
        cards = page.locator(".product-card")
        count = await cards.count()

        products = []
        for i in range(count):
            card = cards.nth(i)
            name = await card.locator(".product-name").text_content()
            price = await card.locator(".product-price").text_content()
            link = await card.locator("a").get_attribute("href")

            products.append({
                "name": name.strip(),
                "price": price.strip(),
                "link": link
            })

        print(f"Найдено {len(products)} товаров")
        await browser.close()
        return products

asyncio.run(parse_products())

# Способ 2: получить HTML и парсить через BeautifulSoup
from bs4 import BeautifulSoup

html = await page.content()
soup = BeautifulSoup(html, "lxml")

for card in soup.select(".product-card"):
    name = card.select_one(".product-name").text.strip()
    price = card.select_one(".product-price").text.strip()
    print(f"{name}: {price}")

Скроллинг и подгрузка данных

Многие SPA подгружают контент при скроллинге (infinite scroll). Playwright умеет имитировать это.

async def scroll_and_collect(page, selector, max_items=100):
    """Скроллим страницу, пока не соберём нужное количество элементов."""
    items = set()
    prev_count = 0
    stale_rounds = 0

    while len(items) < max_items:
        elements = page.locator(selector)
        count = await elements.count()

        for i in range(count):
            text = await elements.nth(i).text_content()
            items.add(text.strip())

        if count == prev_count:
            stale_rounds += 1
            if stale_rounds >= 3:
                break  # новых элементов больше нет
        else:
            stale_rounds = 0

        prev_count = count

        # Скроллим вниз
        await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
        await page.wait_for_timeout(1500)

    return list(items)

Скриншоты для отладки

Когда что-то идёт не так, скриншот — лучший инструмент отладки.

# Полная страница
await page.screenshot(path="debug_full.png", full_page=True)

# Конкретный элемент
element = page.locator(".product-card").first
await element.screenshot(path="debug_card.png")

# PDF страницы
await page.pdf(path="page.pdf", format="A4")

Обход anti-bot защит

Сайты с защитой от ботов (Cloudflare, reCAPTCHA) распознают headless-браузеры. Базовые меры маскировки:

async def create_stealth_browser(p):
    browser = await p.chromium.launch(
        headless=True,
        args=[
            "--disable-blink-features=AutomationControlled",
            "--no-sandbox",
        ]
    )
    context = await browser.new_context(
        viewport={"width": 1920, "height": 1080},
        user_agent=(
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/131.0.0.0 Safari/537.36"
        ),
        locale="ru-RU",
        timezone_id="Europe/Moscow",
    )

    page = await context.new_page()

    # Скрываем признаки автоматизации
    await page.add_init_script("""
        Object.defineProperty(navigator, 'webdriver', {
            get: () => undefined
        });
    """)

    return browser, page

Для серьёзной anti-bot защиты используйте библиотеку playwright-stealth или коммерческие решения (Browserless, ScrapingBee).

Работа через прокси

browser = await p.chromium.launch(
    headless=True,
    proxy={
        "server": "http://proxy.example.com:8080",
        "username": "user",
        "password": "pass"
    }
)

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

Производительность

Playwright потребляет значительно больше ресурсов, чем httpx. Оптимизируйте:

  • Блокируйте лишние ресурсы — картинки, шрифты, CSS не нужны для парсинга данных
  • Переиспользуйте браузер — создавайте одну инстанцию и открывайте новые страницы
  • Ограничивайте параллельность — 3 – 5 страниц одновременно для среднего сервера
  • Перехватывайте API — если SPA загружает данные через fetch/XHR, берите данные оттуда напрямую
# Блокируем картинки и шрифты
await page.route("**/*.{png,jpg,jpeg,gif,svg,woff,woff2}",
                 lambda route: route.abort())

Когда использовать Playwright, а когда httpx

  • httpx + BeautifulSoup — статические сайты, серверный рендеринг (WordPress, Django, SSR Next.js)
  • Playwright — SPA (React, Vue, Angular), сайты с JavaScript-рендерингом, страницы с бесконечным скроллом, формы с капчей

Правило: попробуйте сначала httpx. Если в HTML нет нужных данных — переходите на Playwright.

Есть идея? Реализуем

Разрабатываем проекты, которые решают задачи бизнеса — от лендинга до сложного сервиса. Расскажите о своей задаче, подберём решение.

Написать в Telegram

27.03.2026

Нужна консультация?

Оставьте свои контактные данные, или свяжитесь с нами удобным для вас способом

Привет! Меня зовут Багира. Пишите, я все передам хозяевам!

Привет! Меня зовут Багира. Пишите, я все передам хозяевам!

Нажимая кнопку «Принять», вы соглашаетесь на сбор cookie. Мы используем их для обеспечения функционирования веб-сайта, аналитики действий и улучшения качества обслуживания. Если Вы не хотите, чтобы эти данные обрабатывались, отключите cookie в настройках браузера или прекратите использовать сайт.
Принять