Как решать капчу в Patchright, не ломая stealth

patchright captcha - How to Solve CAPTCHA in Patchright Without Breaking Stealth

Шаг с капчей в Patchright состоит из тех же трёх действий, что и везде: считать sitekey, отправить его решателю, вернуть токен в страницу. На третьем действии эта библиотека и удивляет. По умолчанию Patchright выполняет ваш JavaScript в изолированном контексте, а изолированный контекст делит со страницей её DOM, но не её глобальный объект JavaScript. Поэтому запись токена проходит, textarea действительно содержит значение, а собственный callback сайта так и не срабатывает, потому что его просто нет в той области, где выполняется ваш код. Лечится это одним дополнительным аргументом.

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

  • Python 3.10 или новее, либо Node.js, если вам ближе JavaScript-пакет. Patchright выпускается для обеих сред выполнения и в каждой из них служит прямой заменой Playwright.
  • Браузер Chromium, загруженный через собственный установщик Patchright. Firefox и WebKit не пропатчены и не поддерживаются.
  • URL страницы с защищённой формой. Sitekey считывается во время выполнения.
  • Запущенный CapSkip в режиме Local, когда скрипт и решатель работают на одной машине, или в режиме Server, когда это не так. Оба режима описаны в разделе Настройки подключения.
# pip install patchright
pip install -U capskip patchright

# Pulls the browser. Real Chrome is the recommended channel,
# and the maintainers say so explicitly.
patchright install chrome

Что именно меняет Patchright

Patchright представляет собой Playwright с убранными очевидными признаками автоматизации. Главный патч в том, что он никогда не вызывает Runtime.enable, самый громкий сигнал, который выдаёт обычная сессия Playwright, и достигается это за счёт запуска ваших скриптов в изолированных ExecutionContexts. Кроме того, он полностью отключает Console API, добавляет флаг, скрывающий navigator.webdriver, и убирает несколько значений Playwright по умолчанию, которые выдают автоматизированную сессию: сам флаг автоматизации, переопределение блокировщика всплывающих окон, блокировку обновления компонентов и переключатели, отключавшие приложения и расширения по умолчанию.

Два из них напрямую влияют на работу с капчей, и споткнуться о них легко.

Отключённая консоль означает, что ничего из того, что вы логируете изнутри страницы, до вас не дойдёт. Функциональность консоли в Patchright не работает вовсе, поэтому привычка воткнуть строку лога в вызов evaluate и прочитать её со стороны драйвера здесь бесполезна. Вместо этого возвращайте значение из вызова. Так всё равно правильнее, и другого варианта у вас нет.

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

Ещё одна возможность, о которой стоит знать: Patchright достаёт содержимое закрытых shadow root обычными локаторами и XPath. К виджетам, которые прячут свою разметку за закрытым root, можно обращаться без всякой особой обработки.

Шаг 1: запускайте так, как рекомендуют сопровождающие

Скрытность Patchright зависит от конфигурации запуска не меньше, чем от самих патчей. В документации описан постоянный контекст на настоящем канале Chrome, с видимым окном и без переопределения viewport, а также вообще без своего user agent и заголовков. Последние два пункта важны: заданный вручную user agent противоречит остальному отпечатку и сводит всю работу на нет.

# pip install patchright
from patchright.sync_api import sync_playwright

with sync_playwright() as p:
    context = p.chromium.launch_persistent_context(
        user_data_dir="C:\\profiles\\scraper",
        channel="chrome",
        headless=False,
        no_viewport=True,
        # Do not set user_agent or extra headers here.
    )
    page = context.new_page()
    page.goto("https://example.com/page-with-recaptcha")

Обратите внимание на видимое окно. Больше всего бюджета детекта уходит именно на headless, и рекомендуемая конфигурация его не использует. На Windows это значит, что учётной записи, из-под которой работает скрипт, нужна интерактивная сессия рабочего стола, и это стоит запланировать заранее, до того как вы поставите всё это на сервер.

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

Ключ sitekey лежит в документе-хосте, а не внутри iframe виджета, и его читает обычный локатор. Локаторы идут по протоколу браузера, а не через какой-либо контекст выполнения, поэтому изолированный мир на этот шаг никак не влияет.

# Locators auto-wait, 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

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

# 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]

Шаг 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 не отправить вместе с ним. Отправляйте его только в запросе с формой. Не передавайте его в запуск браузера, потому что заданный вручную user agent это ровно то, чего велит избегать конфигурация stealth. Полные списки параметров приведены в документации CapSkip API.

Шаг 4: положите токен туда, где страница сможет им воспользоваться

Вот та часть, которая специфична именно для этой библиотеки. Поле ответа textarea скрыто через display:none, поэтому напечатать в него ничего нельзя, и значение приходится присваивать через JavaScript. Это присваивание работает и из изолированного контекста, потому что DOM общий. Собирайте строку через json.dumps, а не через f-строку, поскольку строковый литерал JSON одновременно является корректным строковым литералом JavaScript, вместе с кавычками и экранированием.

# A DOM write is fine from the isolated context.
import json

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

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

Этого достаточно для сайтов, форма которых читает textarea при отправке. Многие сайты так не делают. Они регистрируют у виджета success callback и вообще не смотрят на textarea, поэтому токен нужно передать в функцию, объявленную страницей. У изолированного контекста Patchright свой собственный глобальный объект JavaScript, а значит, функций страницы и клиентской конфигурации reCAPTCHA в нём попросту нет. Вызов падает с reference error, и сколько ни повторяй, ничего не изменится.

Ответ Patchright на это: дополнительный аргумент. Все методы evaluate, evaluate_handle и evaluate_all принимают isolated_context, по умолчанию он равен True, а значение False выполняет скрипт в собственном main world страницы.

# The main world is where the page's own globals live.
page.evaluate(
    "token => window.onRecaptchaSuccess(token)",
    token,
    isolated_context=False,
)

Имя callback считайте из разметки страницы, а не угадывайте. Используйте main world только для вставки и ни для чего больше: работающий там код виден сайту, и именно поэтому изолированный контекст стоит по умолчанию. Оба способа отправки под капотом остаются обычной reCAPTCHA v2, и оба разобраны на странице сервиса распознавания reCAPTCHA v2.

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

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

# pip install capskip patchright
import json
from patchright.sync_api import sync_playwright
from capskip import CapSkip

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

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

with sync_playwright() as p:
    context = p.chromium.launch_persistent_context(
        user_data_dir="C:\\profiles\\scraper",
        channel="chrome",
        headless=False,
        no_viewport=True,
    )
    page = context.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.")

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

    length = page.evaluate(
        "document.getElementById('g-recaptcha-response').value.length"
    )
    print(length)   # 0 means the injection did not land

    page.click("button[type=submit]")
    page.wait_for_load_state("networkidle")
    context.close()

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

Patchright обычно оказывается на машине помощнее той, где вы писали скрипт, а рекомендуемая конфигурация с видимым окном довольно быстро подталкивает к выделенной виртуальной машине. Решателю переезжать вместе с ним не обязательно.

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

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

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

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

Что вы видитеПричинаИсправить
В textarea токен есть, но форма не проходитСайт использует callback и вообще не читает textareaВызовите callback с isolated_context=False
Reference error с именем функции страницыГлобальных объектов страницы в изолированном контексте нетТо же решение: выполните этот один вызов в main world
От console log на странице ничего не приходитPatchright полностью отключает Console APIВозвращайте значение из evaluate вместо логирования
Firefox или WebKit ведёт себя как обычный PlaywrightПропатчены только браузеры на ChromiumИспользуйте канал Chromium или Chrome
Блокировка несмотря на патчиСвой user agent или заголовок противоречит отпечаткуУберите их и используйте постоянный контекст на Chrome
ERROR_GOOGLEKEYДо решателя дошёл пустой sitekeyПроверяйте значение до вызова recaptcha
NetworkExceptionCapSkip не запущен или указан неверный хостЗапустите приложение или направьте host на адрес сервера
TimeoutExceptionРешение заняло больше, чем recaptchaTimeoutПоднимите его выше значения по умолчанию в 300 секунд

FAQ

Можно ли перенести мой скрипт на Playwright без изменений?

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

Не выдаст ли меня использование main world?

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

Не лучше ли кликнуть по виджету Turnstile вместо вставки токена?

Иногда. Флажок Turnstile в режиме managed может пройти сам, если браузер выглядит убедительно, а Patchright именно для этого и нужен, поэтому стоит сначала попробовать клик, а решение оставить запасным вариантом. Клик по флажку reCAPTCHA лишь открывает задание с картинками, так что выигрыша тут нет. Сторона виджета разобрана на страницу решателя Cloudflare Turnstile.

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

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

Коротко

Установите Patchright, запустите постоянный контекст на канале Chrome с видимым окном и без своего user agent, считайте sitekey обычным локатором и решите капчу через CapSkip на 127.0.0.1:8080. Токен пишите прямо в textarea из изолированного контекста по умолчанию, а к isolated_context=False обращайтесь только тогда, когда сайту нужен вызов callback. Об остальном ландшафте Python, включая Selenium и Playwright, смотрите странице сервиса распознавания капч для Python.

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