Tutorials

Внедрение токена GeeTest v3 в Playwright, Puppeteer и Selenium

Если после решения GeeTest v3 форма не проходит проверку, дело обычно не в решении, а в месте подстановки — в полях формы, JS-колбэке или теле XHR-запроса.

На практике QA-инженеры в СНГ чаще всего перехватывают виджет GeeTest на формах логина или чекаута, автоматизированных через Playwright, Puppeteer или Selenium, — и место подстановки токена отличается для каждого фреймворка. Ниже — рабочие сценарии для всех трёх, плюс отдельный разбор варианта, когда сайт вообще не использует классическую форму, а отправляет результат через XHR.

Что подставляем

CaptchaAI возвращает результат из трёх частей — нужны все сразу:

{
  "geetest_challenge": "a1b2c3d4e5...modified_challenge",
  "geetest_validate": "abc123def456_validate",
  "geetest_seccode": "abc123def456_validate|jordan"
}

Каждое поле отвечает за свою часть проверки на стороне сайта:

  • geetest_challenge — идентификатор сессии решения, привязанный к конкретному challenge, который вы получили при инициализации виджета.
  • geetest_validate — подтверждение того, что проверка пройдена; сервер обычно проверяет именно это поле в первую очередь.
  • geetest_seccode — контрольный хэш, который защищает пару challenge/validate от подмены задним числом.

Если хотя бы одно значение отсутствует, проверка завершится ошибкой — типичные причины разобраны в таблице ниже.

Шаг 1. Извлеките gt и challenge, затем решите капчу

Значения gt и challenge формирует сам виджет GeeTest при инициализации — они приходят в ответе эндпоинта регистрации ещё до того, как пользователь пройдёт визуальную часть проверки. Получите их со страницы и передайте в CaptchaAI:

import requests
import time

def solve_geetest(gt, challenge, page_url):
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": "YOUR_API_KEY",
        "method": "geetest",
        "gt": gt,
        "challenge": challenge,
        "pageurl": page_url,
        "json": 1
    })
    task_id = resp.json()["request"]

    for _ in range(60):
        time.sleep(3)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": "YOUR_API_KEY",
            "action": "get",
            "id": task_id,
            "json": 1
        })
        data = result.json()
        if data["status"] == 1:
            return data["request"]  # Returns dict with three values
    raise TimeoutError("GeeTest solve timed out")

Функция solve_geetest выше — общий код для всех трёх фреймворков ниже: меняется только то, как вы перехватываете gt/challenge и куда подставляете готовое решение.

Шаг 2. Внедрение в Playwright (Python)

Перехватите gt и challenge

Поймайте ответ эндпоинта регистрации GeeTest с gt и challenge:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()

    # Capture GeeTest registration response
    geetest_data = {}
    def capture_geetest(response):
        if "register" in response.url and response.status == 200:
            try:
                data = response.json()
                if "gt" in data and "challenge" in data:
                    geetest_data.update(data)
            except Exception:
                pass

    page.on("response", capture_geetest)
    page.goto("https://staging.example.com/qa-login")
    page.wait_for_selector(".geetest_holder")

    gt = geetest_data["gt"]
    challenge = geetest_data["challenge"]

Подставьте решение в форму

Добавьте три значения в скрытые поля формы и отправьте её:

# Solve with CaptchaAI
solution = solve_geetest(gt, challenge, page.url)

# Method 1: Set hidden form fields
page.evaluate(f"""
    const fields = {{
        'geetest_challenge': '{solution["geetest_challenge"]}',
        'geetest_validate': '{solution["geetest_validate"]}',
        'geetest_seccode': '{solution["geetest_seccode"]}'
    }};
    for (const [name, value] of Object.entries(fields)) {{
        let input = document.querySelector(`input[name="${{name}}"]`);
        if (!input) {{
            input = document.createElement('input');
            input.type = 'hidden';
            input.name = name;
            document.querySelector('form').appendChild(input);
        }}
        input.value = value;
    }}
""")

# Submit the form
page.click("#submit-button")

Способ 2: вызовите обратный вызов GeeTest напрямую

Некоторые сайты слушают JS-событие виджета вместо полей формы. Подмените результат в объекте капчи и вызовите событие успеха:

page.evaluate(f"""
    // Find the GeeTest captcha object
    if (window.captchaObj) {{
        // Simulate a successful solve
        const result = {{
            geetest_challenge: '{solution["geetest_challenge"]}',
            geetest_validate: '{solution["geetest_validate"]}',
            geetest_seccode: '{solution["geetest_seccode"]}'
        }};

        // Override getValidate to return our solution
        window.captchaObj.getValidate = function() {{ return result; }};

        // Trigger the success callback
        const successEvent = new Event('geetest_success');
        document.dispatchEvent(successEvent);
    }}
""")

Шаг 3. Внедрение в Puppeteer (JavaScript)

Логика та же, что в Playwright — отличается только синтаксис перехвата ответов и то, что решение передаётся в page.evaluate аргументом объекта, а не через форматированную строку с подстановкой полей:

const puppeteer = require('puppeteer');

async function solveAndInject() {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();

  // Capture GeeTest params
  let gt, challenge;
  page.on('response', async (response) => {
    if (response.url().includes('register') && response.status() === 200) {
      try {
        const data = await response.json();
        if (data.gt && data.challenge) {
          gt = data.gt;
          challenge = data.challenge;
        }
      } catch (e) {}
    }
  });

  await page.goto('https://staging.example.com/qa-login');
  await page.waitForSelector('.geetest_holder');

  // Solve with CaptchaAI (implementation from earlier)
  const solution = await solveCaptcha(gt, challenge, page.url());

  // Inject the three values
  await page.evaluate((sol) => {
    // Set hidden inputs
    const form = document.querySelector('form');
    ['geetest_challenge', 'geetest_validate', 'geetest_seccode'].forEach(name => {
      let input = document.querySelector(`input[name="${name}"]`);
      if (!input) {
        input = document.createElement('input');
        input.type = 'hidden';
        input.name = name;
        form.appendChild(input);
      }
      input.value = sol[name];
    });
  }, solution);

  await page.click('#submit-button');
}

Шаг 4. Внедрение в Selenium (Python)

В Selenium нет перехвата ответов: gt/challenge берём через execute_script, решение внедряем так же — через скрытые поля формы:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.Chrome()
driver.get("https://staging.example.com/qa-login")

# Wait for GeeTest widget
WebDriverWait(driver, 15).until(
    EC.presence_of_element_located((By.CLASS_NAME, "geetest_holder"))
)

# Extract gt and challenge from the page
gt = driver.execute_script(
    "return document.querySelector('[data-gt]')?.dataset.gt"
)
challenge = driver.execute_script(
    "return document.querySelector('[data-challenge]')?.dataset.challenge"
)

# Solve with CaptchaAI
solution = solve_geetest(gt, challenge, driver.current_url)

# Inject via JavaScript
driver.execute_script(f"""
    var fields = {{
        'geetest_challenge': '{solution["geetest_challenge"]}',
        'geetest_validate': '{solution["geetest_validate"]}',
        'geetest_seccode': '{solution["geetest_seccode"]}'
    }};
    var form = document.querySelector('form');
    for (var name in fields) {{
        var input = document.querySelector('input[name="' + name + '"]');
        if (!input) {{
            input = document.createElement('input');
            input.type = 'hidden';
            input.name = name;
            form.appendChild(input);
        }}
        input.value = fields[name];
    }}
""")

driver.find_element(By.ID, "submit-button").click()

Если сайт отправляет результат через XHR, а не форму

Некоторые сайты не используют форму вообще, а отправляют три значения отдельным XHR-запросом. Перехватите его и подмените тело:

# Playwright: Intercept the XHR and inject values
def handle_route(route):
    if "login" in route.request.url and route.request.method == "POST":
        # Modify the POST data to include our solution
        post_data = route.request.post_data
        # Add GeeTest values to the request
        route.continue_(post_data=modified_data)
    else:
        route.continue_()

page.route("**/api/login**", handle_route)

Типичные ошибки

Проблема Причина Решение
Проверка не проходит Не все значения переданы Отправьте challenge, validate, seccode вместе
Challenge истёк Задержка перед решением Решайте сразу после извлечения
Форма без значений Неверный селектор Сверьте имена input в DOM
Callback не срабатывает Своё имя функции у сайта Найдите имя в initGeetest
Виджет не обновился Состояние не сброшено Вызовите событие успеха вручную

Если ни одна из причин не подходит, включите логирование сетевых ответов (page.on('response', ...) в Playwright или аналог в Puppeteer/Selenium) и сверьте реальные имена полей формы. На практике большинство проблем сводится либо к опечатке в имени input, либо к тому, что решение подставляется до того, как форма появилась в DOM.

Пример из практики: QA-прогон формы логина в staging

Типичный сценарий для QA-команды, тестирующей форму логина с GeeTest v3 в CI: staging-окружение поднимается на каждый pull request, виджет инициализируется на домене вида staging.example.com, и тест должен пройти форму без участия человека. Порядок действий тот же, что и выше — перехватить gt/challenge, решить через CaptchaAI, подставить три значения, — но добавляется нюанс: staging часто перевыпускает challenge при каждом деплое, поэтому кешировать значения между прогонами нельзя.

Если тест собирает данные пользователей — даже тестовые логины и email, — команды, работающие с аудиторией в РФ, обычно фиксируют это в чек-листе по 152-ФЗ «О персональных данных»: используйте только синтетические тестовые аккаунты, а не реальные данные пользователей, даже во внутреннем staging-окружении.

Частые вопросы

Почему GeeTest использует три значения, а не один токен?

geetest_challenge — сессия, geetest_validate — подтверждение, geetest_seccode — хэш от подмены. Нужны все три.

Сколько потоков CaptchaAI хватит для QA-тестирования GeeTest?

Обычно достаточно BASIC ($15/мес, 5 потоков); при росте нагрузки — STANDARD ($30/мес, 15 потоков).

Работает ли это для GeeTest v4?

Нет, CaptchaAI решает только GeeTest v3, GeeTest v4 пока в статусе «скоро».

Как проверить, что XHR-перехват сработал, если сайт не использует форму?

Залогируйте route.request.post_data до и после подмены в перехватчике Playwright. Если тело запроса после route.continue_(post_data=...) содержит все три значения — перехват сработал; если нет, проверьте, что паттерн в page.route() совпадает с реальным URL логина.

Что делать, если challenge устаревает быстрее, чем успевает пройти QA-прогон?

Сокращайте время между извлечением gt/challenge и вызовом CaptchaAI: не кешируйте эти значения между тестовыми прогонами и не откладывайте решение в фоновый воркер с задержкой.

Что читать дальше

Комментарии для этой статьи отключены.