Use Cases

Парсинг веб-сайтов, защищенных CAPTCHA

Парсер редко ломается на разметке. Он ломается в тот момент, когда сервер вместо данных отдаёт страницу с проверкой, а код продолжает раскладывать её по селекторам и писать в базу пустые строки. Практический ответ короткий: парсер должен распознавать проверку прямо в HTTP-ответе, отправлять её в API CaptchaAI и повторять исходный запрос уже с токеном. Остальной пайплайн при этом трогать не нужно.

Ниже разобраны три ситуации, которые покрывают почти любой сбор данных: проверка появляется изредка, проверка стоит на конкретной странице всегда и вся площадка закрыта Cloudflare. Примеры на Python, но логика одинакова для Node.js, Go и PHP — in.php принимает задачу, res.php отдаёт токен.

Как понять, что вместо данных пришла проверка

Первый шаг — не решение, а обнаружение: пока парсер не отличает страницу с данными от страницы с проверкой, логика повторов работает вслепую. Надёжные маркеры в теле ответа:

  • g-recaptcha и атрибут data-sitekey в разметке — reCAPTCHA v2;
  • контейнер cf-turnstile и скрытое поле cf-turnstile-response — Cloudflare Turnstile;
  • HTTP 403 или 503 с коротким HTML вместо ожидаемого документа — полностраничная проверка Cloudflare;
  • картинка в base64 рядом с текстовым полем и коротким <form> — классическая image/OCR-проверка.

Заведите метрику «доля ответов с проверкой» и пишите её в логи: её рост — раннее предупреждение о том, что площадка изменила настройки защиты.

Какие проверки CaptchaAI решает, а какие нет

Тип проверки Метод CaptchaAI Где встречается при парсинге
reCAPTCHA v2 method=userrecaptcha формы входа, страницы поиска
reCAPTCHA v3 method=userrecaptcha&version=v3 фоновая оценка на любой странице
Cloudflare Turnstile method=turnstile сайты за Cloudflare
Полностраничная проверка Cloudflare method=turnstile блокирующая страница на весь домен
Изображение / OCR method=base64 устаревшие формы, архивы, каталоги
GeeTest v3 method=geetest площадки с азиатским анти-бот стеком
hCaptcha ❌ не поддерживается сайты, ориентированные на приватность
GeeTest v4 ❌ пока нет, заявлено как «скоро» новые сборки GeeTest

CaptchaFox, Friendly Captcha и Lemin доступны в статусе beta — закладывать их в продакшн-парсер как штатный шаг пока рано. Если целевая площадка стоит на hCaptcha или FunCaptcha (Arkose Labs), честный вывод один: этот источник данных придётся закрывать другим способом.

Подход 1: решать проверку по факту появления

Самый экономный сценарий. Парсер работает обычным образом, а решение запрашивается только тогда, когда проверка реально пришла в ответе. Так вы не тратите потоки на страницы, которые отдаются свободно.

Класс ниже держит одну сессию requests с постоянным набором заголовков, проверяет ответ на маркеры, вытаскивает sitekey из разметки и отправляет задачу в in.php. Метод _poll опрашивает res.php раз в пять секунд, пока не получит OK| или ошибку.

import requests
import time
from bs4 import BeautifulSoup

API_KEY = "YOUR_API_KEY"

class ProtectedScraper:
    def __init__(self):
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
        })

    def scrape(self, url):
        resp = self.session.get(url)

        # Check for CAPTCHA
        if self._has_captcha(resp.text):
            resp = self._handle_captcha(resp.text, url)

        return resp.text

    def _has_captcha(self, html):
        indicators = ["g-recaptcha", "cf-turnstile", "h-captcha", "captcha"]
        return any(ind in html.lower() for ind in indicators)

    def _handle_captcha(self, html, url):
        soup = BeautifulSoup(html, "html.parser")

        # reCAPTCHA v2
        rc = soup.find("div", class_="g-recaptcha")
        if rc:
            token = self._solve_recaptcha(rc["data-sitekey"], url)
            return self.session.post(url, data={"g-recaptcha-response": token})

        # Cloudflare Turnstile
        ts = soup.find("div", class_="cf-turnstile")
        if ts:
            token = self._solve_turnstile(ts["data-sitekey"], url)
            return self.session.post(url, data={"cf-turnstile-response": token})

        raise Exception("Unknown CAPTCHA type")

    def _solve_recaptcha(self, site_key, page_url):
        resp = requests.get("https://ocr.captchaai.com/in.php", params={
            "key": API_KEY, "method": "userrecaptcha",
            "googlekey": site_key, "pageurl": page_url
        })
        return self._poll(resp.text.split("|")[1])

    def _solve_turnstile(self, site_key, page_url):
        resp = requests.get("https://ocr.captchaai.com/in.php", params={
            "key": API_KEY, "method": "turnstile",
            "sitekey": site_key, "pageurl": page_url
        })
        return self._poll(resp.text.split("|")[1])

    def _poll(self, task_id):
        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
            })
            if result.text == "CAPCHA_NOT_READY": continue
            if result.text.startswith("OK|"): return result.text.split("|")[1]
            raise Exception(result.text)
        raise TimeoutError()

# Usage
scraper = ProtectedScraper()
html = scraper.scrape("https://example.com/data")

Ключевая деталь — токен возвращается в то же поле, которого ждёт форма: g-recaptcha-response для reCAPTCHA и cf-turnstile-response для Turnstile. Если подставить его не туда, сервер молча отдаст ту же страницу с проверкой, и парсер зациклится.

Подход 2: решать заранее там, где проверка стоит всегда

Некоторые страницы — поиск, экспорт, форма фильтра — показывают проверку каждый раз. Ходить за ними дважды бессмысленно: запросите токен и сразу отправляйте форму вместе с ним.

def scrape_known_captcha_page(url, site_key):
    # Solve before even loading the page
    token = solve_recaptcha(site_key, url)

    # Submit directly with token
    resp = requests.post(url, data={
        "g-recaptcha-response": token,
        "query": "search term"
    })
    return resp.text

Важная оговорка: токен запрашивается непосредственно перед отправкой, а не складывается в запас. Время жизни у него короткое, и попытка накопить решения впрок даёт пачку просроченных токенов.

Подход 3: страницы за Cloudflare

Если Cloudflare закрывает домен целиком, отдельного поля формы нет — результат проверки фиксируется в cookie, которую нужно донести до всех последующих запросов.

def get_cloudflare_clearance(url, proxy):
    resp = requests.get("https://ocr.captchaai.com/in.php", params={
        "key": API_KEY,
        "method": "turnstile",
        "pageurl": url,
        "proxy": proxy,
        "proxytype": "HTTP"
    })
    task_id = resp.text.split("|")[1]

    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
        })
        if result.text == "CAPCHA_NOT_READY": continue
        if "qa_validation_cookie" in result.text:
            # Parse qa_validation_cookie and user_agent from response
            return result.text
    raise TimeoutError()

Прокси здесь указывается по технической причине: проверка решается с того же маршрута, с которого пойдут дальнейшие запросы. Смените прокси или User-Agent между решением и запросом — и cookie перестанет соответствовать сессии.

Пагинация: собрать десятки страниц и не потерять данные

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

def scrape_multiple_pages(base_url, site_key, pages):
    scraper = ProtectedScraper()
    results = []

    for page in pages:
        url = f"{base_url}?page={page}"
        try:
            html = scraper.scrape(url)
            soup = BeautifulSoup(html, "html.parser")
            items = soup.find_all("div", class_="item")
            results.extend([item.text.strip() for item in items])
            print(f"Page {page}: {len(items)} items")
        except Exception as e:
            print(f"Page {page} failed: {e}")

        time.sleep(random.uniform(2, 5))

    return results

Для длинных проходов сохраняйте промежуточный результат на диск каждые N страниц: ночной сбор на нестабильном канале — обычная ситуация, и терять четыре часа работы из-за разрыва соединения не хочется.

Сколько потоков нужно парсеру

CaptchaAI тарифицируется по потокам, а не по числу решений: поток — это одна проверка в работе, количество решений в месяц не ограничено. Cloudflare Turnstile, по данным CaptchaAI, решается менее чем за 10 секунд, то есть пять параллельных потоков дают ориентировочно 1800 проверок в час. Отсюда практическое сопоставление:

  • одиночный парсер на одной-двух машинах — BASIC ($15/мес, 5 потоков) или STANDARD ($30/мес, 15 потоков);
  • регулярный сбор с десятков воркеров — ADVANCE ($90/мес, 50 потоков);
  • постоянные ночные проходы по крупным каталогам — PREMIUM ($170/мес, 100 потоков) и выше.

Для фрилансеров и агентств, которые считают расходы в валюте с высокой волатильностью, предсказуемый месячный платёж в USD часто важнее цены за отдельное решение: бюджет проекта фиксируется на старте.

Что проверить, если парсер всё равно не проходит

Симптом Что делать
Проверка появляется на каждой странице снизьте частоту запросов и разнесите проход по времени
Токен отклонён сразу после решения срок его жизни короткий; отправляйте токен в пределах 120 секунд после получения
Cloudflare блокирует даже с полученной cookie используйте один и тот же прокси и User-Agent и при решении, и при последующих запросах
После прохождения приходит не та страница проверьте промежуточные редиректы и cookie, которые ставит форма
HTTP 200, но нужные поля пустые контент дорисовывается на JavaScript — нужен браузерный движок, а не голый HTTP-клиент

Собирайте только те данные, которые вправе обрабатывать

Техническая возможность получить страницу и право хранить её содержимое — разные вещи. Перед регулярным сбором посмотрите условия использования площадки и robots.txt, а если в выгрузку попадают персональные данные — оцените свои обязанности по 152-ФЗ «О персональных данных» или по применимому к вам регламенту. Правило простое: собирайте только те поля, которые вам действительно нужны, и фиксируйте, зачем нужен каждый атрибут.

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

Почему проверка появилась там, где её раньше не было?

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

Сколько живёт токен и почему сервер его не принимает?

Практический ориентир — 120 секунд. Если между получением токена и отправкой формы парсер успевает сходить ещё за тремя страницами, токен приедет просроченным. Вторая частая причина — токен подставлен не в то поле или отправлен на другой URL, чем тот, который был указан в pageurl.

Как подключить решение проверки к парсеру на Selenium или Playwright?

Схема та же: браузер рендерит страницу, вы забираете sitekey из DOM, отправляете задачу в API, а полученный токен вставляете в скрытое поле и вызываете отправку формы. Подробный разбор с кодом — в материале обработка CAPTCHA в Selenium на Python.

Что делать, если стоит hCaptcha?

Менять источник данных: этот тип не поддерживается — как и FunCaptcha (Arkose Labs). Поддерживаются reCAPTCHA v2 и v3, Cloudflare Turnstile и Cloudflare Challenge, GeeTest v3, image/OCR, grid и BLS; CaptchaFox, Friendly Captcha и Lemin — в статусе beta.

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

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