Kак обойти капчу Сloudflare Turnsile — или обход Cloudflare разной степени сложности

В рамках научного и исследовательского интереса решил поэкспериментировать с распознаванием сложных типов капчи, ну как поэкспериментировать — скорее проверить работоспособность и умение электронного коллеги писать за меня код. Да, там было куча — соблюдайте этические нормы, бла бла бла… Но простое — чувак, я это делаю исключительно в рамках исследования — все решили.

e9c895c44e2c4ed409cff042592207db.jpg

Я пошел в сторону Cloudflare Turnstile капчи, так как до этого не сталкивался конкретно с этим типом капчи и предлагаю двигаться поступательно, сперва расскажем что за Turnstile такая, кто еще не в теме:

Что такое Turnsile CAPTCHA и почему обход Cloudflare Turnstile может быть занозой в одном месте?

Turnstile — это CAPTCHA-решение, разработанное компанией Cloudflare, предназначенное для защиты веб-сайтов от автоматизированного доступа (ботов) без создания лишних препятствий для пользователей. Основная идея заключается в том, чтобы обеспечить высокую степень защиты при минимальном вмешательстве в пользовательский опыт. В ряде случаев пользователю даже не требуется выполнять дополнительные действия — проверка может проходить в фоновом режиме. 

Но не в моем случае, так как оба типа Turnstile капчи, которые я обходил, имели вполне осязаемый вид.

У Turnstile CAPTCHA есть более простой вариант — напоминающий reCAPTCHA и более сложный вариант:

 Challenge CAPTCHA — это усовершенствованная версия CAPTCHA, которая применяется в ситуациях, когда базовые проверки не дают окончательного результата о том, является ли посетитель человеком. Такая система интегрирует дополнительные этапы верификации, чтобы повысить уровень защиты, не прибегая сразу к сложным задачам для всех пользователей. 

Для простоты понимания на базовом уровне скажу так — чтобы решить простую капчу все необходимое для ее решения можно найти в html коде страницы, а именно sitekey (открываем страницу в режиме разработчика и с помощью клавиш ctrl + F ищем sitekey на странице). А вот со вторым вариантом такой способ не прокатит, тут все необходимые параметры генерятся в JS и просто в коде страницы их не раскопать, нужно перехватывать данные (а это уже что то более сложное).

Я пошел простым путем, у меня было два URL, на одном из которых стоит простая Turnstile CAPTCHA, а на втором сложная.

https://privacy.deepsync.com/ — тут простая
https://crash.chicagopolice.org/ — тут сложная

Простая Turnstile CAPTCHA или обход Cloudflare на Python без мам, пап и бабушкиных советов 

Первым делом разберемся с простой капчей — я набрал в поисковике — решить Turnstile CAPTCHA и снова наткнулся на популярный сервис распознавания капчи, в АПИ было все подробно расписано, но есть нюанс — не хотелось писать ничего руками и задача была делегирована нейросетевому коллеге, который и сверстал методом проб и ошибок вот такое решение

import argparse
import requests
import time


from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait, Select
from selenium.webdriver.support import expected_conditions as EC


def get_turnstile_solution(api_key, site_key, page_url):
    """
    Отправляет задачу на решение Turnstile CAPTCHA через 2captcha и опрашивает результат.
    Возвращает токен решения (str) или None, если что-то пошло не так.
    """
    in_url = 'http://2captcha.com/in.php'
    payload = {
        'key': api_key,
        'method': 'turnstile',
        'sitekey': site_key,
        'pageurl': page_url,
        'json': 1
    }
   
    try:
        response = requests.post(in_url, data=payload)
        result = response.json()
    except Exception as e:
        print("Ошибка при отправке запроса к 2captcha:", e)
        return None


    if result.get('status') != 1:
        print("Ошибка отправки задачи:", result.get('request'))
        return None


    captcha_id = result.get('request')
    print("Задача на решение CAPTCHA отправлена, ID:", captcha_id)


    # Опрос результата на 2captcha каждые 5 секунд
    res_url = 'http://2captcha.com/res.php'
    params = {
        'key': api_key,
        'action': 'get',
        'id': captcha_id,
        'json': 1
    }


    while True:
        time.sleep(5)
        try:
            res_response = requests.get(res_url, params=params)
            res_result = res_response.json()
        except Exception as e:
            print("Ошибка при получении результата:", e)
            return None


        if res_result.get('status') == 1:
            solution_token = res_result.get('request')
            print("Решение CAPTCHA получено:", solution_token)
            return solution_token
        elif res_result.get('request') == "CAPCHA_NOT_READY":
            print("Решение ещё не готово, повторная попытка...")
            continue
        else:
            print("Ошибка получения решения:", res_result.get('request'))
            return None


def main():
    parser = argparse.ArgumentParser(
        description='Демонстрация автозаполнения формы и решения Turnstile CAPTCHA через 2captcha.'
    )
    parser.add_argument('api_key', type=str, nargs='?', help='Ваш API ключ от 2captcha')
    parser.add_argument('url', type=str, nargs='?', help='URL страницы с формой и Turnstile CAPTCHA')
    args = parser.parse_args()


    if not args.api_key:
        args.api_key = input("Введите ваш API ключ от 2captcha: ")
    if not args.url:
        args.url = input("Введите URL страницы с CAPTCHA: ")


    # 1) Запускаем Selenium в визуальном режиме (без headless),
    #    чтобы вы могли наблюдать процесс
    chrome_options = Options()
    driver = webdriver.Chrome(options=chrome_options)
    driver.get(args.url)


    wait = WebDriverWait(driver, 30)


    try:
        # 2) Ожидаем появления элемента с классом .cf-turnstile
        turnstile_div = wait.until(
            EC.presence_of_element_located((By.CSS_SELECTOR, ".cf-turnstile"))
        )
    except Exception as e:
        print("Ошибка: не найден элемент с классом .cf-turnstile:", e)
        driver.quit()
        return


    # Извлекаем sitekey из атрибута data-sitekey
    site_key = turnstile_div.get_attribute("data-sitekey")
    print("Найден sitekey:", site_key)


    # ----- Шаг 1: Автоматически заполняем поля формы -----
    try:
        # Пример: выбираем "A deceased individual"
        select_request_type = Select(driver.find_element(By.ID, "request_type"))
        select_request_type.select_by_value("deceased")  
       
        # Имя, фамилия
        driver.find_element(By.ID, "first_name").send_keys("John")
        driver.find_element(By.ID, "last_name").send_keys("Doe")
       
        # Email, телефон
        driver.find_element(By.ID, "email").send_keys("test@example.com")
        driver.find_element(By.ID, "phone").send_keys("1234567890")
       
        # Адрес
        driver.find_element(By.ID, "who_address").send_keys("123 Test Street")
        driver.find_element(By.ID, "who_address2").send_keys("Apt 4")
        driver.find_element(By.ID, "who_city").send_keys("Test City")
       
        select_state = Select(driver.find_element(By.ID, "who_state"))
        select_state.select_by_value("CA")  # California
       
        driver.find_element(By.ID, "who_zip").send_keys("90001")
       
        # Проставляем чекбоксы "Requests"
        driver.find_element(By.ID, "request_type_1").click()  # Do not sell/share my personal information
        driver.find_element(By.ID, "request_type_2").click()  # Do not use my personal data for targeted advertising
        # ... можно отметить остальные при необходимости
       
        print("Поля формы заполнены тестовыми данными.")
    except Exception as e:
        print("Ошибка при автозаполнении полей формы:", e)
        driver.quit()
        return


    # ----- Шаг 2: Решаем CAPTCHA через 2captcha -----
    token = get_turnstile_solution(args.api_key, site_key, args.url)
    if not token:
        print("Не удалось получить решение CAPTCHA.")
        driver.quit()
        return


    # ----- Шаг 3: Подстановка решения в скрытое поле и вызов callback -----
    try:
        # Ищем скрытое поле, которое Turnstile использует для хранения ответа
        input_field = wait.until(
            EC.presence_of_element_located(
                (By.CSS_SELECTOR, 'input#cf-chl-widget-yi26c_response, input[name="cf-turnstile-response"]')
            )
        )
        # Вставляем полученный токен
        driver.execute_script("arguments[0].value = arguments[1];", input_field, token)
        print("Токен решения CAPTCHA подставлен в скрытое поле.")


        # Генерируем событие 'change'
        driver.execute_script("""
            var event = new Event('change', { bubbles: true });
            arguments[0].dispatchEvent(event);
        """, input_field)


        # Если сайт использует callback-функцию для Turnstile, попробуем вызвать
        driver.execute_script("""
            if (window.tsCallback) {
                window.tsCallback(arguments[0]);
            }
        """, token)
        print("Callback вызван (если он был определён).")


    except Exception as e:
        print("Ошибка при подстановке токена CAPTCHA:", e)
        driver.quit()
        return


    # ----- Шаг 4: Отправляем форму, чтобы перейти на следующий этап -----
    try:
        submit_button = wait.until(
            EC.element_to_be_clickable((By.ID, "submit_button"))
        )
        submit_button.click()
        print("Клик по кнопке 'Submit' выполнен.")


        # Ожидаем, что URL изменится после отправки
        wait.until(EC.url_changes(args.url))
        print("Переход на следующий этап зафиксирован. Текущий URL:", driver.current_url)


    except Exception as e:
        print("Не удалось нажать 'Submit' или дождаться следующего этапа:", e)


    print("Автоматизация завершена. Окно браузера остаётся открытым для проверки.")
    input("Нажмите Enter, чтобы закрыть браузер...")
    driver.quit()


if __name__ == '__main__':
    main()

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

Для скрипта нужно установить Селениум, библиотеку requests, ставится это все простой командой в консоли

pip install selenium requests

Есть нюанс — данный код адаптирован под конкретный сайт (указан выше), и он не только обходит капчу, но и вводит автоматом данные на сайте.

Как работает скрипт для обхода Turnstile Cloudflare — подробный разбор


С помощью argparse скрипт принимает API-ключ для 2captcha и URL страницы, на которой находится CAPTCHA. Скрипт просит ввести их руками в консоли (ничего сложного)

Далее открывается браузер (я использовал не headless режим, чтобы записать видео, как все работает) и с использованием WebDriverWait скрипт ждёт появления на странице элемента с классом .cf-turnstile, который отвечает за отображение CAPTCHA.

Из найденного элемента извлекается атрибут data-sitekey — уникальный ключ, необходимый для взаимодействия с CAPTCHA.

Параллельно происходит заполнение полей (это нам мало интересно, было реализовано, чтобы скрипт отработал до конца).

После получения нужного параметра, он отправляется на сервер 2капчи где капча решается и решение (токкен) направляется обратно скрипту, чтобы тот его подставил.

Скрипт ищет на странице скрытое поле, в которое должен быть подставлен токен (используя CSS-селекторы, ориентированные на поля с именем cf-turnstile-response или с определённым ID).

С помощью execute_script токен вставляется в найденное поле, после чего создаётся и диспатчится событие change, что позволяет странице отреагировать на подстановку решения.

Если на странице определена функция обратного вызова (например, window.tsCallback), она вызывается, чтобы уведомить скрипт страницы о том, что CAPTCHA решена.

Как только кнопка отправки капчи становится кликабельной, скрипт ее жмет. В целом все, ниже видео как это все работает в реальности

Дабы был выбор, и не чтобы не топить постоянно за 2капчу, я изменил скрипт под SolveCaptcha — благо метод решения особо не отличается, вот аналогичный скрипт, но в качестве стороннего сервиса тут уже выступает SolveCaptcha.

import argparse
import requests
import time


from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait, Select
from selenium.webdriver.support import expected_conditions as EC


def get_turnstile_solution(api_key, site_key, page_url):
    """
    Отправляет задачу на решение Turnstile CAPTCHA через solvecaptcha и опрашивает результат.
    Возвращает токен решения (str) или None, если что-то пошло не так.
    """
    # URL для отправки задачи в solvecaptcha
    in_url = 'https://api.solvecaptcha.com/in.php'
    payload = {
        'key': api_key,
        'method': 'turnstile',
        'sitekey': site_key,
        'pageurl': page_url,
        'json': 1
    }
   
    try:
        response = requests.post(in_url, data=payload)
        result = response.json()
    except Exception as e:
        print("Ошибка при отправке запроса к solvecaptcha:", e)
        return None


    if result.get('status') != 1:
        print("Ошибка отправки задачи:", result.get('request'))
        return None


    captcha_id = result.get('request')
    print("Задача на решение CAPTCHA отправлена, ID:", captcha_id)


    # Опрос результата через solvecaptcha каждые 5 секунд
    res_url = 'https://api.solvecaptcha.com/res.php'
    params = {
        'key': api_key,
        'action': 'get',
        'id': captcha_id,
        'json': 1
    }


    while True:
        time.sleep(5)
        try:
            res_response = requests.get(res_url, params=params)
            res_result = res_response.json()
        except Exception as e:
            print("Ошибка при получении результата:", e)
            return None


        if res_result.get('status') == 1:
            solution_token = res_result.get('request')
            print("Решение CAPTCHA получено:", solution_token)
            return solution_token
        elif res_result.get('request') == "CAPCHA_NOT_READY":
            print("Решение ещё не готово, повторная попытка...")
            continue
        else:
            print("Ошибка получения решения:", res_result.get('request'))
            return None


def main():
    parser = argparse.ArgumentParser(
        description='Демонстрация автозаполнения формы и решения Turnstile CAPTCHA через solvecaptcha.'
    )
    parser.add_argument('api_key', type=str, nargs='?', help='Ваш API ключ от solvecaptcha')
    parser.add_argument('url', type=str, nargs='?', help='URL страницы с формой и Turnstile CAPTCHA')
    args = parser.parse_args()


    if not args.api_key:
        args.api_key = input("Введите ваш API ключ от solvecaptcha: ")
    if not args.url:
        args.url = input("Введите URL страницы с CAPTCHA: ")


    # 1) Запускаем Selenium в визуальном режиме (без headless),
    #    чтобы вы могли наблюдать процесс
    chrome_options = Options()
    driver = webdriver.Chrome(options=chrome_options)
    driver.get(args.url)


    wait = WebDriverWait(driver, 30)


    try:
        # 2) Ожидаем появления элемента с классом .cf-turnstile
        turnstile_div = wait.until(
            EC.presence_of_element_located((By.CSS_SELECTOR, ".cf-turnstile"))
        )
    except Exception as e:
        print("Ошибка: не найден элемент с классом .cf-turnstile:", e)
        driver.quit()
        return


    # Извлекаем sitekey из атрибута data-sitekey
    site_key = turnstile_div.get_attribute("data-sitekey")
    print("Найден sitekey:", site_key)


    # ----- Шаг 1: Автоматически заполняем поля формы -----
    try:
        # Пример: выбираем "A deceased individual"
        select_request_type = Select(driver.find_element(By.ID, "request_type"))
        select_request_type.select_by_value("deceased")  
       
        # Имя, фамилия
        driver.find_element(By.ID, "first_name").send_keys("John")
        driver.find_element(By.ID, "last_name").send_keys("Doe")
       
        # Email, телефон
        driver.find_element(By.ID, "email").send_keys("test@example.com")
        driver.find_element(By.ID, "phone").send_keys("1234567890")
       
        # Адрес
        driver.find_element(By.ID, "who_address").send_keys("123 Test Street")
        driver.find_element(By.ID, "who_address2").send_keys("Apt 4")
        driver.find_element(By.ID, "who_city").send_keys("Test City")
       
        select_state = Select(driver.find_element(By.ID, "who_state"))
        select_state.select_by_value("CA")  # California
       
        driver.find_element(By.ID, "who_zip").send_keys("90001")
       
        # Проставляем чекбоксы "Requests"
        driver.find_element(By.ID, "request_type_1").click()  # Do not sell/share my personal information
        driver.find_element(By.ID, "request_type_2").click()  # Do not use my personal data for targeted advertising
        # ... можно отметить остальные при необходимости
       
        print("Поля формы заполнены тестовыми данными.")
    except Exception as e:
        print("Ошибка при автозаполнении полей формы:", e)
        driver.quit()
        return


    # ----- Шаг 2: Решаем CAPTCHA через solvecaptcha -----
    token = get_turnstile_solution(args.api_key, site_key, args.url)
    if not token:
        print("Не удалось получить решение CAPTCHA.")
        driver.quit()
        return


    # ----- Шаг 3: Подстановка решения в скрытое поле и вызов callback -----
    try:
        # Ищем скрытое поле, которое Turnstile использует для хранения ответа
        input_field = wait.until(
            EC.presence_of_element_located(
                (By.CSS_SELECTOR, 'input#cf-chl-widget-yi26c_response, input[name="cf-turnstile-response"]')
            )
        )
        # Вставляем полученный токен
        driver.execute_script("arguments[0].value = arguments[1];", input_field, token)
        print("Токен решения CAPTCHA подставлен в скрытое поле.")


        # Генерируем событие 'change'
        driver.execute_script("""
            var event = new Event('change', { bubbles: true });
            arguments[0].dispatchEvent(event);
        """, input_field)


        # Если сайт использует callback-функцию для Turnstile, попробуем вызвать
        driver.execute_script("""
            if (window.tsCallback) {
                window.tsCallback(arguments[0]);
            }
        """, token)
        print("Callback вызван (если он был определён).")


    except Exception as e:
        print("Ошибка при подстановке токена CAPTCHA:", e)
        driver.quit()
        return


    # ----- Шаг 4: Отправляем форму, чтобы перейти на следующий этап -----
    try:
        submit_button = wait.until(
            EC.element_to_be_clickable((By.ID, "submit_button"))
        )
        submit_button.click()
        print("Клик по кнопке 'Submit' выполнен.")


        # Ожидаем, что URL изменится после отправки
        wait.until(EC.url_changes(args.url))
        print("Переход на следующий этап зафиксирован. Текущий URL:", driver.current_url)


    except Exception as e:
        print("Не удалось нажать 'Submit' или дождаться следующего этапа:", e)


    print("Автоматизация завершена. Окно браузера остаётся открытым для проверки.")
    input("Нажмите Enter, чтобы закрыть браузер...")
    driver.quit()


if __name__ == '__main__':
    main()

Принцип действия абсолютно такой же — только АПИ ключ нужно заменить на АПИ SolveCaptcha.

Сложный тип Turnstile CAPTCHA — когда обход Cloudflare становится по настоящему занозой и при чем тут node.js?

Со вторым видом Turnstile CAPTCHA конечно пришлось прям повозиться, так как мой железный коллега никак не мог выкатить рабочее решение на Python, постоянно что то ломалось, не перехватывалось и чего то не хватало.

Пришлось действовать по старинке, и идти снова искать информацию в интернете.

f151d06e5e6da90e8ef6ab9d588f1795.png

Нагуглил вот такой репо — https://github.com/2captcha/cloudflare-demo

Что ты говоришь мальчишка? Да, елки-палки, снова тот же сервис -, но я же не виноват, что они в этой теме из каждого утюга!

Репозиторий то я нагуглил, и он даже оказался рабочий — там надо поменять в этой строке
    page.goto ('https://2captcha.com/demo/cloudflare-turnstile-challenge')
УРЛ, подставить свой ключ АПИ и все сработает, но проблемка в том, что я хотел решение на Python, а это решение на node.js

Ситуация

dadf20ca1c14de7abc01e972ccc5241c.jpg

Я загрузил все файлы в бездонную базу своего железнодумающего коллеге и попросил скопировать решение и преобразовать его на Python.

Сперва мы пошли простым путем — через Пупитр (как говориться — «Это жидкий стул и мы не будем его показывать»), решение не сработало… Ни с первого, ни даже с 5 раза, все без толку.

Тогда мы поменяли подход и модернизировали скрипт под работу с Playwright, и вот тут уже машинка завелась, да не с первого раза, но завелась.

Вот скрипт

import asyncio
import json
import re
import os
import time
import requests
from playwright.async_api import async_playwright


# Прописываем API-ключ прямо в файле
API_KEY = "Ваш ключ АПИ"


async def normalize_user_agent(playwright):
    """
    Получает user agent через временный headless-браузер и нормализует его.
    """
    browser = await playwright.chromium.launch(headless=True)
    context = await browser.new_context()
    page = await context.new_page()
    user_agent = await page.evaluate("() => navigator.userAgent")
    normalized = re.sub(r'Headless', '', user_agent)
    normalized = re.sub(r'Chromium', 'Chrome', normalized)
    await browser.close()
    return normalized.strip()


def solve_turnstile(params, api_key):
    """
    Отправляет параметры CAPTCHA в 2Captcha и опрашивает результат до решения.
    """
    base_url = 'http://2captcha.com'
    in_params = {
        'key': api_key,
        'method': 'turnstile',
        'sitekey': params.get('sitekey'),
        'pageurl': params.get('pageurl'),
        'data': params.get('data'),
        'action': params.get('action'),
        'userAgent': params.get('userAgent'),
        'json': 1
    }
    if 'pagedata' in params:
        in_params['pagedata'] = params.get('pagedata')
   
    print("Отправка CAPTCHA на решение...")
    r = requests.get(f"{base_url}/in.php", params=in_params)
    res_json = r.json()
    if res_json.get('status') != 1:
        raise Exception("Ошибка отправки CAPTCHA: " + res_json.get('request'))
    captcha_id = res_json.get('request')
    print(f"Задача отправлена, ID: {captcha_id}")
   
    # Начальная задержка перед поллингом
    time.sleep(10)
    while True:
        r = requests.get(f"{base_url}/res.php", params={
            'key': api_key,
            'action': 'get',
            'id': captcha_id,
            'json': 1
        })
        res_json = r.json()
        if res_json.get('status') == 1:
            token = res_json.get('request')
            print(f"CAPTCHA решена, токен: {token}")
            return token
        elif res_json.get('request') == 'CAPCHA_NOT_READY':
            print("Решение не готово, ожидаем 5 секунд...")
            time.sleep(5)
        else:
            raise Exception("Ошибка при решении CAPTCHA: " + res_json.get('request'))


async def handle_console(msg, page, captcha_future):
    """
    Обработчик консольных сообщений. При получении строки с префиксом "intercepted-params:"
    парсит JSON, отправляет параметры на 2Captcha и возвращает полученный токен.
    """
    text = msg.text
    if "intercepted-params:" in text:
        json_str = text.split("intercepted-params:", 1)[1]
        try:
            params = json.loads(json_str)
        except json.JSONDecodeError:
            print("Ошибка парсинга JSON из intercepted-params")
            return
        print("Перехваченные параметры:", params)
        api_key = API_KEY
        if not api_key:
            print("Переменная окружения APIKEY не задана.")
            await page.context.browser.close()
            return
        try:
            token = solve_turnstile(params, api_key)
            # Передаём токен обратно на страницу через вызов callback
            await page.evaluate("""(token) => {
                window.cfCallback(token);
            }""", token)
            if not captcha_future.done():
                captcha_future.set_result(token)
        except Exception as e:
            print("Ошибка при решении CAPTCHA:", e)
            if not captcha_future.done():
                captcha_future.set_exception(e)


async def main():
    async with async_playwright() as p:
        # Получаем нормализованный user agent
        user_agent = await normalize_user_agent(p)
        print("Нормализованный user agent:", user_agent)
       
        # Запускаем браузер с видимым окном
        browser = await p.chromium.launch(headless=False)
        context = await browser.new_context(user_agent=user_agent)
       
        # Читаем содержимое файла inject.js
        with open("inject.js", "r", encoding="utf-8") as f:
            inject_script = f.read()
        # Добавляем скрипт для инъекции в каждую страницу
        await context.add_init_script(script=inject_script)
       
        page = await context.new_page()
       
        # Создаём future для ожидания результата решения CAPTCHA
        captcha_future = asyncio.Future()
       
        # Регистрируем обработчик консольных сообщений
        page.on("console", lambda msg: asyncio.create_task(handle_console(msg, page, captcha_future)))
       
        # Переходим на целевую страницу
        await page.goto("https://crash.chicagopolice.org/")
       
        try:
            token = await asyncio.wait_for(captcha_future, timeout=120)
            print("Получен CAPTCHA токен:", token)
        except asyncio.TimeoutError:
            print("Время ожидания CAPTCHA истекло")
        await browser.close()


if __name__ == "__main__":
    asyncio.run(main())

Скрипт работает в паре с этой инъекцией (собсвенно на английском именно так она и называется, да и функции у нее такие же)

console.log('inject.js загружен');
console.clear = () => console.log('Console was cleared');
const i = setInterval(() => {
    if (window.turnstile) {
        console.log('Обнаружен window.turnstile');
        clearInterval(i);
        const originalRender = window.turnstile.render;
        window.turnstile.render = (a, b) => {
            console.log('Переопределяем window.turnstile.render');
            let params = {
                sitekey: b.sitekey,
                pageurl: window.location.href,
                data: b.cData,
                pagedata: b.chlPageData,
                action: b.action,
                userAgent: navigator.userAgent,
                json: 1
            };
            console.log('intercepted-params:' + JSON.stringify(params));
            window.cfCallback = b.callback;
            // Если требуется вызвать оригинальную функцию, раскомментируйте следующую строку:
            // return originalRender ? originalRender(a, b) : null;
            return;
        };
    }
}, 50);

Эта инъекция внедряется в каждую страницу через метод context.add_init_script. Его назначение — перехват вызова метода window.turnstile.render, который изначально отвечает за отображение и обработку CAPTCHA.

Скрипт каждые 50 мс проверяет, появился ли объект window.turnstile на странице.

Далее формирует объект params, содержащий:

  • sitekey: ключ сайта, передаваемый через параметр b.

  • pageurl: текущий URL страницы.

  • data: дополнительные данные (из b.cData).

  • pagedata: данные страницы (из b.chlPageData).

  • action: действие, которое должно быть выполнено.

  • userAgent: строка User Agent браузера.

  • json: флаг запроса в формате JSON.

Далее эти параметры при помощи нашего основного скрипта отправляются на сервис распознавания капчи, и происходит та же самая магия, что и в Простой капче — все решается, передается обратно и подставляется.

Точно также, я изменил в скрипте посредника на SolveCaptcha — вот скрипт с другим сервисом

import asyncio
import json
import re
import os
import time
import requests
from playwright.async_api import async_playwright


# Прописываем API-ключ прямо в файле
API_KEY = "Ваш АПИ ключ"


async def normalize_user_agent(playwright):
    """
    Получает user agent через временный headless-браузер и нормализует его.
    """
    browser = await playwright.chromium.launch(headless=True)
    context = await browser.new_context()
    page = await context.new_page()
    user_agent = await page.evaluate("() => navigator.userAgent")
    normalized = re.sub(r'Headless', '', user_agent)
    normalized = re.sub(r'Chromium', 'Chrome', normalized)
    await browser.close()
    return normalized.strip()


def solve_turnstile(params, api_key):
    """
    Отправляет параметры CAPTCHA в solvecaptcha и опрашивает результат до решения.
    """
    base_url = 'https://api.solvecaptcha.com'
    in_params = {
        'key': api_key,
        'method': 'turnstile',
        'sitekey': params.get('sitekey'),
        'pageurl': params.get('pageurl'),
        'data': params.get('data'),
        'action': params.get('action'),
        'userAgent': params.get('userAgent'),
        'json': 1
    }
    if 'pagedata' in params:
        in_params['pagedata'] = params.get('pagedata')
   
    print("Отправка CAPTCHA на решение через solvecaptcha...")
    r = requests.get(f"{base_url}/in.php", params=in_params)
    res_json = r.json()
    if res_json.get('status') != 1:
        raise Exception("Ошибка отправки CAPTCHA: " + res_json.get('request'))
    captcha_id = res_json.get('request')
    print(f"Задача отправлена, ID: {captcha_id}")
   
    # Начальная задержка перед поллингом
    time.sleep(10)
    while True:
        r = requests.get(f"{base_url}/res.php", params={
            'key': api_key,
            'action': 'get',
            'id': captcha_id,
            'json': 1
        })
        res_json = r.json()
        if res_json.get('status') == 1:
            token = res_json.get('request')
            print(f"CAPTCHA решена, токен: {token}")
            return token
        elif res_json.get('request') == 'CAPCHA_NOT_READY':
            print("Решение не готово, ожидаем 5 секунд...")
            time.sleep(5)
        else:
            raise Exception("Ошибка при решении CAPTCHA: " + res_json.get('request'))


async def handle_console(msg, page, captcha_future):
    """
    Обработчик консольных сообщений. При получении строки с префиксом "intercepted-params:"
    парсит JSON, отправляет параметры на solvecaptcha и возвращает полученный токен.
    """
    text = msg.text
    if "intercepted-params:" in text:
        json_str = text.split("intercepted-params:", 1)[1]
        try:
            params = json.loads(json_str)
        except json.JSONDecodeError:
            print("Ошибка парсинга JSON из intercepted-params")
            return
        print("Перехваченные параметры:", params)
        api_key = API_KEY
        if not api_key:
            print("Переменная окружения APIKEY не задана.")
            await page.context.browser.close()
            return
        try:
            token = solve_turnstile(params, api_key)
            # Передаём токен обратно на страницу через вызов callback
            await page.evaluate("""(token) => {
                window.cfCallback(token);
            }""", token)
            if not captcha_future.done():
                captcha_future.set_result(token)
        except Exception as e:
            print("Ошибка при решении CAPTCHA:", e)
            if not captcha_future.done():
                captcha_future.set_exception(e)


async def main():
    async with async_playwright() as p:
        # Получаем нормализованный user agent
        user_agent = await normalize_user_agent(p)
        print("Нормализованный user agent:", user_agent)
       
        # Запускаем браузер с видимым окном
        browser = await p.chromium.launch(headless=False)
        context = await browser.new_context(user_agent=user_agent)
       
        # Читаем содержимое файла inject.js
        with open("inject.js", "r", encoding="utf-8") as f:
            inject_script = f.read()
        # Добавляем скрипт для инъекции в каждую страницу
        await context.add_init_script(script=inject_script)
       
        page = await context.new_page()
       
        # Создаём future для ожидания результата решения CAPTCHA
        captcha_future = asyncio.Future()
       
        # Регистрируем обработчик консольных сообщений
        page.on("console", lambda msg: asyncio.create_task(handle_console(msg, page, captcha_future)))
       
        # Переходим на целевую страницу
        await page.goto("https://crash.chicagopolice.org/")
       
        try:
            token = await asyncio.wait_for(captcha_future, timeout=120)
            print("Получен CAPTCHA токен:", token)
        except asyncio.TimeoutError:
            print("Время ожидания CAPTCHA истекло")
        await browser.close()


if __name__ == "__main__":
    asyncio.run(main())

Работает, кстати, также — не забудьте ключ АПИ подставить.

Таким образом — в нашу эпоху решение простых задач больше является чем то экстраординарным и при нормальном подходе — можно перестать мучить и без того замученных разработчиков на технических форумах, простыми вопросами. Ну вот только владельцы форумов вероятно будут грустить, ведь эти простые вопросы генерят тонну контента, который привлекает других пользователей, а дальше вы поняли.

© Habrahabr.ru