Как решать капчу в Scrapling с помощью page_action

scrapling captcha - How to Solve CAPTCHAs in Scrapling With page_action

Задача с капчей в Scrapling чётко делится на две части, и понимание того, в какой половине вы находитесь, экономит полдня. У Cloudflare есть собственный переключатель: передайте solve_cloudflare в StealthyFetcher, и проверка Turnstile или промежуточная страница будут пройдены за вас. Всё остальное на вас. reCAPTCHA v2, reCAPTCHA v3, GeeTest и капчи с картинками решаются вне фетчера и вставляются в страницу через хук page_action. Здесь разбирается вторая половина, включая одну деталь, которая незаметно всё ломает.

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

  • Python 3.10 или новее. Scrapling этого требует.
  • Scrapling, установленный с дополнением fetchers, плюс разовый шаг загрузки браузера.
  • URL защищённой страницы. Sitekey заранее не нужен, потому что первый проход считывает его.
  • CapSkip, запущенный в режиме Local, если парсер и решатель стоят на одной машине, или в режиме Server, если на разных. Оба описаны в разделе Настройки подключения.
# pip install capskip
pip install capskip
pip install "scrapling[fetchers]"

# Scrapling needs one more step. The bare package is the parser
# engine only, and importing from scrapling.fetchers without this
# raises ModuleNotFoundError.
scrapling install

Что покрывает solve_cloudflare, а что нет

Здесь стоит быть точным, потому что название звучит шире, чем сама функция. Аргумент solve_cloudflare у StealthyFetcher проходит проверки Turnstile и промежуточные страницы Cloudflare, прежде чем вернуть вам ответ. Это действительно удобно, и этим стоит пользоваться. Но на этом список и заканчивается.

Проверка на страницеКто её обрабатывает
Cloudflare Turnstile и полностраничная промежуточная проверкаScrapling, через аргумент solve_cloudflare
reCAPTCHA v2, включая варианты invisible и enterpriseВы, через хук page_action
reCAPTCHA v3 с именем действияВы, через хук page_action
GeeTest v3, слайдер с тремя полями в ответеВы, через хук page_action
Капча с искажённым текстом на картинкеВы, через хук page_action

То есть схема работы всегда одна и та же. Достаньте параметры капчи со страницы, решите её в другом месте, затем верните ответ в DOM и позвольте собственной форме сайта его отправить. Scrapling даёт ровно одно место для этой средней части, и это page_action.

Шаг 1: считайте sitekey без запуска браузера

В Scrapling три фетчера, и они не взаимозаменяемы. Fetcher это обычный HTTP. DynamicFetcher управляет браузером Playwright. StealthyFetcher является усиленным вариантом, и начиная с версии 0.3.13 он работает на Patchright, а не на сборке Camoufox, которую использовал раньше; это стоит знать, если вы читаете старое руководство.

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

# pip install capskip
from scrapling.fetchers import Fetcher

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

# Plain HTTP. No browser starts, so this costs almost nothing
# and tells you which CAPTCHA you are actually facing.
page = Fetcher.get(PAGE_URL)

sitekey = page.css("[data-sitekey]::attr(data-sitekey)").get()
print(sitekey)

Если ответ пустой, значит виджет добавляется через JavaScript и его нет в отданном HTML. Загрузите ту же страницу один раз через DynamicFetcher и считайте значение из отрисованного DOM или откройте её в браузере и пропишите значение прямо в коде. Sitekey публичен и стабилен, поэтому такая жёсткая запись не тот приём, за который нужно извиняться.

Шаг 2: решите капчу на своём железе

Один вызов. Все варианты reCAPTCHA это один и тот же метод с разными именованными аргументами: invisible со значением 1, enterprise со значением 1 или version со значением v3 вместе с именем действия. У Turnstile и GeeTest есть свои методы такой же формы, а полный список параметров приведён в документации CapSkip API.

# CapSkip listens on your own machine, so this is a loopback call.
from capskip import CapSkip

solver = CapSkip(host="127.0.0.1", port=8080)
token = solver.recaptcha(sitekey=sitekey, url=PAGE_URL)["code"]

Этот вызов блокирует выполнение, пока идёт опрос, и опрос идёт не с постоянным интервалом: SDK начинает с 250 миллисекунд и постепенно увеличивает паузу до pollingInterval, поэтому обычно возвращает результат быстрее, чем самописный цикл к сырому эндпоинту. Верхний предел задаёт recaptchaTimeout, по умолчанию 300 секунд.

Шаг 3: вставьте токен в page_action

page_action принимает функцию, получает объект page из Playwright и выполняется после перехода на страницу и после ожидания network_idle, но до wait_selector. Именно этот порядок и полезен: к моменту запуска вашей функции виджет уже отрисован, а всё, чего вы ждёте дальше, видит результат ваших действий.

from playwright.sync_api import Page

def inject_token(page: Page):
    # The response textarea is hidden, so set the value directly
    # rather than trying to type into it.
    page.evaluate(
        "(t) => document.getElementById('g-recaptcha-response').value = t",
        token,
    )
    page.click("button[type=submit]")

Функция не обязана ничего возвращать. Старые версии Scrapling требовали вернуть объект page, текущая документация этого не требует, поэтому вариант без возврата безопасен в любом случае.

Изолированный контекст выполнения, та самая деталь, которая подводит

По умолчанию StealthyFetcher выполняет ваш JavaScript в изолированном контексте выполнения Patchright. DOM общий, поэтому чтение и запись элементов работают ровно так, как вы ожидаете. Собственные глобальные объекты JavaScript страницы не общие, поэтому всё, что сайт положил в window, там просто отсутствует.

От этого различия зависит, сработает ли ваша вставка. Установка значения textarea с ответом затрагивает только DOM и в изолированном мире работает нормально. Вызов callback reCAPTCHA у сайта не работает, потому что этот callback живёт в глобальной переменной, созданной собственным скриптом страницы. Сайты, которые используют callback вместо обычной отправки формы, будут выглядеть так, будто проигнорировали совершенно нормальный токен.

def inject_and_fire_callback(page: Page):
    # isolated_context=False drops into the page's own world,
    # where the site's grecaptcha object actually exists.
    page.evaluate(
        """(t) => {
            document.getElementById('g-recaptcha-response').value = t;
            window.onCaptchaSuccess(t);
        }""",
        token,
        isolated_context=False,
    )

Перед тем как это писать, считайте имя callback с виджета. Оно указано в атрибуте data-callback на элементе reCAPTCHA и отличается от сайта к сайту.

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

Дешёвый HTTP-проход, чтобы считать sitekey, одно решение, затем один stealth-запрос, который вставляет токен и отправляет форму. Обратите внимание: решение происходит до запроса, поэтому к моменту работы page_action токен уже на руках.

# pip install capskip
from capskip import CapSkip
from playwright.sync_api import Page
from scrapling.fetchers import Fetcher, StealthyFetcher

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

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

sitekey = Fetcher.get(PAGE_URL).css("[data-sitekey]::attr(data-sitekey)").get()
token = solver.recaptcha(sitekey=sitekey, url=PAGE_URL)["code"]

def inject_token(page: Page):
    page.evaluate(
        "(t) => document.getElementById('g-recaptcha-response').value = t",
        token,
    )
    page.click("button[type=submit]")

page = StealthyFetcher.fetch(
    PAGE_URL,
    headless=True,
    network_idle=True,
    page_action=inject_token,
    wait_selector=".dashboard",
)

print(page.css(".dashboard h1::text").get())

Два аргумента здесь несущие. network_idle заставляет фетчер ждать, пока в течение 500 миллисекунд не будет сетевых соединений, чего обычно хватает для отрисовки виджета. wait_selector доказывает, что отправка сработала: направьте его на то, что существует только по ту сторону формы, и неудачная отправка станет таймаутом вместо тихого успеха.

Несколько страниц через сессию

Каждый вызов fetch на уровне класса запускает браузер и тут же его выбрасывает. Если страниц больше пары, откройте вместо этого сессию и сохраняйте браузер между запросами. Большинство аргументов fetch можно задать один раз на сессии и переопределять для каждого запроса, включая page_action, wait_selector и solve_cloudflare.

from scrapling.fetchers import StealthySession

# solve_cloudflare here handles the Cloudflare layer for every
# page in the session. Anything else still goes through
# page_action, per request.
with StealthySession(headless=True, solve_cloudflare=True) as session:
    for url in urls:
        page = session.fetch(url, page_action=inject_token)
        print(page.css("title::text").get())

Решайте капчу как можно позже. Токен reCAPTCHA живёт около двух минут, поэтому если решить двадцать штук заранее, а потом идти по очереди, большинство из них протухнет. Решайте внутри цикла, непосредственно перед запросом, который использует токен. Что этот срок жизни означает на практике, разобрано в руководстве по сроку действия токена reCAPTCHA.

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

Парсеры переезжают. VPS, контейнер или воркер по расписанию это не та машина, на которой стоит решатель, и loopback там указывает на сам воркер, где никто не слушает.

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

# The SDK reads these three itself, so the same script runs
# whether the solver is on this box or on another one:
#   CAPSKIP_HOST=192.0.2.10
#   CAPSKIP_PORT=8080
#   CAPSKIP_API_KEY=your-key

solver = CapSkip()

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

Прокси с обеих сторон

Если сайт проверяет, что токен пришёл с того же адреса, с которого он отправляется, решение и запрос должны выходить через один и тот же выход. Scrapling принимает аргумент proxy у фетчеров, а CapSkip принимает прокси для каждой задачи reCAPTCHA, Turnstile и GeeTest. Капчи с картинками прокси не принимают и не нуждаются в нём.

PROXY = "http://user:[email protected]:3128"

token = solver.recaptcha(
    sitekey=sitekey,
    url=PAGE_URL,
    proxy={"type": "HTTP", "uri": "user:[email protected]:3128"},
)["code"]

page = StealthyFetcher.fetch(PAGE_URL, proxy=PROXY, page_action=inject_token)

Записываются они по-разному намеренно: решатель принимает тип и URI отдельными полями, а фетчер принимает одну строку URL. Ошибка в одном из них означает, что решение уходит с вашего настоящего адреса, а запрос через прокси, и выглядит это ровно как плохой токен, хотя токен нормальный. Выбор типа прокси разобран в руководстве по ротации прокси при решении капчи.

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

Что вы видитеПричинаИсправить
ModuleNotFoundError при импорте fetchersГолый пакет содержит только движок парсераУстановите с дополнением fetchers, затем выполните команду scrapling install
Селектор sitekey ничего не возвращаетВиджет добавляется через JavaScript, поэтому в отданном HTML его нетСчитайте его один раз через DynamicFetcher или пропишите в коде
Токен попадает в поле, но форма так и не отправляетсяСайт использует callback, а этой глобальной переменной нет в изолированном миреПередайте isolated_context со значением False в вызов evaluate
Таймаут на wait_selector после успешного решенияОтправка не прошла, поэтому элемент по ту сторону так и не появилсяСделайте скриншот в page_action и посмотрите, что на самом деле пишет страница
Действительный токен отклоняетсяРешение и запрос ушли с разных адресовПропишите один и тот же прокси в обоих местах, каждый в своём формате
NetworkException от решателяCapSkip не запущен либо неверны хост и портЗапустите приложение или направьте переменную окружения с хостом на адрес сервера
ValidationException от решателяАргумент, который этот тип капчи не принимаетПроверьте тип. Action на v2 или invisible на v3 вызывают эту ошибку

FAQ

Обрабатывает ли solve_cloudflare ещё и reCAPTCHA?

Нет. Он покрывает виджет Turnstile и промежуточную страницу проверки, то есть продукты самого Cloudflare. reCAPTCHA принадлежит Google, GeeTest сам по себе, а капча с картинкой это то, что нарисовал сайт. Все три проходят через page_action с токеном, который вы решили сами.

StealthyFetcher всё ещё построен на Camoufox?

Начиная с версии 0.3.13 уже нет. Теперь он работает на Patchright, а это стек Chromium, а не Firefox. В документации до сих пор описан необязательный способ вернуться к Camoufox через наследование от сессии, но значение по умолчанию изменилось, поэтому руководство, где сказано, что StealthyFetcher является обёрткой над Camoufox, описывает более старый релиз.

Можно ли решать капчу внутри page_action, а не до запроса?

Можно, а для reCAPTCHA v3 или GeeTest часто и приходится, потому что параметры появляются только после того, как страница отработала. Просто помните, что браузер всё время решения простаивает. Если sitekey есть в отданном HTML, то решить сначала и вставить потом позволяет держать браузер открытым в разы меньше.

Меняет ли что-нибудь асинхронный фетчер?

Только запись. Используйте асинхронный метод fetch и сделайте page_action асинхронной функцией, которая принимает асинхронный объект page из Playwright. На стороне решателя используйте AsyncCapSkip: в Python это настоящая асинхронная реализация, а не псевдоним, поэтому она работает в вашем цикле событий, а не занимает отдельный поток.

Коротко

Пусть Cloudflare берёт на себя Scrapling через solve_cloudflare, а всё остальное делайте сами в page_action. Считайте sitekey обычным HTTP-фетчером, решите капчу, затем задайте поле ответа изнутри хука. Если сайт вызывает callback вместо отправки формы, выполняйте код с выключенным изолированным контекстом, иначе токен окажется в мире, которого страница не видит. Направьте wait_selector на то, что существует только после успешной отправки, чтобы сбой был заметен.

Более широкая картина по Python есть на странице сервиса распознавания капч для Python, особенности капчи с флажком на странице сервиса распознавания reCAPTCHA v2, а сторона Cloudflare на страницу решателя Cloudflare Turnstile. Те же три вызова есть в Node.js, PHP и C#, они перечислены на на странице SDK для распознавания капчи.

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