Скрапинг с nodriver: асинхронный CDP-браузер и residential-прокси

Практическое руководство по nodriver — полностью асинхронному преемнику undetected-chromedriver. Архитектура, идиоматичный API, настройка прокси через ProxyHat, масштабирование и этика автоматизации.

Scraping With nodriver: A Practical Guide to Async CDP + Residential Proxies

Скрапинг с nodriver — это следующий шаг для Python-инженеров, которые устали бороться с блокировками undetected-chromedriver. nodriver (ранее известный как undetected-chromedriver v2) — полностью асинхронная библиотека, управляющая Chrome напрямую через DevTools Protocol без единой строки Selenium-кода. В сочетании с residential-прокси от ProxyHat это даёт комбинацию, которая обходит Cloudflare Turnstile и Imperva Incapsula на сайтах, где обычный Playwright или Selenium падает за секунды.

Важно: эта статья посвящена сбору общедоступных данных с сайтов, где вы имеете право на автоматизацию или где это разрешено законом. Несанкционированный доступ к защищённым системам может нарушать CFAA (18 U.S.C. § 1030) и GDPR. Проверяйте ToS сайта и robots.txt перед запуском скрапера.

Архитектура nodriver: почему отсутствие WebDriver меняет всё

Традиционный стек автоматизации браузера — Selenium, Playwright, Puppeteer — работает поверх W3C WebDriver или его проприетарных вариаций. Проблема в том, что WebDriver оставляет enumerable-следы: navigator.webdriver === true, специфичные CDP-команды в runtime, патчи в navigator-объекте. Анти-бот-системы давно научились их детектить.

nodriver (GitHub-репозиторий) работает иначе:

  • Прямой CDP over WebSocket — никаких HTTP-комманд WebDriver, только бинарный протокол DevTools Protocol.
  • Нативный Chrome — запускает реальный Chrome/Chromium без chromedriver-бинарника как посредника.
  • Патчи на уровне процесса — nodriver модифицирует Chrome до запуска, убирая navigator.webdriver, CDP-артефакты и cdc_-переменные, которые Selenium оставляет в DOM.
  • Полностью async — построен на asyncio, нет блокирующих вызовов, вкладки управляются конкурентно.

Результат: браузер, который Cloudflare's bot management не отличает от ручного пользователя в ~95% случаев (по данным сообщества скраперов на крупных e-commerce и ticketing-платформах). Но есть нюанс — nodriver не скрывает ваш IP-адрес. Если дата-центровый IP уже в blacklist-базе Imperva, никакие патчи не помогут. Вот где residential-прокси становятся обязательными.

Идиоматичный API nodriver: Browser, Tab и event hooks

Если вы переходите с Selenium, первый шок — нет driver.get(), нет WebDriverWait, нет expected_conditions. nodriver построен вокруг трёх концепций:

Browser и Tab — асинхронные объекты

nodriver.start() возвращает объект Browser. Каждая вкладка — отдельный Tab-объект с собственным CDP-соединением. Вы можете открыть 20 вкладок и управлять ими конкурентно в одном event loop.

select() и find() вместо WebDriverWait

nodriver не использует polling-based waits. Вместо этого tab.select(selector) ждёт появления элемента через CDP-события DOM. Это быстрее и не нагружает CPU:

import nodriver as uc
import asyncio

async def main():
    browser = await uc.start()
    tab = await browser.get("https://example.com")

    # select() ждёт элемент через CDP-события, не polling
    heading = await tab.select("h1")
    print(await heading.get_attribute("textContent"))

    # find() — неблокирующий поиск, возвращает None если нет
    link = await tab.find("a.read-more")
    if link:
        await link.click()

    browser.stop()

if __name__ == "__main__":
    asyncio.run(main())

Event hooks вместо неявных ожиданий

nodriver позволяет подписываться на CDP-события — Page.loadEventFired, Network.responseReceived, Runtime.executionContextCreated. Это даёт точный контроль без sleep-хаков:

async def main():
    browser = await uc.start()
    tab = await browser.get("https://example.com")

    # Перехватываем сетевые ответы
    responses = []
    async def on_response(event):
        if "api/data" in event.response.get("url", ""):
            responses.append(event.response)

    tab.add_handler(uc.cdp.network.ResponseReceived, on_response)

    # Взаимодействуем со страницей
    await tab.select("button#load-data")
    await asyncio.sleep(2)  # ждём XHR

    for r in responses:
        body = await tab.send(uc.cdp.network.get_response_body(r.requestId))
        print(body)

    browser.stop()

Этот паттерн — перехват XHR/fetch через CDP-события — особенно полезен для SPA-сайтов, где данные приходят через JSON-API, а не в HTML. Вы можете извлечь JSON напрямую, минуя рендеринг.

Настройка nodriver прокси: browser_args и аутентификация

Здесь начинается самая важная часть. nodriver не имеет встроенной ротации прокси — это библиотека для управления браузером, а не прокси-менеджер. Прокси настраиваются через стандартные Chrome flags:

browser = await uc.start(
    browser_args=[
        "--proxy-server=http://gate.proxyhat.com:8080",
    ]
)

Но есть проблема: Chrome's --proxy-server не поддерживает аутентификацию по логину/паролю. Для прокси с авторизацией нужен extension-based подход или перехват ProxyRequiresAuthentication через CDP. nodriver решает это через ProxyAuth-обработчик:

import nodriver as uc
import asyncio

PROXY_HOST = "gate.proxyhat.com"
PROXY_PORT = 8080
PROXY_USER = "user-country-US-session-abc123"
PROXY_PASS = "your_password"

async def main():
    browser = await uc.start(
        browser_args=[
            f"--proxy-server=http://{PROXY_HOST}:{PROXY_PORT}",
        ]
    )

    # nodriver автоматически обрабатывает proxy auth через CDP
    tab = await browser.get("https://example.com")

    # Проверяем IP через прокси
    ip_element = await tab.select("body")
    print(await ip_element.get_attribute("textContent")[:200])

    browser.stop()

if __name__ == "__main__":
    asyncio.run(main())

Почему residential-прокси обязательны для stealth-скрапинга

Datacenter-IP подсети (AWS, DigitalOcean, OVH) находятся в публичных blacklist-базах. Cloudflare и Imperva помечают их как «high risk» ещё до проверки JS-фингерпринта. Residential-прокси используют IP реальных ISP (Comcast, AT&T, Deutsche Telekom), что даёт:

  • Trust score 90+ против 10–30 у дата-центров.
  • Гео-таргетинг — можно выбрать страну и город, что критично для локального контента (SERP, цены, доступность товаров).
  • Sticky-сессии — один IP на всю сессию, что необходимо для многостраничных сценариев (логин → навигация → checkout).

С ProxyHat вы кодируете гео и сессию прямо в username, без дополнительного API-вызова:

ПараметрФорматПример
Странаuser-country-{CODE}user-country-DE
Страна + городuser-country-{CODE}-city-{name}user-country-DE-city-berlin
Sticky-сессияuser-session-{id}user-session-abc123
Комбинацияuser-country-US-session-abc123US IP, держится всю сессию

Подробнее о доступных локациях — на странице ProxyHat Locations.

Runnable-пример: nodriver + ProxyHat residential-прокси

Полный пример: запускаем nodriver через US residential-эндпоинт со sticky-сессией, обходим Cloudflare и извлекаем JSON из защищённой страницы:

import nodriver as uc
import asyncio
import json
import urllib.parse

# --- Конфигурация ProxyHat ---
PROXY_HOST = "gate.proxyhat.com"
PROXY_PORT = 8080
PROXY_USER = "user-country-US-session-abc123"
PROXY_PASS = "your_api_key"

TARGET_URL = "https://protected-site.example.com/data"

def build_proxy_url(user: str, pwd: str, host: str, port: int) -> str:
    """Собирает proxy URL для Chrome --proxy-server flag."""
    auth = urllib.parse.quote(f"{user}:{pwd}", safe="")
    return f"http://{host}:{port}"

async def scrape_with_nodriver():
    # 1. Запускаем Chrome с прокси
    browser = await uc.start(
        browser_args=[
            f"--proxy-server=http://{PROXY_HOST}:{PROXY_PORT}",
            "--disable-blink-features=AutomationControlled",
            "--no-sandbox",
            "--disable-dev-shm-usage",
        ],
        headless=False,  # headful лучше для anti-bot обхода
    )

    # 2. Открываем целевую страницу
    tab = await browser.get(TARGET_URL)

    # 3. Ждём загрузки (nodriver сам ждёт через CDP)
    await tab.select("body", timeout=30)

    # 4. Перехватываем JSON-ответы через CDP-события
    captured_data = []

    async def on_response(event):
        url = event.response.get("url", "")
        mime = event.response.get("mimeType", "")
        if "json" in mime and "api" in url:
            body = await tab.send(
                uc.cdp.network.get_response_body(event.requestId)
            )
            captured_data.append({"url": url, "body": body})

    tab.add_handler(uc.cdp.network.ResponseReceived, on_response)

    # 5. Триггерим загрузку данных (клик, скролл, и т.д.)
    try:
        btn = await tab.select("button#load-data", timeout=10)
        await btn.click()
    except Exception:
        pass  # данные могут загрузиться автоматически

    await asyncio.sleep(3)

    # 6. Извлекаем данные
    for item in captured_data:
        parsed = json.loads(item["body"][0])  # body — кортеж
        print(f"URL: {item['url']}")
        print(f"Data keys: {list(parsed.keys())}")

    # 7. Graceful shutdown
    browser.stop()
    return captured_data

if __name__ == "__main__":
    results = asyncio.run(scrape_with_nodriver())
    print(f"\nЗахвачено {len(results)} JSON-ответов")

Ключевые моменты этого примера:

  • headless=False — headful-режим обходит больше анти-бот-систем. В Docker используйте Xvfb.
  • --disable-blink-features=AutomationControlled — дополнительный флаг, убирающий navigator.webdriver.
  • Перехват JSON через CDP-события — извлекаем данные из XHR, не парся DOM.
  • session-abc123 — sticky-сессия гарантирует, что один IP держится всю сессию, что критично для сайтов с CSRF-токенами.

Для SOCKS5-прокси используйте порт 1080:

browser = await uc.start(
    browser_args=[
        "--proxy-server=socks5://gate.proxyhat.com:1080",
    ]
)

Масштабирование: конкурентные вкладки, Docker-флот, per-context прокси

Конкурентные вкладки с разными прокси

Для параллельного скрапинга с разными IP нужно запускать отдельные экземпляры Chrome, так как --proxy-server применяется на уровне процесса, а не вкладки. Паттерн — пул браузеров с разными прокси:

import nodriver as uc
import asyncio

PROXY_HOST = "gate.proxyhat.com"
PROXY_PORT = 8080
PROXY_PASS = "your_api_key"

# Список стран для ротации
COUNTRIES = ["US", "DE", "GB", "FR", "JP"]
URLS = [
    "https://example.com/page1",
    "https://example.com/page2",
    "https://example.com/page3",
    "https://example.com/page4",
    "https://example.com/page5",
]

async def scrape_one(url: str, country: str, sem: asyncio.Semaphore):
    async with sem:
        proxy_user = f"user-country-{country}-session-{hash(url) % 100000}"
        browser = await uc.start(
            browser_args=[
                f"--proxy-server=http://{PROXY_HOST}:{PROXY_PORT}",
                "--disable-blink-features=AutomationControlled",
                "--no-sandbox",
            ],
            headless=True,
        )
        try:
            tab = await browser.get(url)
            await tab.select("body", timeout=20)
            title = await tab.evaluate("document.title")
            print(f"[{country}] {url} -> {title}")
            return title
        except Exception as e:
            print(f"[{country}] {url} FAILED: {e}")
            return None
        finally:
            browser.stop()

async def main():
    sem = asyncio.Semaphore(5)  # максимум 5 параллельных браузеров
    tasks = [
        scrape_one(url, country, sem)
        for url, country in zip(URLS, COUNTRIES)
    ]
    results = await asyncio.gather(*tasks)
    print(f"\nУспешно: {sum(1 for r in results if r)}/{len(results)}")

if __name__ == "__main__":
    asyncio.run(main())

Важно: 5 параллельных браузеров Chrome потребляют ~2.5 GB RAM и ~50% CPU на среднем VPS. Не запускайте 50+ браузеров на одной машине — используйте Docker-флот.

Docker-флот с Xvfb для headful-режима

Headful-режим в Docker требует виртуального дисплея. Пример Dockerfile:

FROM python:3.11-slim

RUN apt-get update && apt-get install -y \
    chromium \
    xvfb \
    dbus-x11 \
    && rm -rf /var/lib/apt/lists/*

RUN pip install nodriver

COPY scraper.py /app/scraper.py

# Запускаем Xvfb и скрапер
CMD ["sh", "-c", "Xvfb :99 -screen 0 1280x720x24 & DISPLAY=:99 python /app/scraper.py"]

Для масштабирования используйте Docker Compose или Kubernetes с per-pod прокси-конфигурацией. Каждый контейнер получает свой user-country-X-session-Y, что обеспечивает IP-разнообразие. Целевая конфигурация: 1 браузер на контейнер, 10–20 контейнеров на node, горизонтальное масштабирование через k8s HPA.

Graceful shutdown

nodriver не всегда корректно закрывает Chrome-процессы при исключениях. Оборачивайте в try/finally и используйте browser.stop():

async def safe_scrape(url, proxy_user):
    browser = await uc.start(
        browser_args=[f"--proxy-server=http://{PROXY_HOST}:{PROXY_PORT}"],
    )
    try:
        tab = await browser.get(url)
        await tab.select("body", timeout=20)
        return await tab.evaluate("document.title")
    except Exception as e:
        print(f"Error: {e}")
        return None
    finally:
        browser.stop()  # гарантированно закрываем Chrome

Для долгоживущих процессов добавьте signal-обработчик для SIGTERM, чтобы Kubernetes мог корректно останавливать поды.

Когда НЕ использовать nodriver: HTTP + curl_cffi дешевле

nodriver — тяжёлое оружие. Запуск Chrome требует ~500 MB RAM на экземпляр, ~200ms на старт, и полную JS-среду. Если сайт не защищён Cloudflare/Imperva или использует только простую JS-проверку, есть более дешёвые варианты:

ПодходRAMВремя стартаОбходит CloudflareСтоимость
nodriver + residential-прокси~500 MB~200msДа (Turnstile, JS challenge)Высокая
curl_cffi + residential-прокси~20 MB~10msИногда (TLS fingerprint)Низкая
httpx + datacenter-прокси~10 MB~5msНетМинимальная

Документация ProxyHat описывает, как использовать прокси с HTTP-клиентами напрямую — без браузера. Для SERP-tracking и price-monitoring на незащищённых сайтах это в 50 раз дешевле по ресурсам.

Правило выбора:

  • nodriver — Cloudflare Turnstile, Imperva, PerimeterX, DataDome, интерактивный JS (React/Vue SPA с динамическим рендерингом).
  • curl_cffi + прокси — сайты с TLS-fingerprint-проверкой, но без полноценного JS-challenge.
  • httpx + datacenter — API-эндпоинты, статичные HTML-страницы, внутренние системы.

См. также наши материалы по web-scraping и SERP-tracking для HTTP-first подходов.

Частые ошибки и edge cases

1. Headless = блокировка

Многие анти-бот-системы детектят HeadlessChrome в User-Agent и через фингерпринтинг (отсутствие WebGL, специфичные размеры окна). nodriver в headless-режиме всё ещё уязвим. Решение: headful + Xvfb в Docker, или --headless=new (Chrome 112+), который менее детектируем.

2. Один IP на все запросы

Запуск 100 вкладок через один прокси-IP — гарантированный бан. Используйте ротацию: разные session-ID для каждого браузера или per-request ротацию (без session-флага).

3. Игнорирование timeout

tab.select() по умолчанию ждёт бесконечно. Всегда передавайте timeout:

element = await tab.select(".data-container", timeout=15)

4. Утечка памяти при долгих сессиях

Chrome накапливает память в долгоживущих вкладках. Перезапускайте браузер каждые 50–100 страниц или используйте fresh-tab паттерн: закрывайте вкладку после каждой страницы.

5. SOCKS5 без DNS через прокси

При использовании SOCKS5 (socks5://gate.proxyhat.com:1080) DNS-запросы могут идти вне прокси, раскрывая ваш реальный IP. Используйте socks5h:// или флаг --host-resolver-rules для маршрутизации DNS через прокси.

Key Takeaways

  • nodriver — это не Selenium. Прямой CDP, async, патчи на уровне процесса. Нет WebDriver-следов = выше stealth.
  • Прокси обязательны. nodriver скрывает автоматизацию, но не IP. Residential-прокси от ProxyHat дают trust score, который пропускает анти-бот-фильтры.
  • Прокси через --proxy-server с аутентификацией в username: user-country-US-session-abc123. Порт 8080 для HTTP, 1080 для SOCKS5.
  • Масштабируйте через пул браузеров, а не вкладки. 1 браузер = 1 IP. Docker + Xvfb для headful в продакшене.
  • Не везде нужен браузер. curl_cffi + residential-прокси в 25–50 раз дешевле для сайтов без JS-challenge.
  • Этика. Только общедоступные данные, только с разрешения. CFAA и GDPR реальны.

Готовы начать? Изучите тарифы ProxyHat и доступные локации для residential-прокси, или загляните в документацию для деталей интеграции.

Готовы начать?

Доступ к более чем 50 млн резидентных IP в 148+ странах с AI-фильтрацией.

Смотреть ценыРезидентные прокси
← Вернуться в Блог