Как решить reCAPTCHA в MechanicalSoup и отправить форму

MechanicalSoup никогда не запускает виджет капчи, поэтому поля, которое заполнил бы виджет, в форме обычно нет, и попытка задать его вызывает LinkNotFoundError. Решение капчи в MechanicalSoup укладывается в три шага. Прочитайте sitekey со страницы, которую MechanicalSoup уже разобрал, и решите капчу с помощью CapSkip, работающего на вашей собственной машине с Windows. Затем сами добавьте поле g-recaptcha-response через new_control, прежде чем вызывать submit_selected. Остальное MechanicalSoup берёт на себя: сессионный cookie, скрытое поле CSRF и заголовок Referer, который проверяет строгий сервер. В этом руководстве разобраны чекбокс reCAPTCHA, невидимая кнопка, v3, Enterprise, другие виджеты, которые решает CapSkip, множество входов одновременно и запуск решателя на другой машине.
Что понадобится
- CapSkip, запущенный на машине с Windows, с включённым решателем reCAPTCHA.
- Python 3.10 или новее, пакет capskip и MechanicalSoup. Примеры запускались с capskip 1.3.0 и MechanicalSoup 1.4.0, которые устанавливают вместе с собой requests, Beautiful Soup и lxml.
- URL страницы с формой. Sitekey, поля формы и тип виджета берутся из HTML именно этой страницы.
- Адрес решателя. В режиме Local CapSkip отвечает на 127.0.0.1 и только для этого устройства. В режиме Server он слушает ваш сетевой адрес или публичный IP, чтобы скрипт на VPS или на другом ПК мог обращаться к нему через API. Оба режима задаются в разделе Настройки подключения.
# Python 3.10 or later; MechanicalSoup brings requests, bs4 and lxml pip install capskip mechanicalsoup
Шаг 1: открываем страницу и находим sitekey
StatefulBrowser представляет собой сессию requests с прикреплённой к ней разобранной страницей. Он хранит все cookies, которые ставит сайт, следует редиректам и запоминает адрес, на котором оказался в итоге. Откройте страницу входа и найдите элемент reCAPTCHA:
# pip install capskip mechanicalsoup
import mechanicalsoup
PAGE_URL = "https://example.com/login"
# One StatefulBrowser per session: it keeps cookies, follows redirects
# and sends the current page as Referer when it submits a form.
browser = mechanicalsoup.StatefulBrowser(raise_on_404=True)
browser.open(PAGE_URL)
# The checkbox div, or the button on invisible reCAPTCHA, holds the key.
widget = browser.page.select_one(".g-recaptcha[data-sitekey]")
if widget is None:
raise RuntimeError("No reCAPTCHA widget in the HTML; see the v3 section.")
sitekey = widget["data-sitekey"]
# The form around the widget; a page often has a search form first.
browser.select_form(widget.find_parent("form") or 'form[method="post" i]')
print(sitekey, browser.url)Виджет с чекбоксом представляет собой div с классом g-recaptcha и атрибутом data-sitekey. Невидимая reCAPTCHA обычно ставит тот же класс и атрибут на кнопку отправки, поэтому один селектор находит оба варианта. Страница, которая загружает api.js с render=explicit, строит виджет собственным скриптом и передаёт ключ в grecaptcha.render, поэтому читайте его из этого вызова в DevTools. Выбирайте форму вокруг виджета, а не первую форму на странице. Многие сайты ставят форму поиска в шапку, и select_form по умолчанию выбирает именно её. Если виджет находится вне своей формы, запасной вариант берёт первую форму с методом POST, поэтому если на странице их две, передайте вместо этого id формы входа.
browser.url содержит адрес, с которого пришла страница после всех редиректов. Именно этот URL нужно передавать решателю, потому что sitekey зарегистрирован на домены своего сайта, и решение для не той страницы пропадёт впустую. raise_on_404 заставляет отсутствующую страницу громко падать ещё на open, а не где-то дальше.
Шаг 2: решаем reCAPTCHA с помощью CapSkip
Само решение выполняет один вызов пакета capskip. CapSkip решает капчу на вашей машине с Windows и возвращает токен, который виджет положил бы в форму:
from capskip import CapSkip
solver = CapSkip(host="127.0.0.1", port=8080)
# enterprise.js in place of api.js means reCAPTCHA Enterprise.
enterprise = int("recaptcha/enterprise.js" in str(browser.page))
if widget.name == "button" and widget.get("data-action"):
# Google's v3 button binding: a g-recaptcha button with an action is v3.
result = solver.recaptcha(sitekey=sitekey, url=browser.url, version="v3",
action=widget["data-action"], enterprise=enterprise)
else:
# A g-recaptcha button, or data-size="invisible", is the invisible widget.
invisible = widget.name == "button" or widget.get("data-size") == "invisible"
result = solver.recaptcha(sitekey=sitekey, url=browser.url,
invisible=int(invisible), enterprise=enterprise)
token = result["code"]
print(token[:40]) # what the widget would have written into the formЧекбокс решается простым вызовом, а невидимая reCAPTCHA решается тем же вызовом с invisible, равным 1. Кнопка g-recaptcha, у которой есть ещё и data-action, представляет собой предусмотренную Google привязку v3 к кнопке, поэтому скрипт решает её как v3 с этим action. Страницы, которые загружают enterprise.js вместо api.js, работают на reCAPTCHA Enterprise, и enterprise=1 сообщает об этом CapSkip. Токен лежит в result["code"]. Он действителен около двух минут и проходит проверку только один раз, поэтому решайте непосредственно перед отправкой и заново для каждого входа.
recaptcha опрашивает результат не дольше recaptchaTimeout, по умолчанию 300 секунд. Другое значение задавайте в конструкторе, потому что timeout, переданный в отдельный вызов, вызывает TypeError: метод уже передаёт собственный. У CapSkip есть и свой таймер. Задание ждёт свободного потока reCAPTCHA до 250 секунд (Wait Timeout), а затем у самого решения есть собственный лимит: Row Timeout, по умолчанию 250 секунд. Когда истекает один из этих лимитов, CapSkip завершает задание с ошибкой, и SDK вызывает ApiException с ERROR_CAPTCHA_UNSOLVABLE. Используемый здесь клиент CapSkip не задаёт таймаут для собственных HTTP-вызовов, поэтому если CapSkip принял соединение и так и не ответил, вызов остаётся открытым.
Шаг 3: добавляем поле, которое записал бы виджет, и отправляем форму
Именно на этом шаге спотыкается каждый скрипт с капчей на MechanicalSoup. В настоящем браузере скрипт Google создаёт скрытую textarea с именем g-recaptcha-response и заполняет её, когда капча решена. MechanicalSoup разбирает HTML, который прислал сервер, и не выполняет JavaScript, поэтому на большинстве страниц этого поля нет. Тогда обычное присваивание ему значения вызывает LinkNotFoundError с сообщением No valid element named g-recaptcha-response. new_control добавляет поле в выбранную форму:
# This line raises LinkNotFoundError on most pages: the widget script
# creates the field in a real browser, so the parsed HTML lacks it.
# browser["g-recaptcha-response"] = token
# Add the field the widget would have written, replacing any copy.
browser.new_control("hidden", "g-recaptcha-response", token)
browser["email"] = "YOUR_EMAIL"
browser["password"] = "YOUR_PASSWORD"
response = browser.submit_selected()
print(response.status_code, browser.url)new_control удаляет любое поле с тем же именем, прежде чем добавить своё, и это важно на страницах, где сохранена резервная разметка Google для noscript. В ней есть textarea с тем же именем, и парсер MechanicalSoup считает её частью формы. На таких страницах обычное присваивание тоже работает, но new_control одной строкой покрывает оба типа страниц.
submit_selected отправляет все поля формы, включая скрытый CSRF-токен, в той же сессии, которая загрузила страницу. Кроме того, он отправляет адрес страницы в заголовке Referer. Обычный requests его не отправляет, как и низкоуровневый метод Browser.submit самого MechanicalSoup. Некоторые фреймворки, в том числе Django, отклоняют отправку формы по HTTPS без Referer или Origin, какими бы правильными ни были cookie и токен. Поэтому используйте submit_selected в том браузере, который открыл страницу. Если в форме несколько кнопок отправки, передайте имя нужной в btnName. И ответ 200 не доказывает, что вход удался. Многие сайты отвечают на отклонённую капчу той же формой и кодом 200, поэтому ищите в browser.page то, что показывает только страница после входа.
У reCAPTCHA v3 нет виджета, который нужно искать
v3 ничего не спрашивает у пользователя. Сайт загружает скрипт Google с sitekey в адресе, после render=, а собственный скрипт сайта запрашивает токен с именем action, например login. Затем большинство страниц с v3 сами записывают токен в скрытый input. Считайте оба значения из HTML, отметьте, подключён ли скрипт api.js или enterprise.js, и передайте оба значения дальше с version, равным v3. Если же страница привязывает v3 к кнопке, это уже обрабатывает шаг 2.
import re
# A v3 page has no widget element; the key sits in the script tags.
html = str(browser.page)
key = re.search(r"recaptcha/(api|enterprise)\.js\?[^\"'<>\s]*?\brender=(?!explicit\b)([\w-]+)", html)
action = re.search(r"execute\(\s*[^,()]+,\s*\{\s*[\"']?action[\"']?\s*:\s*[\"']([\w/]+)", html)
if key is None:
raise RuntimeError("No v3 render key in the HTML; check DevTools.")
v3 = solver.recaptcha(sitekey=key.group(2), url=browser.url, version="v3",
enterprise=int(key.group(1) == "enterprise"),
**({"action": action.group(1)} if action else {}))
browser.select_form('form[method="post" i]')
# The field the page's own script fills in; read its name in DevTools.
browser.new_control("hidden", "g-recaptcha-response", v3["code"])На странице с v3 выполните шаг 1 только до browser.open, затем строки шага 2, которые создают решатель, затем этот блок, а потом учётные данные и submit_selected из шага 3. Значение action важно, потому что бэкенд сайта обычно проверяет, что токен выпущен для того action, которого он ждал. Если шаблон не находит action, вызов уходит без него, и CapSkip использует verify, а сайт, который ждёт login, такой токен отклоняет. Если вызов execute находится во внешнем скрипте, прочитайте action в DevTools и передайте его сами. Имя поля определяется тем, что заполняет скрипт страницы: часто это g-recaptcha-response, но не всегда, поэтому проверьте его в DevTools, один раз отправив форму вручную. Затем заполните учётные данные и вызовите submit_selected, как показано выше. Официальная документация Google по reCAPTCHA v3 показывает вызовы render и execute, на которые рассчитаны эти шаблоны. В руководстве по решению reCAPTCHA v3 на Python сам вызов разобран подробнее.
Другие виджеты работают так же
Каждый виджет, который решает CapSkip, в MechanicalSoup следует одной схеме: прочитать ключ, решить, добавить поле, которое создал бы виджет, отправить. Меняются только селектор, имя поля и метод:
| Виджет | Где находится ключ | Какое поле добавить | Метод capskip |
|---|---|---|---|
| reCAPTCHA v2 checkbox | div.g-recaptcha, data-sitekey | g-recaptcha-response | solver.recaptcha() |
| Невидимая reCAPTCHA v2 | кнопка с классом g-recaptcha или div с data-size=invisible | g-recaptcha-response | solver.recaptcha() с invisible=1 |
| reCAPTCHA v3 | api.js?render= или enterprise.js?render= в адресе скрипта либо кнопка g-recaptcha с data-action | input, который заполняет скрипт страницы | solver.recaptcha() с version="v3" и action |
| reCAPTCHA Enterprise | enterprise.js вместо api.js | как для v2 или v3 | solver.recaptcha() с enterprise=1 |
| Виджет Cloudflare Turnstile | div.cf-turnstile, data-sitekey | cf-turnstile-response | solver.turnstile() |
| Friendly Captcha | div.frc-captcha, data-sitekey | frc-captcha-response в v2, frc-captcha-solution в v1 | solver.friendly_captcha() с version="v1" или "v2" |
| CaptchaFox | div.captchafox, data-sitekey | cf-captcha-response | solver.captchafox() |
Дополнительное правило есть только у CaptchaFox. Его токен принадлежит браузеру, который этот токен выпустил. Поэтому если в результате есть userAgent, задайте его браузеру через set_user_agent перед отправкой, потому что иначе MechanicalSoup отправляет python-requests и собственное имя. В руководстве по решению CaptchaFox на Python разобраны сборка виджета и повторы.
Полный рабочий пример
Вот полный вход с капчей на MechanicalSoup в одном файле. login открывает страницу в новом браузере, находит reCAPTCHA с чекбоксом, невидимую или v3, решает её, добавляет поле и отправляет форму:
# pip install capskip mechanicalsoup
# login.py: run with python login.py
import os
import re
import mechanicalsoup
from capskip import CapSkip, CapSkipError, TimeoutException
PAGE_URL = "https://example.com/login"
# render= carries a v3 key on api.js or enterprise.js; render=explicit is a v2 page.
V3_KEY = re.compile(r"recaptcha/(api|enterprise)\.js\?[^\"'<>\s]*?\brender=(?!explicit\b)([\w-]+)")
# The action passed to execute, whether the key is a string or a variable.
V3_ACTION = re.compile(r"execute\(\s*[^,()]+,\s*\{\s*[\"']?action[\"']?\s*:\s*[\"']([\w/]+)")
solver = CapSkip(
apiKey=os.getenv("CAPSKIP_API_KEY", "capskip"),
host=os.getenv("CAPSKIP_HOST", "127.0.0.1"),
port=int(os.getenv("CAPSKIP_PORT", "8080")),
)
def solve_page(browser):
"""Solve the page's reCAPTCHA and select its form. Returns the token."""
html = str(browser.page)
enterprise = int("recaptcha/enterprise.js" in html)
widget = browser.page.select_one(".g-recaptcha[data-sitekey]")
if widget is not None:
browser.select_form(widget.find_parent("form") or 'form[method="post" i]')
if widget.name == "button" and widget.get("data-action"):
# Google's v3 button binding: plain api.js, the action on the button.
return solver.recaptcha(sitekey=widget["data-sitekey"], url=browser.url, version="v3",
action=widget["data-action"], enterprise=enterprise)["code"]
invisible = widget.name == "button" or widget.get("data-size") == "invisible"
return solver.recaptcha(sitekey=widget["data-sitekey"], url=browser.url,
invisible=int(invisible), enterprise=enterprise)["code"]
key = V3_KEY.search(html)
if key is None:
raise RuntimeError(f"No reCAPTCHA found on {browser.url}")
action = V3_ACTION.search(html)
browser.select_form('form[method="post" i]')
return solver.recaptcha(sitekey=key.group(2), url=browser.url, version="v3",
enterprise=int(key.group(1) == "enterprise"),
**({"action": action.group(1)} if action else {}))["code"]
def login(email, password, page_url=PAGE_URL):
# A new browser per login, so sessions never share cookies.
with mechanicalsoup.StatefulBrowser(raise_on_404=True) as browser:
browser.open(page_url)
token = solve_page(browser)
browser.new_control("hidden", "g-recaptcha-response", token)
browser["email"] = email
browser["password"] = password
response = browser.submit_selected()
return response.status_code, browser.url
if __name__ == "__main__":
try:
print(login("YOUR_EMAIL", "YOUR_PASSWORD"))
except TimeoutException:
raise SystemExit("gave up after recaptchaTimeout")
except CapSkipError as exc:
raise SystemExit(f"solve failed: {type(exc).__name__}: {exc}")
except mechanicalsoup.LinkNotFoundError as exc:
# A 404, or a form or field the page does not have.
raise SystemExit(f"not found: {str(exc) or PAGE_URL}")
except RuntimeError as exc:
raise SystemExit(str(exc))Каждый вызов login создаёт собственный StatefulBrowser и закрывает его по завершении, поэтому два входа никогда не делят cookies. Блок with закрывает сессию, даже если какой-то шаг падает. Большинство сбоев подсказывают, где искать. LinkNotFoundError означает 404, отсутствующую форму или имя поля, которое не совпадает со страницей. RuntimeError означает, что на странице нет reCAPTCHA, которую распознаёт скрипт, поэтому откройте её в настоящем браузере и посмотрите, что загружается. Если там она загружается нормально, выведите browser.open(page_url).status_code: код 403 означает, что сайт отклонил user agent по умолчанию. CapSkipError приходит от самого решения, поэтому смотрите таблицу ошибок ниже.
Множество входов одновременно и решатель на другой машине
MechanicalSoup синхронный, как и используемый здесь клиент CapSkip. Поэтому когда задаче с капчей на MechanicalSoup нужно пройти много входов, потоки оказываются самым простым способом запустить их параллельно. Держите по одному StatefulBrowser на поток (login уже так делает) и ограничьте пул десятью: это значение Max. Threads для reCAPTCHA в CapSkip по умолчанию. Задание сверх этого числа ждёт свободного потока, а длинная очередь съедает часть тех 300 секунд, в течение которых SDK опрашивает результат. Поднимите Max. Threads в настройках reCAPTCHA в CapSkip, прежде чем поднимать max_workers. Сохраните полный пример как login.py и запустите этот код из второго файла рядом с ним:
# pool.py: save next to login.py, the full example, and run this file
from concurrent.futures import ThreadPoolExecutor
from login import login
accounts = [("YOUR_EMAIL_1", "YOUR_PASSWORD_1"), ("YOUR_EMAIL_2", "YOUR_PASSWORD_2")]
def attempt(account):
# One failed login must not stop the others.
try:
return login(*account)
except Exception as exc:
return f"{type(exc).__name__}: {exc}"
# Ten at a time, to match CapSkip's default Max. Threads for reCAPTCHA.
with ThreadPoolExecutor(max_workers=10) as pool:
for account, outcome in zip(accounts, pool.map(attempt, accounts)):
print(account[0], outcome)pool.map возвращает результаты в том порядке, в котором были переданы аккаунты, а attempt превращает сбой в значение, поэтому один неудачный вход не останавливает остальные. На мок-решателе двадцать пять входов через этот пул ни разу не держали в работе больше десяти решений одновременно.
Адрес 127.0.0.1 подходит, пока скрипт и CapSkip работают на одном ПК с Windows. Перенесите скрипт на VPS, в задание по расписанию на другой машине или в контейнер, и локальная петля будет указывать не на ту машину, поэтому первое же решение вызовет NetworkException. Переключите CapSkip в режим Server, и он начнёт слушать ваш сетевой адрес или публичный IP, так что любой из этих вариантов сможет обращаться к нему через тот же API. Если маршрут идёт через интернет, используйте статический публичный IP. При запуске режима Server CapSkip открывает свой порт для всех адресов, поэтому, чтобы к нему подключались только ожидаемые машины, добавьте блокирующее правило Windows Firewall для остальных. Это по-прежнему ваша собственная машина, и решения по-прежнему не тарифицируются. Клиент сам не читает переменные окружения, поэтому полный пример читает CAPSKIP_HOST и передаёт его в клиент.
Частые ошибки и что они означают
| Что вы видите | Причина | Исправить |
|---|---|---|
| LinkNotFoundError: No valid element named g-recaptcha-response | Скрипт виджета так и не выполнился, поэтому поля, которое он создаёт, нет в разобранной форме | Добавьте его через new_control перед отправкой |
| Токен уходит вместе с формой поиска, или select_form вызывает LinkNotFoundError | На странице первой идёт другая форма, или виджет находится вне своей формы | Выбирайте форму вокруг виджета или форму входа по её id |
| 403 или ошибка CSRF со свежим токеном | Отправка ушла в новой сессии, через requests.post или через Browser.submit, поэтому не хватило cookie или Referer | Заполняйте и отправляйте форму через submit_selected в том браузере, который открыл страницу |
| Сайт сообщает, что капча не пройдена, хотя CapSkip вернул токен | Невидимый виджет решён как чекбокс, action для v3 не совпадает с action страницы, страница Enterprise решена без enterprise=1, или токен был старше двух минут | Передайте invisible=1, enterprise=1 или action страницы и решайте непосредственно перед отправкой |
| TypeError: capskip.solver.CapSkip.solve() got multiple values for keyword argument timeout | В отдельный вызов recaptcha был передан timeout | Вместо этого задайте recaptchaTimeout в конструкторе CapSkip |
| ApiException с ERROR_CAPTCHA_UNSOLVABLE | CapSkip завершил задание с ошибкой по собственному таймеру, или sitekey и URL страницы не соответствуют друг другу | Передавайте browser.url и собственный sitekey страницы и держите в работе не больше десяти решений |
| TimeoutException через 300 секунд | Ожидание свободного потока и само решение вместе превысили лимит SDK | Ограничьте пул значением Max. Threads или поднимите recaptchaTimeout в конструкторе |
| reCAPTCHA не найдена на странице, которая нормально загружается в настоящем браузере | Сайт ответил кодом 403, потому что отклоняет user agent по умолчанию, в котором указаны python-requests и MechanicalSoup | Передайте user agent браузера в параметре user_agent при создании StatefulBrowser |
| NetworkException при первом решении | CapSkip не запущен, неверны хост или порт, либо скрипт работает на другой машине | Запустите CapSkip и проверьте, в каком режиме он должен работать: Local или Server |
FAQ
Зачем использовать MechanicalSoup вместо обычного requests?
Он избавляет вас от сборки формы заново. С requests вы сами собираете скрытые поля, поддерживаете сессию, разрешаете action формы и задаёте Referer, и если упустить хоть что-то одно, хороший токен будет отклонён. MechanicalSoup делает все четыре вещи по разобранной странице, так что ради капчи вы добавляете только одно поле. Для страниц, которые строят свои формы на JavaScript, не хватит ни того, ни другого, и там лучше подходит инструмент на основе браузера.
Остаётся ли сессия авторизованной после капчи?
Да. StatefulBrowser хранит cookies, которые сайт ставит после входа, поэтому страницы, которые вы откроете дальше в том же браузере, будут открываться уже с авторизацией. Полный пример закрывает свой браузер, когда login завершается, поэтому перенесите последующие запросы внутрь этого блока with. Или создайте браузер без with, верните его и вызовите browser.close(), когда закончите.
Может ли скрипт на MechanicalSoup на VPS использовать CapSkip на моём ПК с Windows?
Да. Переключите CapSkip в режим Server в настройках подключения, чтобы он слушал сетевой адрес, а не локальную петлю. Затем задайте CAPSKIP_HOST там, где работает скрипт: эту переменную читает полный пример. VPS, задание cron и контейнер подключаются через один и тот же HTTP API. Если маршрут идёт через интернет, используйте статический публичный IP и добавьте блокирующее правило Windows Firewall для адресов, которые не должны достучаться до порта. Решатель остаётся на оборудовании, которое принадлежит вам, поэтому с ростом числа входов ваши расходы не меняются.
Коротко
Решение капчи в MechanicalSoup сводится к одному недостающему полю, потому что MechanicalSoup не может запустить виджет, который его записывает. Откройте страницу через StatefulBrowser и выберите форму вокруг виджета. Прочитайте sitekey из элемента g-recaptcha или ключ из render и action из скриптов страницы с v3, а затем решите капчу с помощью CapSkip, передав browser.url. Добавьте поле g-recaptcha-response через new_control, заполните остальное и вызовите submit_selected в том же браузере: он передаст cookies, поле CSRF и Referer. Решайте непосредственно перед отправкой, держите в работе не больше десяти входов и переключайте CapSkip в режим Server, когда скрипт работает на другой машине.
- Как работает reCAPTCHA v2 и что покрывает решатель, рассказано на странице сервиса распознавания reCAPTCHA v2.
- Все типы капчи, которые поддерживает пакет для Python, перечислены на странице сервиса распознавания капч для Python.
MechanicalSoup сохраняет парсер лёгким, потому что никогда не запускает браузер. Добавьте к нему распознавание капчи на вашей собственной машине с Windows, и тяжёлая работа останется на оборудовании под вашим контролем, без платы за каждое решение.
