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

Практическое руководство по nodriver — преемнику undetected-chromedriver: архитектура через CDP, идиоматичный async API, настройка residential-прокси через ProxyHat, масштабирование и этика.

Scraping With nodriver: A Practical Guide to Undetected Async Browser Automation
В этой статье

Скрапинг с nodriver — это следующий шаг для Python-инженеров, которые переросли undetected-chromedriver. nodriver (ранее undetected-chromedriver v2) полностью отказался от Selenium и WebDriver, управляя Chrome напрямую через Chrome DevTools Protocol (CDP) по веб-сокетам. Результат: меньше утечек, выше скорость, нативная асинхронность и более устойчивое прохождение Cloudflare, Imperva и Datadome.

В этом руководстве мы разберём архитектуру nodriver, идиоматичный API, подключение residential-прокси ProxyHat, масштабирование в Docker и границы применимости. Сначала — важное предупреждение.

Этика и право. Материал предназначен для авторизованной автоматизации и сбора публичных данных. Несанкционированный доступ может нарушать CFAA (США) и GDPR (ЕС). Всегда проверяйте robots.txt, ToS сайта и наличие прав на сбор данных. ProxyHat не поощряет нарушение чужих политик.

Почему скрапинг с nodriver обходит защиту лучше, чем Selenium

undetected-chromedriver (UC) годами был стандартом де-факто для обхода Cloudflare Turnstile. Но он унаследовал архитектуру Selenium: ChromeDriver как посредник, который выставляет navigator.webdriver = true и оставляет десятки CDP-артефактов. Современные WAF-провайдеры давно научились их детектить.

nodriver убирает посредника полностью:

  • Нет ChromeDriver. Браузер запускается напрямую через subprocess, CDP-подключение открывается по ws://. Это устраняет сам класс утечек, связанных с WebDriver.
  • navigator.webdriver отсутствует — nodriver патчит это на уровне запуска, а не через JS-инъекцию post-load.
  • Нет CDP-следов вроде Runtime.enable и Page.enable, которые Selenium-драйверы оставляют в порядке вызовов.
  • Асинхронность из коробки. nodriver построен на asyncio — никаких WebDriverWait и блокирующих вызовов.

На практике это даёт заметный прирост success rate против защищённых страниц. По данным сообщества и бенчмарков, nodriver проходит базовый Cloudflare-челлендж в 85–95% случаев без дополнительных манипуляций, тогда как классический Selenium+UC колеблется в районе 60–75%.

Архитектура nodriver: CDP без Selenium

nodriver — это тонкая Python-обёртка над CDP. В отличие от Playwright, который тоже использует CDP, nodriver фокусируется на минимизации следов автоматизации и не добавляет собственный runtime-слой.

Ключевые компоненты

  • uc.start() — точка входа. Запускает Chrome-процесс, подключается к CDP-эндпоинту, возвращает объект Browser.
  • Browser — представляет браузер. Управляет вкладками (Tab), глобальными событиями и контекстом.
  • Tab — одна вкладка/страница. Все взаимодействия происходят через методы tab: await tab.get(url), await tab.select("#content"), await tab.find("text").
  • Event hooks — вместо WebDriverWait nodriver использует декораторы событий CDP: @browser.event, tab.add_handler. Вы подписываетесь на Network.responseReceived, Page.domContentEventFired и т.д.

Это означает, что весь ваш скрапинг-код — это корутины, а не линейные блокирующие вызовы. Вы можете держать 20–50 параллельных вкладок в одном браузере, не прибегая к multiprocessing.

Идиоматичный async API nodriver

Рассмотрим базовый пример — запуск браузера, навигация и извлечение текста.

import nodriver as uc
import asyncio

async def main():
    browser = await uc.start(
        headless=False,
        browser_args=[
            "--disable-blink-features=AutomationControlled",
            "--no-first-run",
            "--no-default-browser-check",
        ],
    )
    tab = await browser.get("https://example.com")

    # Ждём появления элемента — без WebDriverWait
    element = await tab.select("h1", timeout=10)
    if element:
        text = await element.get_text()
        print("Заголовок:", text)

    browser.stop()

asyncio.run(main())

Обратите внимание: нет driver.get(), нет driver.find_element(By.CSS, ...). nodriver использует tab.select() для CSS-селекторов и tab.find() для поиска по тексту. Таймаут передаётся как аргумент — это корутина, а не неявное ожидание.

Событийно-ориентированный подход вместо поллинга

import nodriver as uc

async def main():
    browser = await uc.start()
    tab = await browser.get("https://httpbin.org/json")

    # Подписываемся на сетевые ответы
    async def on_response(event):
        if event.response and "application/json" in event.response.get("mimeType", ""):
            print("JSON получен:", event.response.get("url"))

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

    await tab.wait_for("body")
    browser.stop()

asyncio.run(main())

Это идиоматичный nodriver: вы реагируете на события CDP, а не опрашиваете DOM в цикле. Для скрапинга SPA-страниц это критично — вы можете перехватывать XHR/fetch-ответы напрямую, минуя рендеринг.

Настройка nodriver proxy через ProxyHat

nodriver не имеет встроенного ротатора прокси. Вы передаёте --proxy-server= в browser_args, а аутентификацию обрабатываете через CDP Fetch.authRequired или расширение. Для ProxyHat это выглядит так:

Формат подключения ProxyHat

  • HTTP: http://USERNAME:PASSWORD@gate.proxyhat.com:8080
  • SOCKS5: socks5://USERNAME:PASSWORD@gate.proxyhat.com:1080
  • Гео-таргетинг в username: user-country-US-session-abc123:pass

Поскольку --proxy-server= не поддерживает inline-аутентификацию для HTTP-прокси, nodriver использует CDP-событие Fetch.authRequired для подстановки credentials. Residential-прокси здесь критичны: datacenter-IP почти гарантированно получат Cloudflare-челлендж, даже если браузер полностью «чист».

Полный пример: nodriver + residential-прокси

import nodriver as uc
import asyncio
import json

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

async def main():
    proxy_arg = f"--proxy-server=http://{PROXY_HOST}:{PROXY_PORT}"

    browser = await uc.start(
        headless=False,
        browser_args=[
            proxy_arg,
            "--disable-blink-features=AutomationControlled",
            "--no-first-run",
        ],
    )
    tab = await browser.get("https://example.com")

    # Обработка прокси-аутентификации через CDP Fetch domain
    async def on_auth(event):
        params = event
        await tab.send(uc.cdp.fetch.continue_with_auth(
            request_id=params.request_id,
            auth_challenge_response=uc.cdp.fetch.AuthChallengeResponse(
                response="ProvideCredentials",
                username=PROXY_USER,
                password=PROXY_PASS,
            ),
        ))

    tab.add_handler(uc.cdp.fetch.AuthRequired, on_auth)
    await tab.send(uc.cdp.fetch.enable())

    # Навигация на защищённую страницу
    await tab.get("https://httpbin.org/json")
    await tab.wait_for("body")

    body_text = await tab.get_content()
    data = json.loads(body_text)
    print("Результат:", json.dumps(data, indent=2))

    browser.stop()

asyncio.run(main())

В этом примере мы:

  1. Запускаем nodriver с --proxy-server, указывающим на gate.proxyhat.com:8080.
  2. Включаем CDP Fetch.enable для перехвата запросов.
  3. Подписываемся на Fetch.authRequired и подставляем credentials из ProxyHat.
  4. Используем sticky-сессию session-abc123 с гео country-US — один IP на весь сеанс.

Для ротации IP меняйте идентификатор сессии при каждом запуске или через N запросов. Подробнее о ротации прокси.

Масштабирование nodriver: конкурентность, Docker, per-context прокси

Параллельные вкладки

nodriver поддерживает несколько вкладок в одном браузере. Для I/O-интенсивного скрапинга это эффективнее, чем запуск отдельных процессов:

import nodriver as uc
import asyncio

URLS = [
    "https://httpbin.org/ip",
    "https://httpbin.org/headers",
    "https://httpbin.org/user-agent",
    "https://httpbin.org/json",
]

async def scrape(tab, url):
    await tab.get(url)
    await tab.wait_for("body")
    return await tab.get_content()

async def main():
    browser = await uc.start(
        browser_args=[
            f"--proxy-server=http://gate.proxyhat.com:8080",
            "--disable-blink-features=AutomationControlled",
        ],
    )

    async def on_auth(event):
        await tab.send(uc.cdp.fetch.continue_with_auth(
            request_id=event.request_id,
            auth_challenge_response=uc.cdp.fetch.AuthChallengeResponse(
                response="ProvideCredentials",
                username="user-country-US-session-batch01",
                password="your_password",
            ),
        ))

    # Создаём вкладки
    tabs = [await browser.get(URLS[0], new_tab=True) for _ in URLS]
    for t in tabs:
        t.add_handler(uc.cdp.fetch.AuthRequired, on_auth)
        await t.send(uc.cdp.fetch.enable())

    # Запускаем конкурентно
    results = await asyncio.gather(*[scrape(t, u) for t, u in zip(tabs, URLS)])
    for url, r in zip(URLS, results):
        print(f"{url}: {len(r)} bytes")

    browser.stop()

asyncio.run(main())

На 8-ядерной машине можно держать 30–50 активных вкладок с residential-прокси, если каждый запрос занимает 2–5 секунд. Память — ~150–300 МБ на вкладку, планируйте контейнер соответственно.

Headless-флот в Docker

Для продакшена упакуйте nodriver в Docker-образ. Базовый Dockerfile:

FROM python:3.12-slim

# Chrome и зависимости
RUN apt-get update && apt-get install -y \
    chromium \
    chromium-driver \
    fonts-liberation \
    libnss3 \
    libxss1 \
    libasound2 \
    libgbm1 \
    && rm -rf /var/lib/apt/lists/*

ENV CHROME_PATH=/usr/bin/chromium

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY scraper.py .
CMD ["python", "scraper.py"]

Запуск с лимитами ресурсов:

docker run --rm \
  --memory=2g \
  --cpus=2 \
  --shm-size=1g \
  -e PROXYHAT_USER=$PROXYHAT_USER \
  -e PROXYHAT_PASS=$PROXYHAT_PASS \
  nodriver-scraper

--shm-size=1g обязателен — Chrome активно использует /dev/shm, и дефолтные 64 МБ приведут к крашам вкладок.

Per-context прокси-назначение

Для разных задач — разные сессии. Назначайте сессии через переменные окружения или конфиг:

import os
import nodriver as uc
import asyncio

SESSIONS = {
    "serp": os.getenv("SERP_SESSION", "serp-prod-001"),
    "prices": os.getenv("PRICE_SESSION", "prices-prod-001"),
    "social": os.getenv("SOCIAL_SESSION", "social-prod-001"),
}

async def launch_context(task_type, country="US"):
    session_id = SESSIONS[task_type]
    username = f"user-country-{country}-session-{session_id}"
    proxy_arg = f"--proxy-server=http://gate.proxyhat.com:8080"

    browser = await uc.start(
        headless=True,
        browser_args=[proxy_arg, "--disable-blink-features=AutomationControlled"],
    )
    tab = await browser.get("about:blank")

    async def on_auth(event):
        await tab.send(uc.cdp.fetch.continue_with_auth(
            request_id=event.request_id,
            auth_challenge_response=uc.cdp.fetch.AuthChallengeResponse(
                response="ProvideCredentials",
                username=username,
                password=os.getenv("PROXYHAT_PASS"),
            ),
        ))

    tab.add_handler(uc.cdp.fetch.AuthRequired, on_auth)
    await tab.send(uc.cdp.fetch.enable())
    return browser, tab

asyncio.run(launch_context("serp", country="DE"))

Это даёт sticky-сессию на задачу: SERP-скрапинг держит один IP для консистентности гео, price-monitoring — другой для распределения нагрузки. См. доступные локации ProxyHat.

Graceful shutdown

import signal

browser = None

async def shutdown(sig):
    print(f"Получен {sig.name}, закрываем браузер...")
    if browser:
        browser.stop()
    loop.stop()

loop = asyncio.get_event_loop()
for sig in (signal.SIGINT, signal.SIGTERM):
    loop.add_signal_handler(sig, lambda s=sig: asyncio.create_task(shutdown(s)))

Это критично в Docker — без graceful shutdown вы теряете незавершённые запросы и оставляете zombie-процессы Chrome.

Когда НЕ нужен nodriver: HTTP + curl_cffi дешевле

nodriver — тяжёлый инструмент. Каждый экземпляр Chrome потребляет 200–500 МБ RAM и требует полноценного браузерного окружения. Для многих задач это избыточно.

Задача Инструмент Стоимость (RAM/запрос)
Cloudflare Turnstile / JS-челлендж nodriver + residential ~300 МБ, 3–8 сек
SERP-сбор (Google, Bing) nodriver или SERP API ~300 МБ / ~50 мс (API)
REST API с TLS-fingerprint проверкой curl_cffi + residential ~5 МБ, 200–500 мс
Простой HTML без защиты httpx + residential ~5 МБ, 100–300 мс
Динамический SPA без WAF Playwright + residential ~200 МБ, 2–5 сек

Правило: если сайт не требует выполнения JS для рендеринга данных и не стоит за Cloudflare/Imperva, используйте curl_cffi с residential-прокси — это в 50–100 раз дешевле по памяти и в 10–20 раз быстрее. nodriver нужен именно там, где без реального браузера не обойтись.

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

  • Забыли Fetch.enable() перед навигацией. Без этого AuthRequired не сработает, и запросы зависнут или уйдут без прокси. Включайте Fetch ДО первого tab.get().
  • Headless=True на Cloudflare. Полный headless всё ещё детектится некоторыми WAF. Используйте headless=False в связке с Xvfb в Docker, либо новый headless-режим Chrome (--headless=new).
  • Один IP на все вкладки. Если вы используете одну сессию для 50 вкладок, Cloudflare увидит аномальный паттерн. Назначайте разные session-XXX на пул вкладок.
  • Нет timeout на tab.select(). Без явного таймаута корутина может висеть бесконечно. Всегда передавайте timeout=10.
  • Игнорирование robots.txt. Даже с residential-прокси игнорирование robots.txt может трактоваться как нарушение добросовестности. См. лучшие практики скрапинга.

ProxyHat-специфичная настройка

ProxyHat предоставляет residential, mobile и datacenter-прокси. Для nodriver рекомендуется именно residential:

  • Residential-IP не попадают в дата-центровые блок-листы Cloudflare и Imperva.
  • Гео-таргетинг до страны и города: user-country-DE-city-berlin.
  • Sticky-сессии через session-XXX — один IP на весь сеанс, до 30 минут.
  • Поддержка HTTP и SOCKS5 — для nodriver используйте HTTP (:8080), SOCKS5 (:1080) для curl_cffi и httpx.

См. тарифы ProxyHat и официальную документацию для актуальных параметров.

Key Takeaways

  • nodriver — полностью async, без Selenium/WebDriver, через CDP по веб-сокетам. Меньше утечек, выше success rate против Cloudflare.
  • Идиоматичный API: uc.start(), Browser, Tab, event hooks вместо WebDriverWait.
  • Прокси настраивается через --proxy-server= + CDP Fetch.authRequired. nodriver не имеет встроенного ротатора.
  • Residential-прокси (ProxyHat gate.proxyhat.com:8080) обязательны для headful stealth — datacenter-IP детектятся.
  • Масштабирование: 30–50 вкладок на браузер, Docker с --shm-size=1g, per-context сессии.
  • Не используйте nodriver для простых задач — curl_cffi + residential в 50–100 раз дешевле.

FAQ

Что такое скрапинг с nodriver?

nodriver — это Python-библиотека для управления Chrome через Chrome DevTools Protocol без Selenium и WebDriver. Она является преемником undetected-chromedriver и обеспечивает полностью асинхронный API, минимизируя следы автоматизации (navigator.webdriver, CDP-утечки), что повышает success rate против Cloudflare, Imperva и Datadome.

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

nodriver не имеет встроенного ротатора прокси — вы должны подключать residential-прокси через --proxy-server и CDP Fetch.authRequired. Residential-прокси критичны, потому что даже «чистый» браузер с datacenter-IP почти гарантированно получает Cloudflare-челлендж. Прокси с гео-таргетингом и sticky-сессиями позволяют эмулировать реального пользователя из нужной локации.

Какой тип прокси лучше для nodriver?

Residential-прокси. Они используют IP реальных ISP, поэтому не попадают в дата-центровые блок-листы WAF. Для nodriver через ProxyHat используйте HTTP-эндпоинт gate.proxyhat.com:8080 с аутентификацией через CDP Fetch.authRequired. SOCKS5 (:1080) подходит, если вы комбинируете nodriver с curl_cffi или httpx в одном пайплайне.

Как избежать блокировок при скрапинге с nodriver?

Используйте residential-прокси с ротацией сессий, не запускайте headless=True на Cloudflare-защищённых сайтах (используйте Xvfb или --headless=new), назначайте разные session-ID на пулы вкладок, соблюдайте таймауты на tab.select(), и не превышайте разумную частоту запросов — 1–3 запроса в секунду на IP. Проверяйте robots.txt и ToS сайта.

Можно ли использовать nodriver с SOCKS5-прокси ProxyHat?

Да. Для SOCKS5 укажите --proxy-server=socks5://gate.proxyhat.com:1080 и используйте тот же механизм CDP Fetch.authRequired для аутентификации. SOCKS5 может быть полезен для задач, где важна совместимость с curl_cffi или httpx в одном скрипте, но для большинства nodriver-сценариев HTTP на порту 8080 работает идентично.

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

Что такое скрапинг с nodriver?

nodriver — это Python-библиотека для управления Chrome через Chrome DevTools Protocol без Selenium и WebDriver. Она является преемником undetected-chromedriver и обеспечивает полностью асинхронный API, минимизируя следы автоматизации (navigator.webdriver, CDP-утечки), что повышает success rate против Cloudflare, Imperva и Datadome.

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

nodriver не имеет встроенного ротатора прокси — вы должны подключать residential-прокси через --proxy-server и CDP Fetch.authRequired. Residential-прокси критичны, потому что даже чистый браузер с datacenter-IP почти гарантированно получает Cloudflare-челлендж. Прокси с гео-таргетингом и sticky-сессиями позволяют эмулировать реального пользователя из нужной локации.

Какой тип прокси лучше для nodriver?

Residential-прокси. Они используют IP реальных ISP, поэтому не попадают в дата-центровые блок-листы WAF. Для nodriver через ProxyHat используйте HTTP-эндпоинт gate.proxyhat.com:8080 с аутентификацией через CDP Fetch.authRequired. SOCKS5 (:1080) подходит, если вы комбинируете nodriver с curl_cffi или httpx в одном пайплайне.

Как избежать блокировок при скрапинге с nodriver?

Используйте residential-прокси с ротацией сессий, не запускайте headless=True на Cloudflare-защищённых сайтах (используйте Xvfb или --headless=new), назначайте разные session-ID на пулы вкладок, соблюдайте таймауты на tab.select(), и не превышайте разумную частоту запросов — 1–3 запроса в секунду на IP. Проверяйте robots.txt и ToS сайта.

Можно ли использовать nodriver с SOCKS5-прокси ProxyHat?

Да. Для SOCKS5 укажите --proxy-server=socks5://gate.proxyhat.com:1080 и используйте тот же механизм CDP Fetch.authRequired для аутентификации. SOCKS5 может быть полезен для задач, где важна совместимость с curl_cffi или httpx в одном скрипте, но для большинства nodriver-сценариев HTTP на порту 8080 работает идентично.

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

Резидентные, ISP и мобильные прокси в 148+ странах. Создайте бесплатный аккаунт.

Создать бесплатный аккаунт
← Вернуться в Блог