Selenium: ротация прокси в Python: ChromeOptions, аутентификация и ротация на сессию
Selenium привязывает прокси к запуску браузера, а не к запросу — из-за этого ротация становится неудобной, а аутентифицированная ротация неудобна по-другому. Вот три подхода, которые действительно работают в Selenium 4, с кодом на Python для ChromeOptions, an a
В Selenium нет понятия ротирующего прокси. Сессия браузера и выходной IP привязаны друг к другу при запуске: вы передаёте --proxy-server= в ChromeOptions, драйвер запускается, и каждый запрос, который делает браузер, идёт через эту конечную точку, пока вы не закроете его.
Этот единственный факт дизайна объясняет, почему поиск по запросу selenium rotating proxy выдаёт так много противоречивых советов. Одни руководства перезапускают браузер для каждого прокси. Другие обращаются к библиотеке-обёртке. Оба способа работают — но они ломаются в разных местах, и то, что чаще всего разрушает первые попытки, — это аутентификация, а не сама ротация.
В этом руководстве рассматриваются механизмы применительно к Chrome и Chromium, управляемым Selenium 4 в Python: что на самом деле принимают флаги прокси, три рабочих способа ротации, как проверить выходной IP и подводные камни, которые отнимают больше всего времени.
Почему ротация в Selenium отличается от requests, httpx или Scrapy
requestsиhttpxпринимают прокси как аргумент для каждого вызова. Ротация — это изменение словаря.- Scrapy может менять прокси между запросами в промежуточном ПО загрузчика, без перезапуска сессии.
- Selenium устанавливает прокси на процесс браузера. Поддерживаемого аргумента прокси для каждого запроса нет, поэтому для ротации в середине сессии требуется слой перехвата, такой как selenium-wire, или работа с DevTools Protocol.
Второе ограничение — это собственный флаг Chrome. --proxy-server=socks5://host:port принимает схему, хост и порт — и ничего больше. Поля для учётных данных нет. Если прокси требует аутентификации, Chrome показывает диалог HTTP-аутентификации, который Selenium не может заполнить и который в headless-режиме вообще не отображается. Некоторые конечные точки SOCKS5 аналогично отказывают в соединении без учётных данных, которые флагу негде передать.
Таким образом, есть две проблемы, которые нужно решать отдельно: ротация выходного IP и передача учётных данных прокси.
Сравнение трёх подходов
| Подход | Гранулярность ротации | Работа с учётными данными | Где болит |
|---|---|---|---|
| Перезапуск драйвера для каждого прокси | На сессию браузера | Нет — в паре с добавлением IP в белый список | Каждая ротация стоит запуска браузера |
Расширение для аутентификации через --load-extension |
На сессию браузера | Да | Возня с расширениями и причуды headless-режима |
| selenium-wire | На запрос | Да | Дополнительная зависимость плюс локальный слой перехвата |
Большинству задач автоматизации браузера не нужна ротация на каждый запрос. Если ваш рабочий процесс — открыть страницу, дождаться JavaScript, извлечь данные, перейти дальше, то ротации на сессию обычно достаточно, и её гораздо проще отлаживать.
Подход 1: перезапуск драйвера для каждого прокси
Это наименее волшебный вариант и самый простой для осмысления в production.
import random
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
# Диапазоны из RFC 5737 — замените на свои эндпоинты.
PROXIES = [
'http://198.51.100.10:8080',
'http://198.51.100.11:8080',
'socks5://203.0.113.7:1080',
]
def build_driver(proxy_url: str) -> webdriver.Chrome:
options = Options()
options.add_argument(f'--proxy-server={proxy_url}')
options.add_argument('--headless=new')
options.add_argument('--disable-dev-shm-usage')
return webdriver.Chrome(options=options)
def exit_ip(driver: webdriver.Chrome) -> str:
driver.get('https://api.ipify.org')
return driver.find_element(By.TAG_NAME, 'body').text.strip()
for proxy in random.sample(PROXIES, len(PROXIES)):
driver = build_driver(proxy)
try:
print(f'{proxy} -> {exit_ip(driver)}')
finally:
driver.quit()
Три привычки отличают работающий скрипт от того, который загадочно умирает за ночь:
- Всегда вызывайте quit в блоке
finally. Осиротевшие процессы chromedriver удерживают память и блокировки файлов. - Дайте каждому параллельному воркеру собственный каталог профиля с
--user-data-dir=/tmp/chrome-worker-1. Два экземпляра Chrome, использующие один профиль, будут соперничать за блокировку. - Используйте
--no-sandboxтолько в контейнерах, которые вы контролируете, и никогда — в обычной настольной сессии.
Этот подход чисто работает с неаутентифицированными прокси, и здесь на сцену выходит добавление IP в белый список.
Подход 2: передача учётных данных в браузер
Вариант A: добавьте свой исходящий IP в белый список
Многие провайдеры позволяют авторизовать IP, с которого вы обращаетесь; тогда прокси принимает вас без имени пользователя и пароля, и Подход 1 работает без изменений. Два предостережения: ваш исходящий IP должен быть стабильным, что неудобно в CI или при домашнем подключении, которое переподключается, а IP, добавленный в белый список, — это постоянная авторизация, которую нужно не забыть отозвать.
Вариант B: расширение, отвечающее на запрос аутентификации
Расширения Chrome могут предоставлять учётные данные прокси через chrome.webRequest.onAuthRequired. Минимальная пара для Manifest V3 выглядит так.
{
"manifest_version": 3,
"name": "proxy-auth",
"version": "1.0",
"permissions": ["webRequest", "webRequestAuthProvider"],
"host_permissions": ["<all_urls>"],
"background": { "service_worker": "background.js" }
}
chrome.webRequest.onAuthRequired.addListener(
(details, callback) => {
callback({ authCredentials: { username: 'USER', password: 'PASS' } });
},
{ urls: ['<all_urls>'] },
['asyncBlocking']
);
Загрузите его при запуске драйвера:
options.add_argument('--disable-extensions-except=/opt/proxy-auth')
options.add_argument('--load-extension=/opt/proxy-auth')
Две вещи, которые нужно знать, прежде чем строить на этом. Обработка обратного вызова аутентификации менялась между релизами Chrome; если форма с callback отклоняется, возвращайте объект с учётными данными из слушателя вместо вызова callback(). И загрузка расширений в headless-режиме — это та часть, которая с наибольшей вероятностью преподнесёт сюрприз, — используйте --headless=new, а не устаревший headless-режим.
Вариант C: selenium-wire
Если вам действительно нужна ротация на каждый запрос или в середине сессии, selenium-wire ставит небольшой локальный прокси перед браузером и позволяет задать вышестоящий прокси в Python.
from selenium.webdriver.common.by import By
from seleniumwire import webdriver
proxy = 'http://USER:[email protected]:8080'
wire_options = {
'proxy': {
'http': proxy,
'https': proxy,
'no_proxy': 'localhost,127.0.0.1',
}
}
driver = webdriver.Chrome(seleniumwire_options=wire_options)
try:
driver.get('https://api.ipify.org')
print(driver.find_element(By.TAG_NAME, 'body').text)
finally:
driver.quit()
Поскольку задействован локальный слой перехвата, фиксируйте версии selenium-wire и Selenium вместе и перепроверяйте после обновления любой из них.
Нужен ли SOCKS5 именно для Selenium?
Обычно нет, но он поддерживается. Chrome принимает socks5://host:port в --proxy-server, и SOCKS5 передаёт трафик на уровне сокета, что полезно, когда ваш пул состоит только из SOCKS. Ограничение с учётными данными такое же: флагу некуда поместить имя пользователя, так что вы возвращаетесь к белому списку или расширению.
Одно уточнение, важное для проверки: маршрутизация браузера через SOCKS5 управляет TCP-трафиком. Сама по себе она не мешает WebRTC открыть непроксированный UDP-путь. См. контрольный список ниже.
Выбор стратегии ротации для автоматизации браузера
| Стратегия | Триггер ротации | Подходит для | Компромиссы |
|---|---|---|---|
| Липкая сессия на задачу | В начале задачи | Входы в систему, многошаговые сценарии, страницы с пагинацией | Один IP поглощает всю задачу |
| На каждые N страниц или временное окно | Счётчик или таймер | Широкий краулинг, страницы со списками | Ротация в середине процесса может сломать состояние |
| На сессию браузера | При каждом запуске драйвера | Простой скрапинг и скриншотные задачи | Стоимость запуска доминирует во времени выполнения |
| Один прокси на воркер | При запуске воркера | Параллельные краулы | Нагрузку на IP нужно отслеживать |
Поскольку сессия браузера дорого обходится, ротация на каждый запрос обычно является неправильным выбором по умолчанию для Selenium — вы платите перезапуском процесса за каждый URL. Согласованность местоположения также важнее, чем чистый объём IP, для результатов, зависящих от географии. Если вы собираете выборку локализованных страниц, витрин или цен для конкретного рынка, фиксируйте регион выхода на всю сессию, иначе собранные числа не будут воспроизводимы при следующем запуске.
Проверка выходного IP, DNS и WebRTC
Проверка не является опциональной для headless-запусков, потому что нет окна, на которое можно взглянуть. Проверьте три вещи.
1. Выходной IP действительно изменился. Используйте повторно вспомогательную функцию exit_ip выше и убедитесь, что значение отличается от вашего собственного адреса. Иногда сравнивайте со второй конечной точкой echo, чтобы вас не обманул кэшированный ответ.
2. Поведение при разрешении DNS. При HTTP-прокси имя хоста обычно разрешается прокси, и это то, что вам нужно. Обработку SOCKS5 стоит подтвердить, а не предполагать: откройте публичную страницу проверки утечек DNS в не-headless-запуске и сравните показанный резолвер с резолвером вашего провайдера.
3. WebRTC. Chrome может использовать непроксированный UDP-путь, даже когда прокси настроен. Флаг ниже заставляет WebRTC избегать непроксированного UDP.
options.add_argument('--force-webrtc-ip-handling-policy=disable_non_proxied_udp')
Наконец, записывайте в лог используемый прокси и наблюдаемый выходной IP для каждой сессии. Когда страница возвращает запрос на прохождение проверки, неверный язык или цену, которую вы не ожидали, эта строка лога — первое, что вам понадобится.
Подводные камни, которые отнимают больше всего времени
- Утечка драйверов. Каждый осиротевший chromedriver удерживает память. В контейнере это обычно выглядит как загадочный сбой, а не как утечка. Используйте
try/finallyвезде. - Общие каталоги профилей. Параллельные экземпляры, использующие один каталог пользовательских данных, блокируют друг друга. Один каталог на воркер.
- Ошибочное восприятие сбоев как блокировок. 403, пустой ответ и страница с запросом на прохождение проверки — это три разные проблемы. Логируйте код состояния, заголовок страницы и длину тела, прежде чем винить прокси.
- Экстраполяция по одной странице. Проверьте выходной IP, прежде чем автоматизировать сотни страниц, а не после.
- Забывание нетехнических правил. Ротация IP не меняет того, что разрешают условия обслуживания сайта, его robots.txt или применимое законодательство о защите данных, — особенно если вы собираете персональные данные в юрисдикции GDPR. Сбавьте темп, отступайте при ошибках и поддерживайте умеренную конкурентность.
Краткая версия
- Selenium привязывает прокси к запуску браузера, поэтому ротация означает перезапуски, если только вы не добавите слой перехвата.
- Флаг Chrome
--proxy-serverне может нести учётные данные. Выбирайте добавление IP в белый список, расширение для аутентификации или selenium-wire. - Для автоматизации браузера предпочитайте ротацию на сессию или липкую ротацию, а не ротацию на каждый запрос.
- Проверяйте выходной IP внутри сессии, отключайте непроксированный UDP WebRTC и логируйте, какой прокси обслужил какой запрос.
- Завершайте драйвер в
finally, изолируйте каталоги профилей для каждого воркера и докажите работоспособность настройки на одном URL, прежде чем масштабировать её.