Современные сайты всё чаще рендерят контент на стороне клиента: 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.
Есть идея? Реализуем
Разрабатываем проекты, которые решают задачи бизнеса — от лендинга до сложного сервиса. Расскажите о своей задаче, подберём решение.

