Как решать капчу в Camoufox с помощью main world eval

camoufox captcha - How to Solve CAPTCHA in Camoufox Using Main World Eval

Шаг с капчей в Camoufox выглядит так же, как и везде: считать sitekey, отправить его решателю, записать токен в страницу. Отличие Camoufox проявляется на третьем действии. JavaScript, который вы передаёте в evaluate, он выполняет в изолированной области, невидимой для страницы, а изолированная область не может менять DOM страницы. Ваша запись завершается без ошибки, textarea остаётся пустой, и форма не проходит валидацию. Лечится это одной опцией запуска и префиксом из двух символов.

Что понадобится

  • Python 3.10 или новее. Camoufox 0.5 сам фиксирует версию Playwright, поэтому дайте pip разрешить зависимость.
  • Браузер Camoufox, один раз загруженный командой fetch. Это сборка Firefox, а не Chromium.
  • URL страницы с защищённой формой. Sitekey считывается во время выполнения.
  • Запущенный CapSkip в режиме Local, когда скрипт и решатель работают на одной машине, или в режиме Server, когда это не так. Оба режима описаны в разделе Настройки подключения.
# The geoip extra is optional and worth having if you use proxies.
pip install -U capskip "camoufox[geoip]"

# Downloads the browser itself. Run once per machine.
camoufox fetch

Почему обычная вставка токена здесь ничего не даёт

Camoufox представляет собой пропатченный Firefox с тонкой Python-обёрткой вокруг Playwright. Класс обёртки служит контекстным менеджером Playwright, поэтому обратно вы получаете обычный объект браузера Playwright, и все привычные локаторы, клики и переходы работают без изменений. Это хорошая новость, и на неё приходится большая часть библиотеки.

Исключение составляет выполнение скриптов. Camoufox выполняет весь JavaScript в изолированной области, невидимой для страницы, и ради этого он и создан: сайт не видит, как автоматизация в него лезет. Чтения это не касается, потому что собственные методы локаторов Playwright идут по протоколу браузера, а не через эту область. А вот записи касается, и запомнить нужно вот что: изолированная область не может изменять DOM. Присваивание токена в ней тихо отбрасывается.

Ответ Camoufox на это: аварийный выход в main world. Передайте main_world_eval при запуске, а затем добавляйте префикс mw: к любому скрипту, которому нужно тронуть настоящую страницу, и он выполнится в её собственной области. Вместе с этим приходят две вещи. Код, работающий там, сайт может обнаружить, поэтому используйте этот приём только для вставки токена. И из main world нельзя вернуть ссылки на элементы, только значения, которые переживают сериализацию в JSON.

Шаг 1: запустите браузер с включённым main world

По умолчанию опция выключена, и задать её нужно именно при запуске. Включить её потом для одного отдельного вызова не получится.

# pip install camoufox[geoip]
from camoufox.sync_api import Camoufox

with Camoufox(main_world_eval=True, headless=True) as browser:
    page = browser.new_page()
    page.goto("https://example.com/page-with-recaptcha")

    # browser is a normal Playwright Browser from here on.
    print(page.title())

Прежде чем идти дальше, стоит узнать про две соседние опции. humanize ведёт курсор по траектории, похожей на человеческую, тратя на пересечение окна примерно до 1,5 секунды, и это важно, если вы кликаете по виджету сами, а не вставляете токен. А disable_coop снимает Cross-Origin-Opener-Policy, чтобы по элементам внутри кросс-доменных iframe, включая чекбокс Cloudflare Turnstile, вообще можно было кликнуть.

Шаг 2: считайте sitekey со страницы

Ключ sitekey лежит в документе-хосте, а не внутри iframe виджета. В разметке от Google он стоит на контейнере в виде атрибута data-sitekey, и его читает обычный локатор Playwright. Префикс main world здесь не нужен, потому что этот путь вообще не проходит через изолированную область.

# Locators wait by default, so this doubles as a wait
# condition for a widget that renders late.
holder = page.locator("div.g-recaptcha")
holder.wait_for(state="attached", timeout=15000)

sitekey = holder.get_attribute("data-sitekey")
print(sitekey)   # 6Lxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Некоторые сайты вообще не выставляют sitekey на странице-хосте и передают его только в URL iframe виджета. В таком случае считайте его из строки запроса.

# Fallback: the k= parameter on the anchor iframe.
from urllib.parse import urlparse, parse_qs

src = page.locator("iframe[src*='recaptcha/api2/anchor']").get_attribute("src")
sitekey = parse_qs(urlparse(src).query)["k"][0]

Проверьте значение, прежде чем тратить на него решение. Пустой sitekey доедет до решателя и вернётся как ERROR_GOOGLEKEY, а это далеко от того локатора, который на самом деле не сработал.

Шаг 3: решите её на своей машине

Python SDK обращается к CapSkip на порту 8080 и возвращает токен обычной строкой. Один метод покрывает reCAPTCHA v2, Invisible, Enterprise и v3, а варианты передаются опциями, а не отдельными вызовами.

# pip install capskip
from capskip import CapSkip

solver = CapSkip(host="127.0.0.1", port=8080)

# Invisible v2 takes invisible=1, Enterprise takes enterprise=1,
# and v3 takes version="v3" with an action.
result = solver.recaptcha(sitekey=sitekey, url=PAGE_URL)

token = result["code"]   # the g-recaptcha-response value

У Turnstile и GeeTest свои методы, устроенные точно так же. Turnstile дополнительно возвращает user agent, с которым было получено решение, и страница проверки отклонит токен, если вы не отправите вместе с ним этот же user agent. Полные списки параметров приведены в документации CapSkip API.

Camoufox поставляется и с асинхронным классом, а AsyncCapSkip из Python SDK представляет собой настоящую реализацию на asyncio, а не псевдоним синхронного класса. Используйте их в паре, когда ведёте сразу несколько контекстов, чтобы решение капчи на одной странице не тормозило остальные.

Шаг 4: вставьте токен в main world

Вот та часть, которая специфична именно для этого браузера. Textarea для ответа скрыта через display:none, поэтому напечатать в неё текст не сможет ни один инструмент автоматизации, и значение приходится присваивать через JavaScript. Добавьте скрипту префикс mw:, чтобы он выполнялся в собственной области страницы, и собирайте строку через json.dumps, а не через f-строку, потому что строковый литерал JSON является одновременно корректным строковым литералом JavaScript, вместе с кавычками и экранированием.

# The mw: prefix is what makes this write land.
import json

page.evaluate(
    "mw:document.getElementById('g-recaptcha-response').value = "
    + json.dumps(token)
)

page.click("button[type=submit]")

С проверкой того, что токен долетел, есть одна тонкость. Вернуть элемент наружу из main world нельзя, поэтому возвращайте значение вместо узла. Длина вполне подойдёт и скажет ровно то, что вам нужно знать.

# Return a number, never the element itself.
length = page.evaluate(
    "mw:document.getElementById('g-recaptcha-response').value.length"
)
print(length)   # 0 means the injection did not land

Если сайт при отправке формы не читает textarea, а вызывает success callback, вызовите этот callback после установки значения. Именно здесь main world не просто удобен, а обязателен, потому что функции, объявленной страницей, в изолированной области не существует вовсе. Имя функции считайте из собственной разметки страницы, а не угадывайте. Под обоими способами отправки лежит всё та же обычная reCAPTCHA v2, и оба они подробно расписаны на странице сервиса распознавания reCAPTCHA v2.

Полный рабочий пример

Всё вышеописанное в одном скрипте. Решатель создаётся один раз, браузер закрывается сам по завершении блока, а sitekey проверяется до того, как на него будет потрачено решение.

# pip install capskip camoufox[geoip]
import json
from camoufox.sync_api import Camoufox
from capskip import CapSkip

PAGE_URL = "https://example.com/page-with-recaptcha"

solver = CapSkip(host="127.0.0.1", port=8080)

with Camoufox(main_world_eval=True, headless=True) as browser:
    page = browser.new_page()
    page.goto(PAGE_URL)

    holder = page.locator("div.g-recaptcha")
    holder.wait_for(state="attached", timeout=15000)
    sitekey = holder.get_attribute("data-sitekey")
    if not sitekey:
        raise RuntimeError("Widget found but data-sitekey was empty.")

    result = solver.recaptcha(sitekey=sitekey, url=PAGE_URL)
    page.evaluate(
        "mw:document.getElementById('g-recaptcha-response').value = "
        + json.dumps(result["code"])
    )

    page.click("button[type=submit]")
    page.wait_for_load_state("networkidle")
    print(page.url)   # where you land after submitting

Запуск решателя на другой машине

Camoufox обычно оказывается на машине помощнее той, где вы писали скрипт, и в этот момент важно не ошибиться с платформой. Если передать headless строкой virtual, запустится дисплей Xvfb, а это возможность только для Linux: на Windows и macOS такое значение вместо запуска выдаёт ошибку о неподдерживаемом режиме. Обычный headless работает везде, поэтому используйте булево значение, если только вы намеренно не работаете на Linux.

Решателю не обязательно ездить вместе с браузером. У CapSkip два режима подключения. Local привязывается к 127.0.0.1 и отвечает только этому устройству, и это правильная настройка, пока вы пишете скрипт на той же машине, где работает приложение. Server привязывается к вашему сетевому или публичному IP, поэтому парсящая виртуальная машина, второй рабочий компьютер или Linux-машина с Camoufox обращаются по API к той же машине с Windows. Статический публичный IP держит этот адрес постоянным. В коде не меняется ничего, кроме хоста, и стоимость тоже не меняется, потому что это по-прежнему ваше железо.

# Same SDK, same call. Only the host moves.
solver = CapSkip(host="10.0.0.12", port=8080, apiKey="YOUR_API_KEY")

Включите проверку ключа, как только решатель начнёт слушать сетевой адрес, и выдайте каждой машине собственный ключ, чтобы один можно было отозвать, не трогая остальные. Оба режима подробно разобраны в руководстве по настройке CapSkip.

Частые ошибки и что они означают

Что вы видитеПричинаИсправить
Токен установлен, но форма всё равно не проходитЗапись выполнилась в изолированной области и была отброшенаЗапускайте с main_world_eval и добавляйте скрипту префикс mw:
Evaluate выдаёт ошибку о неподдерживаемом возвращаемом значенииИз main world вернули ссылку на узелВозвращайте длину или строку вместо элемента
Функция callback не определенаГлобальных объектов страницы в изолированной области нетВызывайте её из main world с тем же префиксом
По чекбоксу Turnstile не получается кликнутьОн находится в кросс-доменном iframeЗапускайте с disable_coop или вставляйте токен вместо клика
ERROR_GOOGLEKEYДо решателя дошёл пустой sitekeyПроверяйте значение до вызова recaptcha
NetworkExceptionCapSkip не запущен или указан неверный хостЗапустите приложение или направьте host на адрес сервера
TimeoutExceptionРешение заняло больше, чем recaptchaTimeoutПоднимите его выше значения по умолчанию в 300 секунд
Виртуальный дисплей не поддерживаетсяheadless выставлен в virtual не на LinuxИспользуйте вместо этого headless=True

FAQ

Повышает ли main world eval риск того, что меня обнаружат?

Всё, что выполняется в main world, странице видно, так что в принципе да. На практике всё раскрытие сводится к одному присваиванию, которое длится микросекунды и выглядит ровно так же, как действия собственного скрипта виджета, когда проверку проходит человек. Держите все остальные скрипты в изолированной области, делайте вставку одним вызовом, а не несколькими, и отдадите вы совсем немного.

Можно ли переиспользовать уже написанный код на Playwright?

Почти весь. Лаунчер отдаёт настоящий браузер Playwright, поэтому локаторы, контексты, маршруты и ожидания ведут себя как обычно. Пересмотреть нужно две вещи: любой вызов, который пишет в DOM через evaluate и которому нужен префикс, и всё, что рассчитывало на Chromium, ведь здесь Firefox. Более широкая картина по этому движку есть на странице сервиса распознавания капч для Playwright.

Стоит ли кликать по виджету вместо вставки токена?

Только для Turnstile и только иногда. Чекбокс Turnstile в managed-режиме может пройти сам, если браузер выглядит убедительно, ради чего Camoufox и нужен, а чтобы по нему кликнуть, сначала придётся снять Cross-Origin-Opener-Policy. Клик по чекбоксу reCAPTCHA просто открывает задание с картинками, так что выигрыша там нет. Подробности со стороны виджета смотрите, открыв страницу решателя Cloudflare Turnstile.

Мой краулер работает на Linux VPS. Куда ставить CapSkip?

На подконтрольную вам машину с Windows, с включённым режимом Server. Тогда VPS обращается к нему по API ровно так же, как к любому внутреннему сервису, поэтому Camoufox и решателю не нужны ни общая операционная система, ни даже общий сегмент сети. Направьте аргумент host на этот адрес, включите проверку ключа и выдайте VPS собственный ключ.

Коротко

Запустите Camoufox с включённым main_world_eval, считайте sitekey обычным локатором, решите капчу через CapSkip на 127.0.0.1:8080, затем вставьте токен вызовом evaluate с префиксом mw: и отправьте форму. Весь фокус именно в префиксе, потому что без него ваша запись попадает в область, которую страница никогда не видит. Про остальной ландшафт Python, включая Selenium и Playwright, смотрите на странице сервиса распознавания капч для Python.

Ещё одну вещь стоит знать, прежде чем масштабировать обход. CapSkip работает как локальный сервис распознавания капчи на оборудовании, которое у вас уже есть, поэтому прогон с тысячей повторных попыток стоит ровно столько же, сколько прогон с десятью.