Скрапинг с 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 — вместо
WebDriverWaitnodriver использует декораторы событий 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())
В этом примере мы:
- Запускаем nodriver с
--proxy-server, указывающим наgate.proxyhat.com:8080. - Включаем CDP
Fetch.enableдля перехвата запросов. - Подписываемся на
Fetch.authRequiredи подставляем credentials из ProxyHat. - Используем 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=+ CDPFetch.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 работает идентично.






