Use Cases

Обработка CAPTCHA для сравнения цен в аптеках

Агрегатор цен на лекарства спотыкается о CAPTCHA уже на первом поисковом запросе. Форму поиска почти на каждом аптечном портале закрывает reCAPTCHA v2, страницу с результатами — Cloudflare Turnstile, а купоны на скидку прячут за графической CAPTCHA. Если не обработать это в коде, каждый запрос пользователя будет упираться в ручную проверку. Ниже — рабочая схема сбора и сравнения цен без остановок на CAPTCHA: решение токена, параллельный опрос нескольких источников и приёмы, снижающие частоту проверок.

Где на аптечных порталах встречается CAPTCHA

Тип страницы Типичная CAPTCHA Когда появляется
Форма поиска лекарства reCAPTCHA v2 При каждом поисковом запросе
Страница с результатами цен Cloudflare Turnstile При подозрении на автоматический доступ
Поиск ближайшей аптеки reCAPTCHA v2 При запросах с геолокацией
Поиск купонов на скидку Графическая CAPTCHA Перед показом кода скидки
Страховой формуляр reCAPTCHA v2 При входе или поиске в защищённом разделе

Показательный сценарий: агрегатор, собирающий цены на инсулин и другие рецептурные препараты сразу с 10–15 региональных аптечных сетей для аудитории в России, Казахстане и Беларуси. У каждой сети — свой набор защит:

  • свой поставщик CAPTCHA (reCAPTCHA v2, Turnstile или графическая капча);
  • свой формат страницы результатов;
  • свой порог срабатывания защиты при частых запросах.

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

Базовая схема решения reCAPTCHA v2

Ядро обработчика — две функции: одна отправляет sitekey и URL страницы в CaptchaAI и опрашивает res.php до готовности токена, вторая достаёт sitekey со страницы поиска и подставляет токен в форму:

import requests
import time

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

def solve_recaptcha(site_key, page_url):
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": "YOUR_API_KEY",
        "method": "userrecaptcha",
        "googlekey": site_key,
        "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"]
    raise TimeoutError("Solve timed out")

def search_drug_prices(drug_name, zipcode):
    search_url = "https://pharmacy.example.com/search"

    # Load the search page to get the site key
    page = session.get(search_url)

    # Extract reCAPTCHA site key
    import re
    match = re.search(r'data-sitekey="([^"]+)"', page.text)
    site_key = match.group(1)

    # Solve the CAPTCHA
    token = solve_recaptcha(site_key, search_url)

    # Submit the search with token
    results = session.post(search_url, data={
        "drug": drug_name,
        "zip": zipcode,
        "g-recaptcha-response": token
    })

    return parse_prices(results.text)

Обратите внимание на session = requests.Session() — она держит cookies между запросами, поэтому reCAPTCHA не всплывает на каждом обращении к одному и тому же порталу.

Сравнение цен сразу по нескольким аптекам

Реальная ценность инструмента — не в одном решённом токене, а в сопоставлении цен из разных источников за один проход:

PHARMACY_SOURCES = [
    {
        "name": "PharmacyA",
        "url": "https://pharmacya.example.com/pricing",
        "captcha_type": "recaptcha_v2"
    },
    {
        "name": "PharmacyB",
        "url": "https://pharmacyb.example.com/drugs",
        "captcha_type": "turnstile"
    },
    {
        "name": "PharmacyC",
        "url": "https://pharmacyc.example.com/search",
        "captcha_type": "image"
    }
]

def compare_drug_price(drug_name, zipcode):
    results = []

    for source in PHARMACY_SOURCES:
        try:
            prices = fetch_prices(
                source["url"],
                source["captcha_type"],
                drug_name,
                zipcode
            )
            results.append({
                "source": source["name"],
                "prices": prices,
                "status": "success"
            })
        except Exception as e:
            results.append({
                "source": source["name"],
                "error": str(e),
                "status": "failed"
            })

    return sorted(results, key=lambda r: r.get("prices", {}).get("lowest", float("inf")))

Ошибка одного источника не должна валить весь запрос — поэтому каждый fetch_prices обёрнут в try/except, а в итоговый список попадает статус failed вместо падения всей функции.

Если у каждого источника свой тип CAPTCHA

Аптечные порталы редко используют одного и того же поставщика CAPTCHA — вынесите выбор типа в отдельную функцию, которая возвращает и токен, и имя поля формы:

def fetch_prices(url, captcha_type, drug_name, zipcode):
    page = session.get(url)

    if captcha_type == "recaptcha_v2":
        import re
        match = re.search(r'data-sitekey="([^"]+)"', page.text)
        token = solve_recaptcha(match.group(1), url)
        field_name = "g-recaptcha-response"

    elif captcha_type == "turnstile":
        import re
        match = re.search(r'data-sitekey="(0x[^"]+)"', page.text)
        token = solve_turnstile(match.group(1), url)
        field_name = "cf-turnstile-response"

    elif captcha_type == "image":
        img_data = extract_captcha_image(page.text)
        token = solve_image_captcha(img_data)
        field_name = "captcha"

    return session.post(url, data={
        "drug": drug_name,
        "zip": zipcode,
        field_name: token
    })

Так добавление нового источника с новым типом CAPTCHA — это одна новая ветка elif, а не переписывание всего парсера.

Сессии для повторных поисков: как реже упираться в CAPTCHA

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

class PharmacySession:
    def __init__(self, base_url):
        self.session = requests.Session()
        self.base_url = base_url
        self.searches_since_captcha = 0

    def search(self, drug_name, zipcode):
        result = self.session.post(f"{self.base_url}/search", data={
            "drug": drug_name,
            "zip": zipcode
        })

        if self.is_captcha_page(result.text):
            token = self.solve_page_captcha(result.text)
            result = self.session.post(f"{self.base_url}/search", data={
                "drug": drug_name,
                "zip": zipcode,
                "g-recaptcha-response": token
            })
            self.searches_since_captcha = 0
        else:
            self.searches_since_captcha += 1

        return result

    def is_captcha_page(self, html):
        return "g-recaptcha" in html or "cf-turnstile" in html

    def solve_page_captcha(self, html):
        import re
        match = re.search(r'data-sitekey="([^"]+)"', html)
        return solve_recaptcha(match.group(1), self.base_url)

searches_since_captcha — не для отчётности: по нему удобно отслеживать, для какого источника CAPTCHA вылезает подозрительно часто, и в первую очередь чинить сессию именно там.

Та же схема на JavaScript

Для стека на Node.js логика идентична: решить CAPTCHA для источника, затем отправить запрос с полученным токеном — только источники опрашиваются параллельно через Promise.all:

async function comparePharmacyPrices(drugName, zipCode, sources) {
  const results = await Promise.all(
    sources.map(async (source) => {
      try {
        const price = await fetchDrugPrice(source, drugName, zipCode);
        return { source: source.name, price, status: 'success' };
      } catch (error) {
        return { source: source.name, error: error.message, status: 'failed' };
      }
    })
  );

  return results
    .filter(r => r.status === 'success')
    .sort((a, b) => a.price - b.price);
}

async function fetchDrugPrice(source, drugName, zipCode) {
  // Solve CAPTCHA for this source
  const token = await solveCaptcha(source.siteKey, source.url);

  const response = await fetch(source.url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      drug: drugName,
      zip: zipCode,
      'g-recaptcha-response': token
    })
  });

  return parsePrice(await response.text());
}

Как не спровоцировать CAPTCHA лишний раз

Аптечные порталы внимательно следят за паттернами доступа, и агрессивный парсинг сам провоцирует появление CAPTCHA чаще.

Особенно это касается агрегаторов, которые опрашивают одни и те же сети по расписанию несколько раз в день.

Стратегия Как реализовать
Разносите запросы во времени Пауза 5–10 секунд между поисками
Ротируйте сессии Новая сессия каждые 20–30 запросов
Меняйте паттерн поиска Не повторяйте один и тот же запрос подряд
Используйте резидентные прокси IP дата-центров чаще провоцируют CAPTCHA, чем обычные жилые адреса

Что делать, если что-то идёт не так

Проблема Причина Решение
CAPTCHA появляется при каждом поиске Сессия не сохраняет cookies Используйте один и тот же requests.Session(), а не новый объект на каждый запрос
После решения CAPTCHA цены не подгружаются Не передан скрытый CSRF-токен формы Извлеките скрытые поля формы и отправьте их вместе с запросом
Портал блокирует после серии запросов Сработало ограничение частоты запросов Добавьте паузы и ротацию сессий между запросами
Цены отличаются от тех, что видны в браузере Не хватает cookies или заголовков сессии Скопируйте заголовки браузера один в один

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

Тариф BASIC или STANDARD — что выбрать для агрегатора цен на лекарства?

Для 10–15 источников обычно достаточно BASIC ($15/мес, 5 потоков): пока каждый запрос решается за секунды, пять параллельных потоков покрывают типичную нагрузку такого агрегатора. Если планируете расширяться до нескольких десятков аптечных сетей или собирать данные чаще, чем раз в день, берите STANDARD ($30/мес, 15 потоков) с запасом — тарифы CaptchaAI считаются по потокам, а не по количеству решений, так что решений внутри потока сколько угодно.

После решения reCAPTCHA v2 цены всё равно не подгружаются — в чём дело?

Чаще всего дело не в CAPTCHA, а в скрытом поле формы (например, CSRF-токене), которое портал ожидает вместе с g-recaptcha-response. Проверьте HTML страницы поиска на дополнительные <input type="hidden"> и отправляйте их вместе с токеном — см. таблицу выше.

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

Если код перестал находить data-sitekey или сходный по логике маркер на странице, скорее всего портал перешёл на другой тип защиты. CaptchaAI решает основные типы CAPTCHA, поэтому при смене провайдера обычно достаточно поменять ветку в fetch_prices, а не переписывать логику парсинга целиком.

Отличается ли обработка CAPTCHA для дженериков и оригинальных препаратов?

Принципиально нет: механизм решения токена одинаков. На практике базы дженериков чаще ограничиваются простой графической CAPTCHA, а порталы брендовых препаратов и крупных аптечных сетей — reCAPTCHA v2 или Cloudflare Turnstile, потому что их страницы результатов интереснее для парсинга ценовым агрегаторам.

Как часто нужно делать паузы между запросами, чтобы не спровоцировать CAPTCHA?

Ориентир — 5–10 секунд между поисковыми запросами на один источник и новая сессия каждые 20–30 запросов. Это не гарантия, что CAPTCHA не появится вовсе, но заметно снижает её частоту по сравнению со сплошным потоком запросов без пауз.

Следующие шаги

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