Как решать капчу в Selenium Grid (RemoteWebDriver)

Решение капчи в Selenium Grid работает ровно так же, как локальное, с одним отличием, на котором спотыкается большинство. Браузер работает на ноде. Ваш тестовый код выполняется не там. Вызов CapSkip происходит в вашем тестовом процессе, поэтому решатель должен быть доступен оттуда, где вы запускаете pytest, а не с машины, где живёт браузер. Поймите этот единственный факт правильно, и всё остальное здесь будет тем же кодом, который вы написали бы для локального Chrome.
Что понадобится
- Доступный вам Selenium Grid. Standalone, хаб с нодами и полностью распределённая схема со стороны клиента ведут себя одинаково.
- CapSkip, запущенный на машине с Windows и доступный с машины, которая выполняет ваши тесты.
- Адрес Grid. Selenium 4 по умолчанию слушает запросы RemoteWebDriver на порту 4444.
- Sitekey и URL страницы тестируемого сайта.
Какая машина на самом деле общается с решателем
Нарисуйте три блока, прежде чем что-то настраивать, потому что два из них выглядят взаимозаменяемыми, но таковыми не являются.
| Какая машина | Что там работает | Общается ли она с CapSkip? |
|---|---|---|
| Ваш тест-раннер | pytest, вызов SDK, RemoteWebDriver | Да. Это единственная, которая общается |
| Хаб Grid | Маршрутизация и очередь сессий | Нет. Он только маршрутизирует сессии |
| Нода с браузером | Собственно браузер | Нет. Она никогда не видит решатель |
Люди на удивление часто разводят это наоборот, обычно потому, что в Docker работают как раз ноды Grid, а проблему с сетью они ожидают именно там. На ноде проблемы с сетью нет. Токен приходит туда по обычному протоколу WebDriver, как аргумент команды выполнения скрипта, ровно так же, как любая другая строка.
Поэтому нужный режим подключения следует из того, где выполняются ваши тесты. CapSkip предлагает Local, который слушает 127.0.0.1 и обслуживает только это устройство, и Server, который слушает ваш сетевой адрес или публичный IP, поэтому другая машина, хост контейнеров или CI-раннер могут обратиться к той же машине Windows по API. Оба живут в разделе Настройки подключения. Server mode меняет только то, где работает решатель, и больше ничего: это по-прежнему ваше железо и по-прежнему без счётчика.
| Где выполняются ваши тесты | Какой режим и значение хоста |
|---|---|
| На вашей машине с Windows, которая управляет удалённым Grid | Local mode. Значение хоста остаётся 127.0.0.1, хотя браузер находится в другом месте |
| На агенте сборки в той же сети, что и решатель | Server mode. В значении хоста указан LAN-адрес машины с решателем |
| На облачном CI-раннере вне вашей сети | Server mode со статическим публичным IP, включённой проверкой ключа и правилом фаервола |
Обратить внимание стоит как раз на первую строку. Разработчик, который гоняет тесты локально против общего Grid, сохраняет адрес loopback, потому что решение капчи не покидает его стол.
Шаг 1: направьте драйвер на Grid
Selenium 4 принимает адрес Grid и объект options. Старый путь с суффиксом hub больше не нужен, достаточно базового URL.
# pip install selenium capskip
from selenium import webdriver
GRID = "http://grid.internal:4444"
options = webdriver.ChromeOptions()
options.add_argument("--no-sandbox")
# command_executor is the Grid, not the browser. Everything you
# call on this driver is a request over the wire to the node.
driver = webdriver.Remote(command_executor=GRID, options=options)В части капчи здесь ничего не меняется. Меняется то, что каждый вызов драйвера теперь идёт по сети туда и обратно, и это станет важным в разделе про таймауты ниже.
Шаг 2: решите капчу, затем вставьте токен
Решение капчи выполняется локальным вызовом функции в вашем тестовом процессе. Вставка токена выполняется скриптом на ноде. Держите эти две вещи мысленно раздельно, и код напишется сам.
# pip install capskip
import os
from capskip import CapSkip
# The host is 127.0.0.1 when the tests run on the solver machine,
# and the solver's address when they do not. It is never the node.
solver = CapSkip(
host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
port=8080,
)
result = solver.recaptcha(
sitekey="YOUR_SITEKEY",
url="https://example.com/page-with-recaptcha",
)
# This runs on the node. The token travels as a script argument.
driver.execute_script(
"document.getElementById('g-recaptcha-response')"
".value = arguments[0];",
result["code"],
)Передача токена аргументом, а не сборка строки скрипта вокруг него, на Grid важнее, чем локально, потому что скрипт сериализуется и уезжает на ноду. Именно на конкатенации строк ошибки с кавычками превращаются в незаметно пустое поле.
Любой вариант reCAPTCHA сводится к тому же методу с дополнительным ключевым аргументом: invisible со значением 1, enterprise со значением 1 или version со значением v3 и именем действия. У Turnstile и GeeTest свои методы такой же формы, а список параметров есть в документации CapSkip API.
Шаг 3: таймаут сессии, который убивает медленное решение
Вот сбой, специфичный для Grid, и он хорош. Нода убивает любую сессию, в которой не было активности в течение её таймаута сессии, а по умолчанию он равен 300 секундам. Пока SDK опрашивает результат, ваш тестовый процесс вообще не обращается к драйверу. Браузер стоит и ничего не делает, и с точки зрения ноды сессия выглядит брошенной.
Для чекбокса reCAPTCHA v2 это никогда не всплывает, потому что ответ обычно приходит заметно быстрее минуты. Всплывает это на страницах проверки Turnstile и на GeeTest, на загруженном решателе и на любом запуске, где вы упираетесь в собственный предел SDK. Этот предел равен 300 секундам, он задан в recaptchaTimeout, и это ровно то же значение, что по умолчанию у ноды. На значениях по умолчанию выиграть тут невозможно. Отсчёт простоя на ноде начинается раньше, чем SDK приступает к опросу, поэтому нода убирает сессию первой, и следующий вызов драйвера падает с недействительным id сессии, а вовсе не с чем-то про капчу.
Три исправления, в том порядке, в котором их стоит пробовать.
- Поднимите таймаут сессии на ноде через её опцию таймаута сессии, с запасом выше 300 секунд. Это честное исправление, и оно ничего не стоит.
- Занимайте сессию работой, пока идёт решение капчи. Синхронный клиент блокирует выполнение, поэтому придётся вынести решение в отдельный поток или взять асинхронный клиент, а затем время от времени делать дешёвый вызов драйвера, чтобы сбросить отсчёт простоя. Это работает, ценой большего числа движущихся частей.
- Опустите собственный предел SDK, чтобы он сдался первым, и дайте вашему тесту повторить попытку. TimeoutException из-за предела, который выбрали вы сами, читать в отчёте проще, чем мёртвую сессию.
Пока вы здесь, стоит знать ещё про два ограничения Grid. Максимальное число сессий на ноду по умолчанию равно числу процессоров на этой ноде, и именно оно ограничивает вашу реальную параллельность. А запрос новой сессии, который простоял в очереди дольше таймаута запроса сессии (по умолчанию он тоже равен 300 секундам), отклоняется ещё до того, как запустится браузер.
Полный рабочий пример
Полный тест, который открывает страницу, решает капчу, вставляет токен и отправляет форму. Отправка идёт сразу после вставки намеренно: токен reCAPTCHA живёт около двух минут, а Grid добавляет сетевые обращения между каждым шагом. Подробнее об этом в руководстве по истечению срока действия токена reCAPTCHA.
# pip install selenium capskip
import os
from selenium import webdriver
from selenium.webdriver.common.by import By
from capskip import CapSkip
from capskip.exceptions import NetworkException, TimeoutException
GRID = "http://grid.internal:4444"
PAGE = "https://example.com/page-with-recaptcha"
def test_login_through_recaptcha():
options = webdriver.ChromeOptions()
driver = webdriver.Remote(command_executor=GRID, options=options)
try:
driver.get(PAGE)
# Read the sitekey off the rendered page rather than
# hardcoding it. It is on the node, so this is a round trip.
sitekey = driver.find_element(
By.CSS_SELECTOR, ".g-recaptcha"
).get_attribute("data-sitekey")
solver = CapSkip(host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"))
result = solver.recaptcha(sitekey=sitekey, url=PAGE)
driver.execute_script(
"document.getElementById('g-recaptcha-response')"
".value = arguments[0];",
result["code"],
)
driver.find_element(By.CSS_SELECTOR, "form").submit()
except NetworkException:
raise AssertionError("CapSkip unreachable from the test runner")
except TimeoutException:
raise AssertionError("Solve did not finish before the ceiling")
finally:
driver.quit()На Grid всегда закрывайте драйвер в блоке finally. Утёкший локальный Chrome оставляет один лишний процесс на вашей собственной машине. Утёкшая сессия Grid держит один из слотов сессий этой ноды, пока её не уберёт таймаут, и на ноде с четырьмя процессорами это четверть вашей ёмкости, выключенная до этого момента.
Решение капчи в параллельных сессиях
Смысл Grid в том, чтобы гонять много браузеров одновременно, и каждой из этих сессий нужен свой токен. Решатель принимает одновременные запросы, поэтому рабочая схема состоит в том, чтобы держать решение асинхронным, а не выстраивать весь ваш набор тестов в очередь за одним блокирующим вызовом.
Python SDK поставляется для этого с настоящим асинхронным клиентом, и сказать об этом прямо стоит, потому что аналогичные классы в SDK для Node.js, PHP и .NET служат псевдонимами, а не отдельными реализациями. Приём с пакетной обработкой разобран в руководстве по параллельному решению капчи на Python, и он без изменений применим, когда браузеры оказываются удалёнными.
Запуск большего их числа ничего не добавляет к цене. Потолок есть у Grid: максимальное число сессий на ноду и таймаут очереди перед ним. Подбирайте параллельность тестов под Grid, а не под решатель.
Частые ошибки и что они означают
| Что вы видите | Причина | Исправить |
|---|---|---|
| NetworkException, отказ в соединении на порту 8080 | Решатель в Local mode, а тесты выполняются на другой машине | Переключитесь в Server mode и укажите в качестве хоста адрес решателя |
| Вы настроили ноду на доступ к решателю, и ничего не изменилось | Нода никогда не вызывает CapSkip. Это делает ваш тестовый процесс | Вместо этого укажите адрес решателя в значении хоста на тест-раннере |
| Недействительный id сессии сразу после долгого решения | Нода убрала простаивающую сессию, пока SDK вёл опрос | Поднимите таймаут сессии на ноде выше 300 секунд |
| Запрос новой сессии истекает по таймауту раньше, чем запустится браузер | Очередь заполнена, и запрос устарел | Добавьте ноды или поднимите таймаут запроса сессии |
| После выполнения скрипта поле ответа пустое | Токен вклеили в строку скрипта, и кавычки сломались | Передавайте его аргументом скрипта, как в примерах выше |
| Сайт отклоняет действительный токен | Он истёк между решением и отправкой | Отправляйте следующей же инструкцией, без ожиданий между ними |
| ERROR_GOOGLEKEY при отправке | Считанный со страницы sitekey был пустым или взят не из того элемента | Проверьте селектор и то, что виджет успел отрисоваться до чтения |
| Сессии накапливаются на одной ноде | Тест упал, не закрыв драйвер | Закрывайте драйвер в блоке finally, как в полном примере |
FAQ
Нужно ли что-то устанавливать на ноды Grid?
Нет. Ноды запускают браузеры и больше ничего. SDK является зависимостью вашего тестового проекта, решение капчи происходит в вашем тестовом процессе, а до ноды доезжает только токен, аргументом команды выполнения скрипта. Именно поэтому нода на Linux в Docker прекрасно работает с решателем, который запускается только на Windows: они друг с другом вообще не разговаривают.
Мои тесты выполняются в CI. Что нужно открывать наружу?
Порт решателя, для машины, которая выполняет тесты. Это Server mode со статическим публичным IP, если раннер облачный, плюс включённая проверка API-ключа и правило фаервола уже, чем весь интернет. Если ваши CI-раннеры свои и стоят в вашей сети, хватит LAN-адреса, и наружу ничего выходить не должно. В любом случае Grid это не затрагивает, потому что он не участвует в этом пути.
Почему моя сессия умерла на середине решения Turnstile?
Потому что нода не видела активности в течение своего таймаута сессии и прибралась. По умолчанию это 300 секунд, и собственный предел SDK для Turnstile тоже 300 секунд, а отсчёт простоя на ноде начинается раньше, поэтому на этих значениях по умолчанию нода всегда убирает сессию раньше, чем SDK сдастся. Поднимите таймаут сессии на ноде и подумайте о том, чтобы опустить предел SDK, чтобы сбой возвращался чистым исключением, а не мёртвой сессией.
Отличается ли это от решения капчи с локальным драйвером?
Только тем, где что работает. Вызов решения и вставка токена идентичны, в этом и суть. Отличия операционные: таймаут простаивающей сессии на ноде, сетевое обращение на каждый вызов драйвера и то, что адрес решателя определяется расположением вашего тест-раннера. Варианты с одним браузером разобраны в руководстве по SeleniumBase и руководстве по undetected-chromedriver.
Коротко
На Grid переезжает браузер, а ваш код остаётся на месте, поэтому адрес решателя определяется тем, где выполняются ваши тесты. Направьте RemoteWebDriver на порт 4444, вызывайте SDK из тестового процесса и передавайте токен на ноду аргументом скрипта, а не собирайте строку. Поднимите таймаут сессии на ноде выше 300 секунд, чтобы медленное решение не лишилось сессии, отправляйте форму сразу после вставки и всегда закрывайте драйвер в блоке finally.
- Фреймворк целиком описан на странице сервиса распознавания капчи для Selenium.
- Клиентская библиотека для Python описана на странице сервиса распознавания капч для Python.
- Сама капча описана на странице сервиса распознавания reCAPTCHA v2.
Стоит знать, прежде чем масштабировать набор тестов: CapSkip работает как распознавание капчи на железе, которое у вас уже есть, поэтому сто параллельных сессий стоят ровно столько же, сколько одна.
