Скрапинг с 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-abc123 | US 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-прокси, или загляните в документацию для деталей интеграции.






