Агрегатор цен на лекарства спотыкается о 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 не появится вовсе, но заметно снижает её частоту по сравнению со сплошным потоком запросов без пауз.