Если один и тот же скрипт получает 0,9 на рабочем ноутбуке и 0,1 в CI, дело почти никогда не в коде. Score reCAPTCHA v3 — это выход движка Advanced Risk Analysis (ARA), который сводит десятки сигналов к числу от 0,0 (почти наверняка бот) до 1,0 (почти наверняка человек), и два самых тяжёлых входа в этой сумме — класс IP-адреса и поведение в странице. Переезд с домашнего интернета в облачный регион меняет оба сразу.
Точную формулу Google не публикует, но набор входных данных известен по многолетним измерениям и разбору клиентского кода. Ниже — разбор в порядке отладки: от того, что проверяется за минуту и чаще всего оказывается причиной, к тому, что стоит трогать последним. Все числовые сдвиги — наблюдаемые ориентиры, а не константы: веса Google меняет без анонсов.
Как сигналы складываются в итоговый score
Формально итог можно записать так:
Final Score = f(Browser Signals, Behavioral Signals, Network Signals,
History Signals, Environmental Signals)
Ключевая деталь: сигналы не суммируются линейно. Одно жёсткое противоречие — например, SwiftShader в WebGL при заявленном настольном Chrome — обесценивает несколько «правильных» сигналов, потому что несогласованность сама по себе читается как признак подмены. Наращивать «плюсы» бессмысленно, пока в наборе остаётся хотя бы одно очевидное расхождение.
Отсюда и порядок разбора, который экономит больше всего времени:
- Откуда идёт запрос — класс адреса и рукопожатие TLS.
- Чем притворяется клиент — набор браузерных API, canvas, WebGL.
- Что происходило на странице — мышь, клавиатура, прокрутка.
- Что Google уже знает про этот профиль — cookie и история.
- Мелочи окружения — часовой пояс, размеры окна, батарея.
Сеть: класс IP, TLS-отпечаток и заголовки
Начинать разбор логично не с браузера, а с сети — это самый дешёвый в диагностике блок. Google ведёт базу репутации адресов и учитывает историю проверок с конкретного IP, принадлежность к диапазонам облачных провайдеров, прокси и VPN.
| Тип IP | Типичное влияние на оценку |
|---|---|
| Домашний, без истории нарушений | Нейтрально до +0,1 |
| Домашний, с эпизодами нарушений | от −0,1 до −0,2 |
| Дата-центр | от −0,3 до −0,5 |
| Известный прокси/VPN | от −0,2 до −0,4 |
| Выходной узел Tor | от −0,4 до −0,6 |
Типичная история из практики: у команды в Алматы интеграционные тесты годами шли с рабочих ноутбуков и стабильно проходили, потом их перенесли в CI на европейский облачный регион — и score упал вдвое при неизменном коде. Менялся не скрипт, а класс адреса. Тот же эффект даёт переезд стенда с офисного канала на арендованный VPS.
Второй сетевой слой — рукопожатие TLS. Набор шифров, их порядок и список расширений у каждого клиента свои, и по ним отпечаток JA3/JA4 читается ещё до того, как отработает хоть одна строка JavaScript:
Chrome— TLS 1.3, характерный порядок шифров, расширения GREASE.python requests— другой порядок, GREASE нет, список расширений короче.curl— минимальный набор по шаблонам OpenSSL.
Небраузерный отпечаток стоит от −0,2 до −0,4, и подменой одного User-Agent это не лечится: заголовок и рукопожатие формируются разными слоями, и их расхождение хорошо видно на стороне сервера.
Сами заголовки проверяются на согласованность друг с другом:
| Заголовок | Признак человека | Признак бота |
|---|---|---|
User-Agent |
Соответствует реальной версии браузера | Устаревшая версия, UA инструмента автоматизации |
Accept-Language |
Заполнен языковыми настройками | Отсутствует или слишком общий |
Accept-Encoding |
gzip, deflate, br |
Отсутствует или нетипичен |
Sec-CH-UA |
Client Hints, согласованные с браузером | Отсутствуют (набор заголовков до 2022 года) |
Браузерная среда: проверка на подлинность
Набор доступных JavaScript API
reCAPTCHA опрашивает API, которых в браузере в headless-режиме либо нет, либо они реализованы иначе:
| Проверяемый API | Ожидается в обычном браузере | Признак headless-режима |
|---|---|---|
navigator.webdriver |
undefined или false |
true (Selenium, Puppeteer) |
navigator.plugins |
PluginArray с одним и более элементом | Пустой или отсутствует |
navigator.languages |
Массив строк локали | Пустой массив или отсутствует |
navigator.hardwareConcurrency |
2–16 (соответствует CPU) | Иногда 0 или undefined |
navigator.deviceMemory |
2–16 ГБ | В части сборок отсутствует |
window.chrome |
Объект с runtime и прочим |
Отсутствует в headless Chrome |
Notification.permission |
default, granted или denied |
Бросает исключение или отсутствует |
Влияние на оценку: пропуски и неправдоподобные значения в этом наборе стоят от 0,3 до 0,5 балла.
Отпечаток canvas
Движок отрисовывает скрытый canvas и считывает пиксели. Результат уникален для связки браузер/OS/GPU.
// reCAPTCHA performs something similar to:
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.textBaseline = "top";
ctx.font = "14px 'Arial'";
ctx.fillText("Hello", 2, 2);
const fingerprint = canvas.toDataURL();
Как это читается движком:
| Результат отрисовки | Сдвиг оценки |
|---|---|
| Стабильный, согласованный с заявленными браузером и OS | нейтрально |
| Совпадает с известным шаблоном headless-сборки | от −0,3 до −0,5 |
| Операции с canvas заблокированы или дают однородные данные | от −0,4 до −0,6 |
Обратите внимание: блокировка canvas хуже, чем «неидеальный» результат. Отсутствие сигнала — тоже сигнал, и приватные расширения, вырезающие canvas, регулярно роняют score живым пользователям.
Строка WebGL-рендерера
Строка рендерера показывает видеокарту:
const gl = document.createElement('canvas').getContext('webgl');
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
// Expected: "ANGLE (NVIDIA GeForce GTX 1060...)"
// Headless: "SwiftShader" or "Google SwiftShader"
Влияние на оценку: SwiftShader — программный рендерер, который Chrome использует в headless-режиме, — снижает score на 0,3–0,5.
Поведение в странице: блок с наибольшим весом
Поведение собирается с момента загрузки страницы и до отправки формы. Это единственная категория, которая способна утопить оценку в одиночку: даже безупречная браузерная среда не спасает страницу, на которой не было ни одного движения курсора.
Что видно по движению мыши
reCAPTCHA пишет координаты курсора и временные метки на протяжении всего визита.
| Сигнал | Человек | Бот |
|---|---|---|
| Количество событий | 50–500+ за просмотр страницы | 0–5 событий |
| Траектория | Изогнутые пути с переменной скоростью | Прямые линии, постоянная скорость |
| Ускорение | Плавные разгон и торможение | Мгновенные старт и стоп |
| Микродвижения | Лёгкое дрожание при наведении | Полная неподвижность между действиями |
| Промах по цели | Небольшой перелёт с коррекцией | Идеальное попадание в центр элемента |
Влияние на оценку: полное отсутствие движений мыши стоит от −0,4 до −0,7 — больше, чем любой другой одиночный фактор.
Тайминги клавиатуры
Inter-key timing:
Human: Variable (50-300ms between keys, varies by key pair)
Bot: Constant (all keys at same interval, or instant paste)
Key events:
Human: keydown → keypress → keyup for each character
Bot: May skip keypress, or fire all events simultaneously
Corrections:
Human: Occasional backspace, re-typing
Bot: Perfect input without corrections
Влияние на оценку: мгновенная подстановка текста — через буфер обмена или присвоение value в JavaScript — снижает score на 0,1–0,2.
Прокрутка и порядок взаимодействия
Человек прокручивает страницу неравномерно, с паузами на чтение, кликает по посторонним элементам и наводит курсор на ссылки. Бот открывает страницу и сразу оказывается в полях формы.
| Что фиксирует движок | Сдвиг оценки |
|---|---|
| Ни одного события прокрутки перед работой с формой | от −0,2 до −0,3 |
| Переход сразу к полям без «осмотра» страницы | от −0,1 до −0,3 |
| Заполнение полей строго по порядку DOM, без возвратов | от −0,1 до −0,2 |
Последняя строка чаще всего и выдаёт скрипт: живой человек возвращается к уже заполненному полю, ошибается в раскладке, перечитывает подпись под чекбоксом.
История клиента: cookie, сессия и репутация профиля
Четвёртый по порядку разбора, но не по весу, блок — то, что Google уже знает об этом клиенте. Пользователь с активной сессией Google и накопленной историей стартует с более высокой позиции, чем чистый контейнер.
| Что видит движок | Сдвиг оценки |
|---|---|
Cookie SID, HSID, SSID — активная сессия Google |
от +0,1 до +0,3 |
Cookie _GRECAPTCHA — было успешное решение ранее |
нейтрально до +0,1 |
| Ни одной cookie Google и reCAPTCHA | от −0,1 до −0,2 |
Служебные rc::a, rc::b, rc::c хранят данные анализа рисков и живут вместе с профилем. Отсюда три практических правила для стендов:
- Профиль браузера, переживающий перезапуски, работает заметно лучше одноразового контейнера — каталог профиля стоит монтировать томом, а не пересоздавать на каждый прогон.
- Один профиль на один домен: Google сопоставляет поведение профиля между сайтами, и грубая автоматизация на одном из них тянет вниз все остальные.
- Профиль, который месяцами использовался только для прогонов и ни разу не видел живой сессии, со временем перестаёт давать прибавку — репутация накапливается по факту нормального использования, а не по возрасту cookie.
Окружение: слабые, но заметные корректоры
Этот блок сам по себе оценку почти не двигает, но исправно добивает и без того низкий score. Все три сигнала проверяются одинаково: движок сверяет заявленное браузером с тем, что видно снаружи.
Первый — часовой пояс:
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// Should match IP geolocation
// "America/New_York" with a New York IP → consistent
// "America/New_York" with a Tokyo IP → suspicious
Расхождение часового пояса с геолокацией адреса — классический побочный эффект прокси: контейнер живёт в UTC, а адрес принадлежит другому региону. Для команд, которые арендуют прокси в одной стране, а стенды держат в другой, это регулярный источник необъяснимых просадок. Лечится дёшево: задайте контейнеру TZ под регион выходного узла и держите то же значение в языковых настройках профиля.
Второй — геометрия экрана и окна:
screen.width, screen.height // Physical screen
window.innerWidth, innerHeight // Browser window
window.outerWidth, outerHeight // Including browser chrome
Третий — батарея, если API доступен:
navigator.getBattery().then(battery => {
battery.level; // 0.0 to 1.0
battery.charging; // true/false
});
Итог по блоку:
| Сигнал | Что настораживает движок | Сдвиг оценки |
|---|---|---|
| Часовой пояс | UTC при адресе из другого региона | от −0,1 до −0,2 |
| Размеры окна | 0×0, чрезмерно большие или ровно дефолтные для инструмента | от −0,1 до −0,2 |
| Батарея | постоянные 100 % заряда и подключение к сети | почти нейтрально |
Что с этим делать в автоматизации
Вариант первый: приводить среду в порядок
| Мера | Ожидаемый эффект | Трудоёмкость |
|---|---|---|
| Запускать настоящий Chrome вместо headless-режима | от +0,3 до +0,5 | низкая |
Убрать флаг navigator.webdriver |
от +0,2 до +0,3 | низкая |
| Добавить имитацию движения мыши | от +0,2 до +0,4 | средняя |
| Использовать резидентный прокси | от +0,2 до +0,3 | зависит от бюджета |
| Сохранять cookie Google между запусками | от +0,1 до +0,2 | средняя |
| Согласовать TLS-отпечаток с Chrome | от +0,1 до +0,3 | высокая |
| Имитировать осмотр страницы до формы | от +0,1 до +0,2 | средняя |
Каждая строка выполнима по отдельности. Проблема в том, что поддерживать весь набор постоянно — это отдельный продукт внутри вашего: Chrome обновляется, наборы шифров меняются, пул прокси деградирует.
Вариант второй: получать токен через API
Вместо ручной подгонки всех сигналов задачу можно отправить в CaptchaAI и забрать готовый токен. Ниже — стандартная схема «отправить задачу → опрашивать res.php» для reCAPTCHA v3 на QA-стенде:
import requests
import time
API_KEY = "YOUR_API_KEY"
# Request a high-score token
submit = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": "6LcR_RsTAAAAAN_r0GEkGBfq3L7KmU5JbPHJtwNp",
"pageurl": "https://staging.example.com/qa-login",
"version": "v3",
"action": "login",
"json": 1,
})
task_id = submit.json()["request"]
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY,
"action": "get",
"id": task_id,
"json": 1,
}).json()
if result.get("status") == 1:
token = result["request"]
print(f"High-score token received")
break
Полученное значение подставляется в поле g-recaptcha-response вашей формы. Параметры googlekey и pageurl берутся со страницы, где стоит проверка, а action должен совпадать с тем, что передаёт сам сайт, — иначе Google посчитает контекст несогласованным.
Тарификация здесь по потокам, а не по числу решений: план BASIC ($15/мес, 5 потоков) закрывает регрессионный прогон небольшой команды, ADVANCE ($90/мес, 50 потоков) — параллельный CI. Один поток занят ровно на время одного решения и сразу берёт следующее, поэтому считать нужно пиковую одновременность, а не суточный объём задач. Для агентств и фрилансеров, которые получают доход в валюте, отличной от USD, это удобно тем, что сумма счёта фиксирована и не зависит от того, сколько проверок отработал прогон.
Схема вызова одинакова для всех поддерживаемых типов: reCAPTCHA v2 и v3, Cloudflare Turnstile и Cloudflare Challenge, GeeTest v3, image/OCR, grid и BLS — меняются только method и параметры типа, так что один обработчик закрывает сразу несколько сценариев. hCaptcha и FunCaptcha сервис не решает, GeeTest v4 заявлен как готовящийся, а CaptchaFox, Friendly Captcha и Lemin доступны в статусе beta.
И ремарка про данные: если автоматизация попутно собирает пользовательские сведения, ограничьте выборку тем, что вы вправе обрабатывать, — требования 152-ФЗ и GDPR к минимизации данных применимы здесь так же, как везде.
Часто задаваемые вопросы
Какой score считается нормальным?
Ориентир — 0,7 и выше: такое значение большинство внедрений пропускает без дополнительной проверки. Порог владелец сайта задаёт сам, типовой диапазон — от 0,5 до 0,7. Значения ниже 0,3 почти всегда означают дополнительную проверку или отказ.
Всегда ли VPN снижает оценку?
Не сам по себе. Значение имеет репутация конкретного адреса: выходные узлы VPN в дата-центрах теряют от 0,2 до 0,4, а адреса резидентного класса ведут себя заметно лучше. Оценивается диапазон, а не факт использования VPN.
Почему score отличается на разных страницах одного сайта?
reCAPTCHA v3 учитывает параметр action, и у разных действий (главная, вход, оформление заказа) базовый уровень свой. Плюс поведенческие данные копятся в течение сессии: на второй-третьей странице движку доступно больше материала, поэтому оценка часто выше, чем на первой.
Можно ли задать нужный score прямо в запросе к API?
Нет. Параметра вроде score-target не существует: значение назначает Google на своей стороне. Повлиять можно только косвенно — передавая в запросе cookies, userAgent и proxy, чтобы контекст решения был ближе к реальной пользовательской сессии. Для сайтов на reCAPTCHA Enterprise дополнительно указывается enterprise=1.
Что чинить первым при низком score?
Класс IP и поведение. Переход на резидентный прокси и имитация движений мыши обычно дают больший прирост, чем любая правка navigator.*. Тонкая настройка TLS-отпечатка — последний шаг, а не первый: она дорогая по трудозатратам и почти не помогает, пока запросы идут из диапазона дата-центра.
Коротко
Веса распределены неравномерно: класс адреса и поведение решают больше, чем браузерные API, cookie и окружение вместе взятые. В этом же порядке стоит вести отладку — иначе легко потратить неделю на navigator.* и не сдвинуть оценку.
Порядок действий, если score просел на стенде:
- Проверьте, из какого диапазона идут запросы, и при необходимости переведите прогон на резидентный прокси.
- Добавьте движение курсора и прокрутку перед отправкой формы.
- Сохраните профиль браузера между запусками.
- Только после этого беритесь за
navigator.*, canvas и TLS-отпечаток.
Если автоматизации нужен предсказуемый результат без бесконечной поддержки браузерной обвязки, отдайте получение токена в CaptchaAI и передавайте вместе с задачей cookies, userAgent и резидентный proxy.