Аутентификация прокси в nodriver: три работающих решения

Первый результат по запросу аутентификации прокси в nodriver — обсуждение на GitHub, а не руководство. У nodriver нет встроенной поддержки user:pass — вот три работающих решения и однострочный ярлык, который многие упускают.

Поиск аутентификация прокси в nodriver и первые десять результатов — обсуждение на GitHub, демонстрационный репозиторий, несколько тем на Stack Overflow о другой библиотеке и один пост на Reddit — никакого реального руководства. Причина проста: nodriver, асинхронный CDP-преемник undetected-chromedriver (у этого проекта 12.8k звезд на GitHub и 1.3k форков), не имеет встроенного способа передачи user:pass прокси. Chrome игнорирует учетные данные, встроенные в флаг командной строки, и nodriver не исправляет это. Это руководство — та страница, в которую должно было превратиться обсуждение: что работает, что нет и три решения, которые позволяют запустить аутентифицированный прокси.

Простой прокси работает; аутентифицированный прокси — нет

Неаутентифицированный прокси — это однострочная команда. Передайте адрес через browser_args, и каждый запрос будет выходить с IP прокси:

import nodriver as uc

async def main():
    browser = await uc.start(
        browser_args=["--proxy-server=gate.quantumproxies.io:PORT"],
    )
    page = await browser.get("https://httpbin.org/ip")
    print(await page.get_content())   # shows the proxy exit IP

uc.loop().run_until_complete(main())

Теперь добавьте учетные данные — --proxy-server=http://USER:PASS@host:port — и это ломается. Chromium удаляет сегмент USER:PASS@, так как формат флага не предусматривает слот для учетных данных, затем прокси отвечает 407 Proxy Authentication Required, и Chrome вызывает нативный диалог входа, который находится вне DOM. nodriver не может его увидеть или заполнить. Этот 407 — та же стена, описанная в нашем руководстве по исправлению ошибок 407: прокси отвергает вас, а не браузер. Поэтому каждое реальное решение должно отвечать на вызов другим способом.

Решение 1: белый список IP — без учетных данных, без диалога

Это тот ярлык, о котором никогда не упоминают в обсуждениях на GitHub, и это самое простое решение. Если ваш скрейпер работает с машины со стабильным публичным IP, зарегистрируйте этот IP в панели управления вашего провайдера и полностью откажитесь от учетных данных — шлюз аутентифицирует вас по исходному адресу. Код nodriver остается простым фрагментом --proxy-server выше, без кода аутентификации. Каждый план QuantumProxies поддерживает белый список IP наряду с user:pass на своих резидентных прокси, поэтому это рекомендуемый путь, когда ваш выходной IP фиксирован. Его единственное ограничение — топологическое: он аутентифицирует машину, а не скрипт, поэтому эфемерные облачные раннеры, контейнеры за NAT и CI-боксы с изменяющимися IP нуждаются в одном из следующих двух решений.

Сравнение четырех маршрутов аутентификации прокси в nodriver: белый список IP, обработчик аутентификации CDP Fetch, сгенерированное расширение Chrome и локальный релей
Белый список не требует кода, когда ваш IP фиксирован; обработчик CDP и расширение отвечают на вызов учетных данных, когда это не так.

Решение 2: ответ на вызов с помощью обработчика CDP Fetch

nodriver напрямую использует протокол Chrome DevTools, поэтому вы можете перехватить вызов аутентификации в процессе — файл расширения не нужен. Включите домен Fetch с handle_auth_requests=True, затем ответьте на каждое событие AuthRequired с помощью continue_with_auth. Две детали, обе из ответа в обсуждении #1798, являются разницей между работающим и зависающим:

import asyncio
import nodriver as uc

PROXY = "gate.quantumproxies.io:PORT"   # host:port for --proxy-server
USER, PASS = "USER", "PASS"

class Scraper:
    def __init__(self):
        uc.loop().run_until_complete(self.run())

    async def run(self):
        browser = await uc.start(browser_args=[f"--proxy-server={PROXY}"])
        self.tab = await browser.get("draft:,")        # blank tab first

        # 1) handlers BEFORE enabling the Fetch domain
        self.tab.add_handler(uc.cdp.fetch.RequestPaused, self.on_request)
        self.tab.add_handler(uc.cdp.fetch.AuthRequired, self.on_auth)
        # 2) only now turn on interception with auth handling
        await self.tab.send(uc.cdp.fetch.enable(handle_auth_requests=True))

        page = await browser.get("https://httpbin.org/ip")
        await asyncio.sleep(3)
        print(await page.get_content())

    async def on_auth(self, event):
        # fire-and-forget: awaiting here deadlocks the loop
        asyncio.create_task(self.tab.send(uc.cdp.fetch.continue_with_auth(
            request_id=event.request_id,
            auth_challenge_response=uc.cdp.fetch.AuthChallengeResponse(
                response="ProvideCredentials", username=USER, password=PASS,
            ),
        )))

    async def on_request(self, event):
        asyncio.create_task(self.tab.send(
            uc.cdp.fetch.continue_request(request_id=event.request_id)))

if __name__ == "__main__":
    Scraper()

Одна оговорка, всплывшая в той же теме: пользователь обнаружил, что это работало на простых HTTP-страницах, но не на HTTPS, и виновником был низкокачественный прокси, а не код — переход на лучший выход это исправил. Это повторяющийся урок скрытого скрейпинга: обработчик отвечает на вызов, но репутация IP решает, пустит ли вас сайт.

Решение 3: сгенерированное расширение Chrome

Другой шаблон сообщества создает небольшое расширение Chrome при запуске, которое как устанавливает прокси, так и отвечает на вызов учетных данных через chrome.webRequest.onAuthRequired — тот же трюк, который работает в Selenium и Puppeteer. Вы пишете небольшой манифест и фоновый рабочий процесс в временный каталог и загружаете его через --load-extension:

import nodriver as uc

async def main():
    # ext_dir holds a Manifest V3 extension: manifest.json + worker.js that
    # calls chrome.proxy.settings.set(...) and returns authCredentials from
    # chrome.webRequest.onAuthRequired. Generate it once, then load it:
    browser = await uc.start(browser_args=[
        "--load-extension=" + ext_dir,
        "--headless=new",   # extensions only load in the NEW headless mode
    ])
    page = await browser.get("https://httpbin.org/ip")
    print(await page.get_content())

uc.loop().run_until_complete(main())

Полный манифест и рабочий процесс идентичны файлам Manifest V3 в нашем руководстве по аутентификации прокси в Selenium — скопируйте их дословно, меняется только вызов запуска. Две подводные камни повторяются везде: расширения загружаются только под --headless=new (простой --headless не работает), и распакованный каталог более надежен в разных версиях Chrome, чем упакованный zip. Расширение обрабатывает любой тип прокси, что делает его запасным вариантом, когда маршрут CDP сражается с вами.

Четвертый вариант: локальный релей

Если вы предпочитаете вообще не трогать nodriver, запустите небольшой локальный релей, который хранит учетные данные и предоставляет конечную точку без аутентификации на 127.0.0.1. nodriver затем указывает на адрес обратной связи с простым флагом и никогда не видит вызова. Это самый чистый путь для SOCKS5, где Chromium категорически отказывается от аутентифицированных прокси (отслеживается как ошибка Chromium 40829748). Мы рассматриваем минимальный релей, готовые инструменты и когда это излишне в руководстве по релею прокси.

Примечание о SOCKS5

Неаутентифицированный SOCKS5 работает через флаг — --proxy-server=socks5://host:port — но аутентифицированный SOCKS5 не работает, и никакой обработчик CDP вас не спасет, потому что Chromium никогда не поддерживал аутентификацию SOCKS5 с именем пользователя и паролем. Практические ответы те же три: включите IP в белый список, запустите релей, или используйте HTTP-эндпоинт провайдера. Каждый план QuantumProxies предоставляет как HTTP, так и SOCKS5 прокси на одном шлюзе, поэтому переключение на HTTP-эндпоинт часто является самым быстрым решением для SOCKS5. Для браузеров с антидетектом как семейства, карта аутентифицированных прокси сравнивает nodriver, zendriver и остальные бок о бок.

Схема, показывающая, как белый список IP на шлюзе прокси позволяет скрипту nodriver работать с простым флагом прокси и без диалога учетных данных
Когда выходной IP стабилен, белый список аутентифицирует машину, и код аутентификации полностью исчезает.

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

Поддерживает ли nodriver аутентифицированные прокси?

Не нативно. Вы можете передать неаутентифицированный прокси через browser_args=["--proxy-server=host:port"], но user:pass в этом флаге удаляется Chromium. Чтобы аутентифицироваться, вы либо включаете IP в белый список у провайдера, отвечаете на вызов с помощью обработчика CDP Fetch.AuthRequired, генерируете расширение Chrome для аутентификации прокси, либо запускаете локальный релей, который хранит учетные данные.

Почему мой обработчик аутентификации в nodriver не получает событий?

Почти всегда потому, что вы включили домен Fetch до регистрации обработчиков. Внутренний enable в nodriver переопределяет регистрацию, поэтому события никогда не достигают вашего обратного вызова. Сначала добавьте обработчики RequestPaused и AuthRequired, затем вызовите fetch.enable(handle_auth_requests=True). Также оберните свои ответы в asyncio.create_task, чтобы ожидание их не могло блокировать цикл.

Может ли nodriver использовать SOCKS5 прокси с именем пользователя и паролем?

Нет. Chromium не поддерживает аутентифицированный SOCKS5 (ошибка Chromium 40829748), и nodriver наследует это ограничение. Неаутентифицированный SOCKS5 работает через --proxy-server=socks5://host:port. Для аутентифицированного SOCKS5 включите IP в белый список, запустите локальный релей, который добавляет учетные данные, или переключитесь на HTTP-эндпоинт провайдера, который чисто обрабатывает Basic auth.

nodriver или zendriver для аутентифицированных прокси?

Оба имеют тот же пробел и те же решения, потому что zendriver — это форк сообщества nodriver. У zendriver более активный трекер проблем, где вопрос аутентификации открыто обсуждается, но рабочие методы идентичны. Если вы используете форк, настройка, специфичная для форка, зеркально отражает все здесь — обработчик CDP и белый список работают одинаково.

Честное резюме: nodriver не будет выполнять аутентификацию прокси за вас, и это нормально, когда вы знаете карту. Используйте белый список, когда ваш IP стабилен, обращайтесь к обработчику CDP или расширению, когда это не так, и держите релей в запасе для SOCKS5. Код выше отвечает на вызов — но чистый резидентный выход — это то, что действительно позволяет вам пройти через дверь.

Запустите nodriver на резидентных IP в белом списке