Use Cases

Многоэтапная автоматизация касс с решением CAPTCHA

QA-скрипт проходит корзину, а падает на шаге доставки или оплаты — CAPTCHA всплывает в разных точках у разных сайтов: где-то reCAPTCHA v3 незаметно проверяет добавление в корзину, где-то Turnstile ловит слишком быстрое заполнение адреса. Ниже — один класс на Python, который решает CAPTCHA перед каждым шагом чекаута, от корзины до подтверждения.


Где в чекауте обычно стоит CAPTCHA

Этап оформления заказа Какая CAPTCHA чаще встречается Почему её показывают
Добавление в корзину reCAPTCHA v3 (невидимая) Защита от ботов
Вход или гостевой чекаут reCAPTCHA v2 Проверка аккаунта
Адрес доставки Cloudflare Turnstile Ограничение частоты запросов
Страница оплаты reCAPTCHA v2 или v3 Защита от мошенничества
Подтверждение заказа reCAPTCHA v2 Финальная проверка

Что подготовить перед автоматизацией

Прежде чем гонять сценарий на реальном чекауте, соберите четыре вещи:

  • API-ключ CaptchaAI — берётся в панели управления после регистрации, без него CaptchaSolver не сможет отправить задачу.
  • Staging-версия магазина или тестовый режим платёжного провайдера — прогонять полный чекаут на боевом каталоге не нужно.
  • Тестовые данные карты — Stripe, Braintree и большинство платформ выдают отдельные номера для sandbox; они не проходят как реальные платежи.
  • Список селекторов под конкретный сайт_find, _click и _fill в примере ниже используют CSS-селекторы с запасными вариантами (selector1, selector2); их придётся подстроить под разметку целевой страницы.

Архитектура: пять точек, где может понадобиться решение CAPTCHA

┌────────┐    ┌───────────┐    ┌──────────┐    ┌─────────┐    ┌──────────┐
│  Cart  │───▶│  Shipping │───▶│ Payment  │───▶│ Review  │───▶│ Confirm  │
│        │    │  Address  │    │  Info    │    │  Order  │    │          │
└────────┘    └───────────┘    └──────────┘    └─────────┘    └──────────┘
     │              │               │               │              │
     ▼              ▼               ▼               ▼              ▼
  [CAPTCHA?]    [CAPTCHA?]     [CAPTCHA?]      [CAPTCHA?]     [CAPTCHA?]

Класс для автоматизации чекаута

Solver и автоматизатор шагов

CaptchaSolver отправляет задачу в CaptchaAI и опрашивает результат, CheckoutAutomator вызывает _solve_if_captcha() перед каждым шагом.

import time
import requests
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

class CaptchaSolver:
    BASE = "https://ocr.captchaai.com"

    def __init__(self, api_key):
        self.api_key = api_key

    def solve(self, params, initial_wait=10):
        params["key"] = self.api_key
        params["json"] = 1
        resp = requests.post(f"{self.BASE}/in.php", data=params).json()
        if resp["status"] != 1:
            raise Exception(resp["request"])
        task_id = resp["request"]
        time.sleep(initial_wait)
        for _ in range(60):
            result = requests.get(
                f"{self.BASE}/res.php",
                params={"key": self.api_key, "action": "get", "id": task_id, "json": 1},
            ).json()
            if result["request"] == "CAPCHA_NOT_READY":
                time.sleep(5)
                continue
            if result["status"] == 1:
                return result["request"]
            raise Exception(result["request"])
        raise TimeoutError("Timed out")

class CheckoutAutomator:
    def __init__(self, api_key):
        self.solver = CaptchaSolver(api_key)
        self.driver = webdriver.Chrome()
        self.wait = WebDriverWait(self.driver, 15)

    def _find(self, selector):
        return self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, selector)))

    def _click(self, selector):
        self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, selector))).click()

    def _fill(self, selector, value):
        el = self._find(selector)
        el.clear()
        el.send_keys(value)

    def _solve_if_captcha(self):
        """Check for and solve any CAPTCHA on the current page."""
        html = self.driver.page_source
        page_url = self.driver.current_url

        # Turnstile
        turnstile = self.driver.find_elements(By.CSS_SELECTOR, ".cf-turnstile")
        if turnstile:
            sitekey = turnstile[0].get_attribute("data-sitekey")
            token = self.solver.solve({
                "method": "turnstile",
                "sitekey": sitekey,
                "pageurl": page_url,
            })
            self.driver.execute_script(
                f'document.querySelector("[name=cf-turnstile-response]").value = "{token}";'
            )
            return "turnstile"

        # reCAPTCHA
        recaptcha = self.driver.find_elements(By.CSS_SELECTOR, "[data-sitekey]")
        if recaptcha and "recaptcha" in html.lower():
            sitekey = recaptcha[0].get_attribute("data-sitekey")

            # Check if v3 (invisible)
            if "grecaptcha.execute" in html:
                token = self.solver.solve({
                    "method": "userrecaptcha",
                    "googlekey": sitekey,
                    "pageurl": page_url,
                    "version": "v3",
                    "action": "checkout",
                }, initial_wait=20)
            else:
                token = self.solver.solve({
                    "method": "userrecaptcha",
                    "googlekey": sitekey,
                    "pageurl": page_url,
                })

            self.driver.execute_script(
                f'document.querySelector("[name=g-recaptcha-response]").value = "{token}";'
            )
            return "recaptcha"

        return None

    # --- Checkout Steps ---

    def add_to_cart(self, product_url, quantity=1):
        """Step 1: Navigate to product and add to cart."""
        self.driver.get(product_url)
        time.sleep(2)

        # Set quantity if field exists
        qty_fields = self.driver.find_elements(By.CSS_SELECTOR, "input[name='quantity']")
        if qty_fields:
            qty_fields[0].clear()
            qty_fields[0].send_keys(str(quantity))

        self._solve_if_captcha()
        self._click("[data-action='add-to-cart'], .add-to-cart, #add-to-cart")
        time.sleep(2)
        return True

    def fill_shipping(self, address):
        """Step 2: Fill shipping address."""
        self._solve_if_captcha()

        field_map = {
            "first_name": "#shipping-first-name, [name='firstName']",
            "last_name": "#shipping-last-name, [name='lastName']",
            "address": "#shipping-address, [name='address1']",
            "city": "#shipping-city, [name='city']",
            "state": "#shipping-state, [name='state']",
            "zip": "#shipping-zip, [name='postalCode']",
            "phone": "#shipping-phone, [name='phone']",
        }

        for field, selectors in field_map.items():
            if field in address:
                for selector in selectors.split(", "):
                    elements = self.driver.find_elements(By.CSS_SELECTOR, selector)
                    if elements:
                        elements[0].clear()
                        elements[0].send_keys(address[field])
                        break

        self._solve_if_captcha()
        self._click("[data-step='shipping-submit'], .continue-to-payment")
        time.sleep(2)
        return True

    def fill_payment(self, payment):
        """Step 3: Fill payment information."""
        self._solve_if_captcha()

        # Handle iframe for card fields (common pattern)
        iframes = self.driver.find_elements(By.CSS_SELECTOR, "iframe[name*='card']")
        if iframes:
            self.driver.switch_to.frame(iframes[0])
            self._fill("input[name='cardnumber']", payment["card_number"])
            self.driver.switch_to.default_content()

            if len(iframes) > 1:
                self.driver.switch_to.frame(iframes[1])
                self._fill("input[name='exp-date']", payment["expiry"])
                self.driver.switch_to.default_content()

            if len(iframes) > 2:
                self.driver.switch_to.frame(iframes[2])
                self._fill("input[name='cvc']", payment["cvv"])
                self.driver.switch_to.default_content()
        else:
            # Direct fields
            self._fill("[name='cardNumber'], #card-number", payment["card_number"])
            self._fill("[name='expiry'], #card-expiry", payment["expiry"])
            self._fill("[name='cvv'], #card-cvv", payment["cvv"])

        self._solve_if_captcha()
        self._click("[data-step='payment-submit'], .continue-to-review")
        time.sleep(2)
        return True

    def confirm_order(self):
        """Step 4: Review and confirm order."""
        self._solve_if_captcha()
        self._click("[data-step='confirm'], .place-order, #place-order")
        time.sleep(3)

        # Check for confirmation
        html = self.driver.page_source.lower()
        if "order confirmed" in html or "thank you" in html or "confirmation" in html:
            return True
        return False

    def run_checkout(self, product_url, address, payment, quantity=1):
        """Run complete checkout flow."""
        steps = [
            ("Add to cart", lambda: self.add_to_cart(product_url, quantity)),
            ("Proceed to checkout", lambda: self._click(".checkout-btn, a[href*='checkout']")),
            ("Fill shipping", lambda: self.fill_shipping(address)),
            ("Fill payment", lambda: self.fill_payment(payment)),
            ("Confirm order", lambda: self.confirm_order()),
        ]

        for step_name, step_fn in steps:
            print(f"Step: {step_name}...")
            try:
                result = step_fn()
                print(f"  ✓ {step_name} complete")
            except Exception as e:
                print(f"  ✗ {step_name} failed: {e}")
                return False
        return True

    def close(self):
        self.driver.quit()

Как _solve_if_captcha() выбирает тип виджета

Метод не гадает, а проверяет DOM в строго заданном порядке. Сначала ищет .cf-turnstile — если селектор найден, дальше идёт только Turnstile-ветка, sitekey берётся из data-sitekey, а токен подставляется в скрытое поле cf-turnstile-response. Если Turnstile-контейнера нет, метод переходит к reCAPTCHA: смотрит на [data-sitekey] и строку recaptcha в HTML-исходнике страницы. Присутствие grecaptcha.execute в коде страницы означает невидимую v3 — тогда в запрос добавляются version и action, а ожидание перед первым опросом увеличивается до 20 секунд, потому что v3 обычно решается дольше v2.

Из этого следует ограничение: метод возвращает управление сразу после первого найденного типа. Если на одном шаге чекаута одновременно висят и Turnstile, и reCAPTCHA (редко, но встречается на страницах с несколькими сторонними виджетами), придётся вызвать _solve_if_captcha() повторно после первого решения — сам метод второй проход не делает.


Как запустить сценарий

Данные ниже — тестовая карта Stripe и вымышленный адрес. Не подставляйте сюда реальные платёжные данные — только sandbox-номера платформы.

automator = CheckoutAutomator("YOUR_API_KEY")

try:
    success = automator.run_checkout(
        product_url="https://store.example.com/product/widget-pro",
        address={
            "first_name": "Jane",
            "last_name": "Smith",
            "address": "123 Test Street",
            "city": "San Francisco",
            "state": "CA",
            "zip": "94102",
            "phone": "415-555-0100",
        },
        payment={
            "card_number": "4111111111111111",
            "expiry": "12/26",
            "cvv": "123",
        },
        quantity=1,
    )
    print(f"Checkout {'succeeded' if success else 'failed'}")
finally:
    automator.close()

Нестандартные ситуации при многошаговом чекауте

CAPTCHA появляется после ошибки валидации

Если сайт откатывает шаг из-за неверного значения, при повторной отправке формы часто всплывает новая CAPTCHA. Оборачивайте шаг в повтор с проверкой:

def retry_step_with_captcha(self, step_fn, max_retries=2):
    for attempt in range(max_retries + 1):
        try:
            return step_fn()
        except Exception:
            if attempt < max_retries:
                self._solve_if_captcha()
                continue
            raise

CAPTCHA, привязанная к сессии

Некоторые сайты показывают CAPTCHA только на первый запрос в сессии. Используйте один экземпляр драйвера с общими cookies на всех шагах — так CAPTCHA всплывёт максимум один раз за прогон.

Если логи сохраняют адрес и данные карты, обращайтесь с ними как с боевыми персональными данными — это касается и 152-ФЗ для РФ, и GDPR-дисциплины у трансграничных команд.

Динамические поля формы

Если поля шага подгружаются через JS уже после рендера, _solve_if_captcha() может сработать раньше, чем появится сам виджет. Дождитесь индикатора загрузки шага:

def wait_for_step(self, indicator_selector, timeout=15):
    """Wait for a step to fully load before proceeding."""
    WebDriverWait(self.driver, timeout).until(
        EC.presence_of_element_located((By.CSS_SELECTOR, indicator_selector))
    )

GeeTest v3 вместо reCAPTCHA на отдельных шагах

Часть площадок ставит GeeTest v3 на шаг входа или на форму доставки вместо reCAPTCHA — CaptchaAI решает и его через тот же in.php/res.php цикл, только с другим набором параметров задачи (challenge, gt, api_server вместо sitekey/googlekey). Если это ваш случай, добавьте отдельную ветку в _solve_if_captcha() по аналогии с веткой Turnstile — общий каркас CaptchaSolver менять не придётся.


Частые проблемы при автоматизации чекаута

Ниже — симптомы, которые чаще всего ловят в логах QA-прогонов, и что за ними обычно стоит.

Проблема Причина Как исправить
Токен CAPTCHA отклонён на оплате Токен истёк Решайте CAPTCHA прямо перед кликом «Отправить»
Поля карты не заполняются Форма в iframe (Stripe/Braintree) Переключитесь в iframe перед вводом
Чекаут кидает на страницу входа Истекла сессия Добавьте шаг входа перед оформлением
Заказ не оформляется без ошибки Не заполнены обязательные поля Проверяйте ошибки после каждого шага
_solve_if_captcha() ничего не находит Виджет ещё не отрендерился Добавьте wait_for_step() перед вызовом решения

Частые вопросы об автоматизации чекаута

Сколько потоков нужно, чтобы тестировать чекаут параллельно?

При 3–5 CAPTCHA за прогон и нескольких браузерах параллельно берите тариф с запасом: ADVANCE ($90/мес, 50 потоков) закрывает 10–15 сессий без очереди. Для разовых прогонов хватит BASIC ($15/мес, 5 потоков).

Можно ли гонять чекаут в песочнице, не тратя реальные карты?

Да. У большинства платформ есть sandbox-режим с тестовыми номерами карт, как в примере выше. CaptchaAI решает CAPTCHA одинаково и в тестовой, и в боевой среде.

Что делать, если CAPTCHA появляется на шаге, где её не было в прошлый раз?

Не привязывайте проверку к конкретному шагу — вызывайте _solve_if_captcha() перед каждым действием, способным вызвать редирект. reCAPTCHA v3 срабатывает по поведенческому скорингу, и порог может сместиться.

Как не столкнуться с истёкшим токеном на длинной форме оплаты?

Решайте CAPTCHA прямо перед отправкой формы — пока пользователь вводит данные карты, ранее полученный токен успевает протухнуть.

Нужна ли отдельная логика, если вместо reCAPTCHA попадается GeeTest v3?

Да, но каркас CaptchaSolver не меняется. Добавьте в _solve_if_captcha() третью ветку с проверкой на GeeTest-виджет и параметрами challenge/gt/api_server вместо sitekey — очередь in.php/res.php и опрос результата остаются теми же.


Похожие материалы


Автоматизация тестирования чекаута — решайте CAPTCHA вместе с CaptchaAI.

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