Как решить Friendly Captcha на Python по тегу script

Чтобы решить Friendly Captcha на Python, прочитайте data-sitekey с элемента frc-captcha, найдите тег script, который загружает виджет, отличите по этому скрипту v1 от v2 и передайте sitekey, URL страницы, версию и полный адрес скрипта в метод friendly_captcha из CapSkip. В ответ вы получите токен, который нужно отправить в поле frc-captcha-solution для v1 или frc-captcha-response для v2. Всё решает именно тег script. Под одним названием и с одним форматом sitekey Friendly Captcha поставляет два не связанных между собой протокола, и если решить не тот, вернётся токен, который выглядит корректным, но будет молча отклонён. CapSkip добавил Friendly Captcha в версии 1.4.0. В этом руководстве разобрано, как прочитать страницу, как сделать вызов, как отправить токен и как запускать много решений одновременно с AsyncCapSkip.
Что понадобится
- CapSkip 1.4.0 или новее на машине с Windows. Именно в этом выпуске появилась Friendly Captcha, а вместе с ней CaptchaFox и Capy Puzzle.
- Python 3.10 или новее и пакет capskip версии 1.3.0 или новее: это первый выпуск, в котором есть friendly_captcha. В примерах также используется requests, чтобы загрузить страницу и отправить форму.
- URL страницы, на которой показан виджет. Sitekey, адрес скрипта и имя поля берутся из HTML этой страницы, а где именно, показывает шаг 1.
- Адрес решателя. Режим Local отвечает на 127.0.0.1 и только для этого устройства; режим Server слушает ваш сетевой адрес или публичный IP, чтобы скрипт на другой машине мог обращаться к нему через API. Оба режима задаются в разделе Настройки подключения, а когда переключаться, объясняет шаг 4.
# pip install capskip requests pip install "capskip>=1.3.0" requests
Не убирайте кавычки. В cmd знак «больше» без кавычек означает перенаправление: pip установит любую найденную версию capskip без всякой проверки и запишет свой вывод в файл с именем 1.3.0. PowerShell передаёт аргумент как есть, но кавычки работают в любой оболочке.
Шаг 1: читаем виджет и его тег script
Обе версии выводят один и тот же элемент: div с классом frc-captcha и атрибутом data-sitekey. Поэтому элемент даёт вам sitekey, но ничего не говорит о протоколе. Версия указана в теге script, который загружает виджет, потому что v1 и v2 представляют собой разные пакеты с разными именами файлов:
<!-- v2: the @friendlycaptcha/sdk package --> <div class="frc-captcha" data-sitekey="YOUR_SITEKEY"></div> <script type="module" src="https://cdn.jsdelivr.net/npm/@friendlycaptcha/sdk/site.min.js" async defer></script> <script nomodule src="https://cdn.jsdelivr.net/npm/@friendlycaptcha/sdk/site.compat.min.js" async defer></script> <!-- v1: the friendly-challenge package --> <div class="frc-captcha" data-sitekey="YOUR_SITEKEY"></div> <script type="module" src="https://cdn.jsdelivr.net/npm/friendly-challenge/widget.module.min.js" async defer></script> <script nomodule src="https://cdn.jsdelivr.net/npm/friendly-challenge/widget.min.js" async defer></script>
Большинство сайтов фиксируют номер версии внутри этого пути, но здесь это ничего не меняет. Чтобы всё это прочитать, хватит стандартной библиотеки Python. Этот парсер сохраняет атрибуты элемента виджета и адрес каждого скрипта, который не служит запасным вариантом nomodule, разрешая его относительно URL страницы:
# pip install capskip requests
from html.parser import HTMLParser
from urllib.parse import urljoin
import requests
class FriendlyPage(HTMLParser):
"""Collects the frc-captcha element and the page's script tags."""
def __init__(self, page_url):
super().__init__()
self.page_url = page_url
self.widget = {}
self.scripts = []
def handle_starttag(self, tag, attrs):
a = dict(attrs)
if "frc-captcha" in (a.get("class") or "").split():
self.widget = a
# Skip nomodule fallbacks; keep full addresses, never relative.
if tag == "script" and a.get("src") and "nomodule" not in a:
self.scripts.append(urljoin(self.page_url, a["src"]))
session = requests.Session()
page = FriendlyPage("https://example.com/signup")
page.feed(session.get(page.page_url, timeout=30).text)Вызов urljoin здесь не лишний. У виджета, который сайт хостит у себя, src может быть относительным, например /js/site.min.js, а CapSkip нужен адрес, который он может загрузить: для v2 он загружает тот же скрипт в своём браузере, чтобы выполнить решение. Передайте голый путь, и виджет так и не загрузится.
Шаг 2: выбираем версию и вызываем friendly_captcha
CapSkip определяет версию в фиксированном порядке и останавливается на первом ответе: сначала версия, которую вы передали, затем адрес скрипта, переданный как module_script, затем значение по умолчанию v1. Ловушка кроется именно в последнем шаге. Скрипт, из которого CapSkip не может прочитать версию, например собранный бандл /assets/app.4f2a.js, молча решается как v1, и тогда сайт на v2 отклоняет каждый токен.
Поэтому определяйте версию в своём коде, где можно отказаться гадать. Имя пакета в адресе CDN однозначно. Сборка, которую сайт хостит у себя, часто его теряет, но сохраняет имя файла, как это делает официальный плагин для WordPress: site.min.js означает v2, а widget.module.min.js или widget.min.js означает v1. Собственные атрибуты виджета остаются последним средством, потому что две версии по-разному называют свои опции:
from urllib.parse import urlparse
V2_FILES = {"site.min.js", "site.compat.min.js"}
V1_FILES = {"widget.module.min.js", "widget.min.js", "widget.polyfilled.min.js"}
def friendly_version(page):
# Package names are unambiguous, so check them first.
for src in page.scripts:
if "@friendlycaptcha/sdk" in src:
return "v2", src
if "friendly-challenge" in src:
return "v1", src
# Self-hosted builds usually keep the file name. Themes ship their own
# site.min.js too, so only trust a path that says friendly.
for src in [s for s in page.scripts if "friendly" in s.lower()]:
name = urlparse(src).path.rsplit("/", 1)[-1]
if name in V2_FILES:
return "v2", src
if name in V1_FILES:
return "v1", src
# Last resort: v2 and v1 name their widget options differently.
if {"data-api-endpoint", "data-form-field-name"} & page.widget.keys():
return "v2", None
if {"data-puzzle-endpoint", "data-solution-field-name"} & page.widget.keys():
return "v1", None
raise RuntimeError("v1 or v2? Read the page and set it by hand.")Проверка по имени файла доверяет только путям, в которых упоминается friendly, потому что тема сайта может загружать собственный site.min.js, не имеющий никакого отношения к виджету.
Затем сделайте вызов. Отправьте найденную версию, а также адрес скрипта, если он у вас есть. Явно указанная версия имеет приоритет, а для v2 адрес говорит CapSkip решать с той самой сборкой, которую загружает сайт. Значение None просто не попадает в запрос.
from capskip import CapSkip
solver = CapSkip(host="127.0.0.1", port=8080)
version, script = friendly_version(page)
result = solver.friendly_captcha(
page.widget["data-sitekey"],
"https://example.com/signup",
version=version,
module_script=script,
# data-api-endpoint="eu" (v2) or data-puzzle-endpoint (v1).
api_server=page.widget.get("data-api-endpoint")
or page.widget.get("data-puzzle-endpoint"),
)
print(result["token"][:40]) # v2 tokens start with AQQA.Строка с api_server нужна для сайтов на эндпоинте EU сервиса Friendly Captcha. Для sitekey сайта из EU глобальный эндпоинт тоже выдаст токен, поэтому такое решение провалится только на собственной проверке сайта: это тот же тихий сбой, что и с версией. Если ни одного из атрибутов нет, значение равно None, и CapSkip использует глобальный эндпоинт.
Результат приходит в виде обычного dict. result["token"] содержит строку для отправки, result["code"] хранит ту же строку для скриптов, перенесённых с другого решателя, а result["captchaId"] служит идентификатором задания в CapSkip. Версия, отличная от v1, v2, 1 или 2, пустой sitekey или опция, которую метод не принимает, вызывают ValidationException ещё до какой-либо отправки.
Шаг 3: отправляем токен в поле, которое использует виджет
Второе различие между версиями касается имени поля, и сайт может переименовать его прямо на элементе виджета:
import requests
# A renamed field wins; otherwise the default for the version.
field = (
page.widget.get("data-form-field-name") # v2 rename
or page.widget.get("data-solution-field-name") # v1 rename
or ("frc-captcha-response" if version == "v2" else "frc-captcha-solution")
)
# session is the requests.Session that fetched the page.
resp = session.post(
"https://example.com/signup",
data={"email": "YOUR_EMAIL", field: result["token"]},
# Browsers send the page as Referer; some servers refuse a post without it.
headers={"Referer": page.page_url},
timeout=30,
)
print(resp.status_code)Особый случай для v1: если data-solution-field-name состоит из одного дефиса, виджет вообще не пишет скрытое поле, а собственный скрипт сайта отправляет токен каким-то другим способом. Повторите этот запрос, как описано ниже.
Отправляйте туда, куда указывает атрибут action формы, и включайте остальные поля формы, в том числе скрытые, например CSRF-токен. Если загружать страницу и отправлять форму через один requests.Session, сохраняются cookies сайта, от которых обычно зависит этот CSRF-токен. Передавайте URL страницы и как Referer: requests не отправляет ни заголовок Referer, ни Origin, а некоторые фреймворки, в том числе Django, отклоняют отправку формы по HTTPS, в которой нет ни того ни другого, какими бы правильными ни были cookie и токен.
Передавайте токен в теле через data= и никогда в URL через params=. Токен v2 весит примерно шесть килобайт, и этого хватает, чтобы упереться в лимит сервера на длину URL, а токен v1 состоит из четырёх частей, разделённых точками, и занимает несколько сотен символов. Передавайте его дословно, ничего не обрезая и не перекодируя. Он годится для одной отправки: проверка Friendly Captcha отклоняет ответ, который уже был использован или истёк, поэтому решайте заново для каждой формы.
Некоторые сайты отправляют форму через JavaScript с телом JSON, а не обычной отправкой формы. Если отправка не проходит при правильном имени поля, откройте DevTools, один раз отправьте форму вручную и скопируйте ровно то, что отправляет страница.
Шаг 4: много форм одновременно и где работает решатель
AsyncCapSkip в Python представляет собой настоящий асинхронный клиент на базе httpx, а не псевдоним блокирующего, поэтому один event loop может держать в работе много решений. Ограничьте их семафором, чтобы одновременно никогда не выполнялось больше решений, чем CapSkip готов взять в работу. Настройка Max. Threads в разделе Friendly Captcha в CapSkip по умолчанию равна 10.
import asyncio
from capskip import AsyncCapSkip
solver = AsyncCapSkip(host="127.0.0.1", port=8080)
limit = asyncio.Semaphore(10) # match Friendly Captcha Max. Threads
async def solve(sitekey, url, version, script):
async with limit:
r = await solver.friendly_captcha(
sitekey, url, version=version, module_script=script)
return r["token"]
async def main(jobs):
# jobs: (sitekey, url, version, script) tuples from Step 2
return await asyncio.gather(*(solve(*j) for j in jobs))Будьте готовы к тому, что время решения будет плавать. Friendly Captcha задаёт объём работы для каждого запроса и повышает его для адресов, которые уже видела много раз, а CapSkip решает каждый виджет v2 в настоящем браузере. Поэтому метод опрашивает результат с таймаутом recaptchaTimeout, по умолчанию 300 секунд, а не с defaultTimeout в 120 секунд. У CapSkip есть и собственные лимиты. В его настройках Friendly Captcha задание может ждать свободного потока 250 секунд, а затем решаться 120 секунд, прежде чем CapSkip признает его неудачным. Так что медленному решению нужен более высокий Row Timeout, а собственный таймаут SDK (по умолчанию 300 секунд или timeout= на вызов) должен покрывать и его, и время, которое задание ждёт свободного потока. Семафор из примера выше сводит это ожидание почти к нулю. Если на долгом прогоне решения замедляются, обычно причина кроется в растущей сложности для одного адреса. Передавайте прокси на каждый запрос в виде dict с ключами type и uri или настройте пул прокси в CapSkip, чтобы не все решения шли с одного и того же адреса.
В примерах стоит 127.0.0.1, потому что это верно, пока ваш скрипт и решатель делят одну машину. Как только скрипт запускается где-то ещё, например на VPS, в контейнере или на раннере CI, локальная петля указывает не на ту машину, и первый же вызов поднимает NetworkException. Переключите CapSkip в режим Server, и он будет слушать ваш сетевой адрес или публичный IP, так что любой из этих вариантов сможет обратиться к нему через тот же API. Если маршрут идёт через интернет, используйте статический публичный IP с правилом файрвола для ожидаемых адресов. Это по-прежнему ваше собственное железо, и по-прежнему без платы за каждое решение. Клиент сам не читает переменные окружения, поэтому читайте CAPSKIP_HOST в своём коде и передавайте его клиенту, как это делает полный пример ниже.
Полный рабочий пример
# pip install capskip requests
import os
from html.parser import HTMLParser
from urllib.parse import urljoin, urlparse
import requests
from capskip import CapSkip
from capskip.exceptions import CapSkipError, ValidationException
PAGE_URL = "https://example.com/signup"
V2_FILES = {"site.min.js", "site.compat.min.js"}
V1_FILES = {"widget.module.min.js", "widget.min.js", "widget.polyfilled.min.js"}
class FriendlyPage(HTMLParser):
def __init__(self, page_url):
super().__init__()
self.page_url, self.widget, self.scripts = page_url, {}, []
def handle_starttag(self, tag, attrs):
a = dict(attrs)
if "frc-captcha" in (a.get("class") or "").split():
self.widget = a
if tag == "script" and a.get("src") and "nomodule" not in a:
self.scripts.append(urljoin(self.page_url, a["src"]))
def friendly_version(page):
for src in page.scripts:
if "@friendlycaptcha/sdk" in src:
return "v2", src
if "friendly-challenge" in src:
return "v1", src
for src in [s for s in page.scripts if "friendly" in s.lower()]:
name = urlparse(src).path.rsplit("/", 1)[-1]
if name in V2_FILES or name in V1_FILES:
return ("v2" if name in V2_FILES else "v1"), src
keys = page.widget.keys()
if {"data-api-endpoint", "data-form-field-name"} & keys:
return "v2", None
if {"data-puzzle-endpoint", "data-solution-field-name"} & keys:
return "v1", None
raise SystemExit("v1 or v2? Read the page and set it by hand.")
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")),
)
session = requests.Session()
page = FriendlyPage(PAGE_URL)
page.feed(session.get(PAGE_URL, timeout=30).text)
if not page.widget.get("data-sitekey"):
raise SystemExit("No frc-captcha widget in the HTML; it may be built by JS.")
version, script = friendly_version(page)
try:
result = solver.friendly_captcha(
page.widget["data-sitekey"], PAGE_URL,
version=version, module_script=script,
api_server=page.widget.get("data-api-endpoint")
or page.widget.get("data-puzzle-endpoint"),
)
except ValidationException as exc:
raise SystemExit(f"not sent: {exc}")
except CapSkipError as exc:
raise SystemExit(f"solve failed: {exc!r}")
field = (page.widget.get("data-form-field-name")
or page.widget.get("data-solution-field-name")
or ("frc-captcha-response" if version == "v2" else "frc-captcha-solution"))
# Post wherever the form's action points, with its other fields.
# Browsers send the page as Referer; some servers refuse a post without it.
resp = session.post(PAGE_URL, data={"email": "YOUR_EMAIL", field: result["token"]},
headers={"Referer": PAGE_URL}, timeout=30)
print(resp.status_code, version, field)Версия определяется один раз и используется дважды: для решения и для имени поля, поэтому они никогда не разойдутся. Если парсер не находит виджет, обычно это значит, что страница строит его из JavaScript, и sitekey нужно брать с отрендеренной страницы или из скрипта, который его создаёт. Все параметры, которые принимает сырой эндпоинт, описаны в справочнике API Friendly Captcha.
Частые ошибки и что они означают
| Что вы видите | Причина | Исправить |
|---|---|---|
| Сайт отклоняет токен, который CapSkip вернул без ошибки | Решена не та версия, часто потому, что по умолчанию молча применилась v1 | Определите версию в коде, как в шаге 2, и передайте её |
| Отклоняется на сайте, где у виджета есть data-api-endpoint (v2) или data-puzzle-endpoint (v1) | Токен пришёл с глобального эндпоинта, а сайт использует эндпоинт EU | Передайте значение атрибута как api_server |
| Токен верный, но отправка всё равно не проходит | Он попал не в то поле, запрос ушёл без Referer, или сайт отправляет JSON | Проверьте атрибуты переименования и передайте URL страницы как Referer, затем повторите запрос, подсмотрев его в DevTools |
| Решение v2 не проходит на сайте, который хостит виджет у себя | module_script был относительным путём, поэтому виджет так и не загрузился | Разрешите src через urljoin, прежде чем передавать его |
| ApiException с ERROR_CAPTCHA_UNSOLVABLE через несколько секунд, каждый раз | Friendly Captcha отклоняет sitekey или origin страницы, либо в аккаунте, которому принадлежит ключ, не включены v2 или эндпоинт EU | Проверьте sitekey, URL страницы и api_server; повторные попытки не помогут |
| ValidationException ещё до какой-либо отправки | Пустой sitekey, версия, отличная от v1, v2, 1 или 2, или неизвестная опция | Проверьте, что парсер нашёл виджет, и уберите опцию, названную в сообщении |
| Решения замедляются по ходу долгого прогона | Friendly Captcha повышает объём работы для слишком активного адреса | Добавьте прокси на каждый запрос или пул прокси в CapSkip |
| ApiException с ERROR_CAPTCHA_UNSOLVABLE через две минуты или позже | Решение вышло за Row Timeout в CapSkip (120 секунд) или ждало свободного потока дольше Wait Timeout (250 секунд) | Держите семафор на уровне Max. Threads, добавьте прокси или поднимите Row Timeout в настройках Friendly Captcha |
| TimeoutException через 300 секунд | Задание ждало свободного потока, а затем решалось, и вместе это длилось дольше, чем SDK опрашивает результат | Держите семафор на уровне Max. Threads или передайте более длинный timeout= |
| NetworkException на первом вызове | CapSkip не запущен либо неверны хост и порт | Запустите CapSkip, затем проверьте, какой режим нужен: Local или Server |
FAQ
Почему бы не передать только module_script и не дать CapSkip решить самому?
Можно, и для скрипта, загруженного с CDN, это работает, потому что имя пакета есть в адресе. Риск кроется в запасном варианте. Если адрес не называет ни одной сборки, которую узнаёт CapSkip, он решает v1, а не выдаёт ошибку, и ничто в результате вам об этом не скажет. Проверка в собственном коде превращает это молчаливое значение по умолчанию в исключение, которое вы видите.
Нужен ли для решения браузер на моей стороне, например Selenium или Playwright?
Нет. Вашему скрипту нужен только HTML, а его загружает requests. Вся работа с браузером происходит внутри CapSkip: виджеты v2 он решает в своём браузере, а головоломки v1 напрямую. Если вы и так управляете браузером по другим причинам, тот же вызов тоже работает; просто читайте sitekey и адрес скрипта с живой страницы.
Может ли скрипт на VPS или в облаке использовать решатель на моём ПК с Windows?
Да. Переведите CapSkip в режим Server в настройках подключения, чтобы он слушал сетевой адрес, а не локальную петлю, задайте CAPSKIP_HOST там, где работает скрипт, и передайте его клиенту, как это делает полный пример. VPS, контейнер и хостинговый раннер подключаются через один и тот же HTTP API. Если маршрут идёт через интернет, используйте статический публичный IP с правилом файрвола. Решатель остаётся на вашем собственном железе, поэтому рост числа решений никак не меняет ваши расходы.
Чем это отличается от руководства по C#?
Сырой эндпоинт, опции и поля результата одинаковы во всех SDK CapSkip; меняется только написание. Настоящая разница кроется в коде вокруг них: парсер из стандартной библиотеки, сессия requests, которая хранит cookies сайта, и AsyncCapSkip, который в Python представляет собой отдельный асинхронный клиент, а в .NET служит просто другим именем для CapSkipClient, и без того асинхронного. Версия этого процесса для .NET описана в руководстве по Friendly Captcha на C#, а подробнее о параллельных решениях в Python рассказано в руководстве по параллельному решению капчи.
Коротко
Чтобы решить Friendly Captcha на Python, разберите страницу, найдите на ней элемент frc-captcha и его теги script и превратите каждый src в полный адрес. Определите v1 или v2 по имени пакета или по собственным атрибутам виджета, а если ни то ни другое не подсказывает, выбросьте исключение. Вызовите friendly_captcha с sitekey, URL страницы, этой версией и адресом скрипта, а для сайтов из EU добавьте api_server. Отправьте токен один раз, в теле запроса, в поле, которое задают версия и виджет.
- Как работает этот тип и что покрывает решатель: страница решения Friendly Captcha.
- Все остальные типы капчи, которые решает пакет для Python, описаны на странице сервиса распознавания капч для Python.
И ещё одно о больших объёмах. Friendly Captcha берёт плату за сложность процессорным временем, а не деньгами, поэтому цена загруженного дня измеряется временем решения на вашей собственной машине. Запустите безлимитный сервис распознавания капчи локально, и ограничивать вас будут только эта машина и потоки, которые вы ей выделите, а не счётчик.
