API Tutorials

Порядок изображений BLS CAPTCHA и обработка ответов сетки

Сетка BLS CAPTCHA не сводится к одному клику: индексы ячеек, порядок и формат ответа отличаются от сайта к сайту, и рабочий solver-код на одном макете легко ломается на другом. Ниже — как сопоставить ячейки с индексами, решить задачу через API CaptchaAI и вернуть ответ в форму в нужном формате.

На практике большинство падений автотестов на BLS-сетках связано не с самим решением, а с рассинхроном между тем, что вернул API, и тем, что ждёт конкретная форма — битовую маску, JSON-массив индексов или клики по DOM.

Прежде чем чинить парсер, откройте DevTools и посмотрите, что именно уходит в request при ручном прохождении сетки — часто формат меняется после редизайна страницы, а не после обновления CaptchaAI.


Какие задачи встречаются в сетке BLS

В интерфейсе BLS всё сводится к трём типам заданий, и от того, какое досталось, зависит код, который будет читать индексы ответа:

  • Порядок изображений — расставить изображения в заданной последовательности: по возрастанию цифр, по алфавиту или по другому правилу из подсказки к заданию.
  • Выбор изображений — отметить изображения, подходящие под описание задачи («выберите все изображения с текстом»).
  • Соответствие образцу — определить, какие изображения совпадают с показанным образцом сверху сетки.

Как сопоставить ячейки сетки с индексами

Сетки BLS обычно приходят в виде 3×3 или 4×4. Дальше всё сводится к переводу «строка/столбец» в плоский индекс и обратно.

Важный нюанс: DOM-порядок ячеек не всегда совпадает с визуальным порядком слева направо и сверху вниз — на части форм BLS сетка собирается через CSS grid с order, и driver.find_elements() вернёт элементы в порядке разметки, а не в порядке отображения. Если клики попадают не туда, первым делом сверьте порядок в HTML, а не пересчитывайте индексы.

# grid_mapping.py

# BLS grids typically use 3x3 or 4x4 layouts
# Each cell maps to an index:

# 3x3 grid:
# [0] [1] [2]
# [3] [4] [5]
# [6] [7] [8]

# 4x4 grid:
#  [0]  [1]  [2]  [3]
#  [4]  [5]  [6]  [7]
#  [8]  [9] [10] [11]
# [12] [13] [14] [15]

def grid_position(index, cols=3):
    """Convert flat index to row, column."""
    return index // cols, index % cols


def index_from_position(row, col, cols=3):
    """Convert row, column to flat index."""
    return row * cols + col


# Example: For a 3x3 grid, position (1, 2) = index 5
print(grid_position(5, cols=3))   # (1, 2)
print(index_from_position(1, 2))  # 5

Отправка сетки BLS на решение через API

Функция отправляет sitekey и pageurl в in.php, затем опрашивает res.php до готового решения. Если у задания есть текстовая инструкция («отметьте все нечётные»), передайте её в параметре instructions — без неё solver будет полагаться только на изображения и может ошибиться в неоднозначных случаях.

# solve_bls_grid.py
import requests
import time
import os
import json


def solve_bls_grid(sitekey, pageurl, instructions=None):
    """Solve a BLS grid CAPTCHA and get response indices."""
    api_key = os.environ["CAPTCHAAI_API_KEY"]

    payload = {
        "key": api_key,
        "method": "bls",
        "sitekey": sitekey,
        "pageurl": pageurl,
        "json": 1,
    }
    if instructions:
        payload["instructions"] = instructions

    resp = requests.post(
        "https://ocr.captchaai.com/in.php",
        data=payload,
        timeout=30,
    )
    result = resp.json()
    if result.get("status") != 1:
        raise RuntimeError(f"Submit failed: {result.get('request')}")

    task_id = result["request"]

    time.sleep(10)
    for _ in range(30):
        resp = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": api_key,
            "action": "get",
            "id": task_id,
            "json": 1,
        }, timeout=15)
        data = resp.json()
        if data.get("status") == 1:
            return data["request"]
        if data["request"] != "CAPCHA_NOT_READY":
            raise RuntimeError(data["request"])
        time.sleep(5)

    raise TimeoutError("BLS grid solve timeout")

Разбор ответа: индексы или битовая маска

CaptchaAI возвращает решение JSON-массивом индексов или строкой через запятую — формат зависит от формы. format_for_submission() конвертирует индексы в битовую маску, если сайт ждёт именно её.

На практике встречаются оба варианта в рамках одного визового портала: форма подачи анкеты ждёт индексы, а форма записи на приём — битовую маску той же длины, что и число ячеек сетки. Отсюда и параметр grid_size в format_for_submission() — без него маска для 4×4-сетки получится короче, чем нужно.

# parse_response.py
import json


def parse_grid_response(solution):
    """Parse CaptchaAI BLS response into actionable grid data."""
    # Solution may be JSON or comma-separated indices
    if isinstance(solution, str):
        try:
            parsed = json.loads(solution)
            return parsed
        except json.JSONDecodeError:
            pass

        # Try comma-separated indices
        if "," in solution:
            return [int(x.strip()) for x in solution.split(",")]

        # Single value
        return [solution]

    return solution


def format_for_submission(indices, grid_size=9):
    """Format indices for form submission."""
    # Some sites expect a bitmask
    bitmask = ["0"] * grid_size
    for idx in indices:
        if isinstance(idx, int) and 0 <= idx < grid_size:
            bitmask[idx] = "1"

    return {
        "indices": indices,
        "bitmask": "".join(bitmask),
        "count": len(indices),
    }

Передача решения в браузере через Selenium

Решение нужно вернуть на страницу — кликами по ячейкам или записью в скрытое поле; для задач с порядком нужна пауза побольше между кликами.

click_grid_cells() подходит для сеток выбора, где порядок кликов не важен; set_order_sequence() — для заданий с последовательностью, где сайт проверяет не только набор ячеек, но и очередность нажатий. Если после нескольких кликов форма всё равно ругается на порядок, увеличьте паузу в set_order_sequence() до 700–800 мс — часть форм BLS не успевает обработать события быстрее.

# inject_grid.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time


def click_grid_cells(driver, indices):
    """Click specific grid cells based on solution indices."""
    wait = WebDriverWait(driver, 10)

    # Find all grid cells
    cells = wait.until(
        EC.presence_of_all_elements_located(
            (By.CSS_SELECTOR, ".captcha-grid .cell, .bls-grid img, .grid-item")
        )
    )

    for idx in indices:
        if isinstance(idx, int) and idx < len(cells):
            cells[idx].click()
            time.sleep(0.3)  # Brief delay between clicks


def set_order_sequence(driver, ordered_indices):
    """Click grid cells in the correct order for ordering challenges."""
    wait = WebDriverWait(driver, 10)

    cells = wait.until(
        EC.presence_of_all_elements_located(
            (By.CSS_SELECTOR, ".captcha-grid .cell, .bls-grid img")
        )
    )

    for idx in ordered_indices:
        if isinstance(idx, int) and idx < len(cells):
            cells[idx].click()
            time.sleep(0.5)  # Ordering needs pauses between clicks


def inject_hidden_response(driver, solution_value):
    """Set the solution in a hidden input field."""
    driver.execute_script("""
        var inputs = document.querySelectorAll(
            'input[name*="captcha"], input[name*="response"], #captcha-answer'
        );
        for (var i = 0; i < inputs.length; i++) {
            inputs[i].value = arguments[0];
        }
    """, str(solution_value))

Полный цикл обработки сетки BLS

Собираем шаги в одну функцию: дождаться CAPTCHA, решить сетку через CaptchaAI и выбрать способ ответа по тому, что есть в DOM.

# full_flow.py
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


def handle_bls_grid(driver, pageurl):
    """Complete BLS grid CAPTCHA handling."""

    wait = WebDriverWait(driver, 15)

    # Wait for CAPTCHA to load
    captcha = wait.until(
        EC.presence_of_element_located(
            (By.CSS_SELECTOR, "[data-sitekey], .bls-captcha")
        )
    )
    sitekey = captcha.get_attribute("data-sitekey")

    # Get instructions
    instructions = None
    try:
        inst = driver.find_element(By.CSS_SELECTOR, ".captcha-instructions")
        instructions = inst.text.strip()
    except Exception:
        pass

    # Solve via CaptchaAI
    solution = solve_bls_grid(sitekey, pageurl, instructions)
    parsed = parse_grid_response(solution)

    # Determine response method
    grid_cells = driver.find_elements(
        By.CSS_SELECTOR, ".captcha-grid .cell, .bls-grid img"
    )

    if grid_cells:
        # Click-based response
        if isinstance(parsed, list) and all(isinstance(x, int) for x in parsed):
            click_grid_cells(driver, parsed)
        else:
            inject_hidden_response(driver, solution)
    else:
        # Hidden input response
        inject_hidden_response(driver, solution)

    # Submit
    submit = driver.find_element(
        By.CSS_SELECTOR, "button[type='submit'], .submit-btn, #verify"
    )
    submit.click()

    return True

handle_bls_grid() сама решает, кликать по ячейкам или писать в скрытое поле — по наличию .captcha-grid .cell / .bls-grid img в DOM. Для форм с нестандартной вёрсткой добавьте свой CSS-селектор в оба списка (grid_cells и внутри click_grid_cells() / set_order_sequence()), иначе функция молча уйдёт в ветку inject_hidden_response().


Где это пригождается на практике

Такие сетки часто встречаются на порталах бронирования и подачи документов, где визовые сервисы вроде BLS используют их вместо обычной капчи. При автотестах формы заявки на staging стабильный парсер спасает от ситуации «тест упал из-за формата ответа».

Типичный порядок внедрения в существующий тестовый pipeline:

  1. Добавить solve_bls_grid() в шаг ожидания перед сабмитом формы, а не в отдельный тестовый сценарий.
  2. Логировать сырой ответ CaptchaAI (solution до парсинга) на первых прогонах — это ускоряет разбор, если формат вдруг изменится.
  3. Прогнать сценарий на staging минимум 10 раз подряд перед переносом в CI — сетки BLS не идентичны на каждой загрузке страницы.

Для точечных прогонов хватает BASIC ($15/мес, 5 потоков); для параллельных в CI берите тариф повыше, от STANDARD ($30/мес, 15 потоков).


Типичные ошибки при обработке сетки BLS

Проблема Причина Как исправить
Нажимает не на те ячейки Несоответствие выбора ячейки сетки Проверьте HTML-код сетки и обновите селекторы CSS.
Заказ отклонен Щелкаем слишком быстро Добавьте задержки в 300–500 мс между нажатиями.
Несоответствие формата решения Сайт ожидает битовую маску, индексы получены. Используйте format_for_submission() для конвертации
Сетка загружена не полностью Изображения загружаются медленно Прежде чем решать, дождитесь загрузки всех изображений сетки.

Если ни одна строка таблицы не объясняет сбой, сравните sitekey и pageurl, которые ушли в solve_bls_grid(), с тем, что видно в DevTools — почти всегда причина не в парсере, а в неверном sitekey из-за динамической подгрузки формы.


Часто задаваемые вопросы

Сколько времени занимает решение сетки BLS через CaptchaAI?

Обычно меньше секунды. Пауза перед первым опросом res.php — защита от лишних запросов, не типичное ожидание.

Что делать, если после отправки ответа появляется вторая сетка?

На части форм BLS после первой сетки подгружается вторая. Проверяйте новый .bls-captcha и при необходимости запускайте handle_bls_grid() заново.

Какой тариф CaptchaAI подойдёт для тестирования сеток BLS?

Для точечных QA-прогонов хватает BASIC ($15/мес, 5 потоков), для CI — ADVANCE ($90/мес, 50 потоков) или крупнее.

Можно ли переиспользовать решение сетки BLS?

Нет. Решение привязано к конкретной сессии — решайте каждую сетку заново.

Почему индексы из ответа не совпадают с тем, что кликается на экране?

Скорее всего, сайт отдаёт координаты в своей системе (например, построчно слева направо), а driver.find_elements() возвращает ячейки в порядке из DOM. Сверьте это через grid_position() / index_from_position() из первого раздела — обычно расхождение лечится сменой cols в вызове.


Связанные руководства


Решайте сетки BLS без гадания с форматом ответа — начните с CaptchaAI.

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