Карточка товара на Amazon отдаёт reCAPTCHA уже после третьего запроса подряд — и стабильный конвейер мониторинга цен вдруг начинает возвращать пустые значения вместо чисел. Рано или поздно защита срабатывает на любом скрапере, который регулярно опрашивает страницы товаров, и без автоматического решения CAPTCHA в базе появляются дыры вместо цен. CaptchaAI закрывает этот разрыв: распознаёт защиту на лету и возвращает токен, по которому скрапер получает страницу с реальной ценой.
Где мониторинг цен упирается в CAPTCHA
Боты мониторинга натыкаются на защиту у всех крупных ритейлеров:
| Платформа | Тип CAPTCHA | Триггер |
|---|---|---|
| Amazon | Image CAPTCHA, reCAPTCHA | Высокий объём запросов |
| Walmart | Cloudflare Turnstile | Обнаружение автоматизации |
| eBay | reCAPTCHA v2 | Подозрительные паттерны поведения |
| Best Buy | Cloudflare Challenge | Весь автоматизированный трафик |
| Магазины на Shopify | reCAPTCHA v3 | Зависит от настроек магазина |
Те же принципы работают и за пределами западных площадок: Wildberries, Ozon и AliExpress, как правило, используют похожие механизмы защиты. Без обработки CAPTCHA конвейер не падает с ошибкой — он молча перестаёт обновлять цены, и вы замечаете это только когда алерты не приходят неделями.
Признаки того, что конвейер уже упёрся в защиту, а не просто вернул пустую страницу:
- Селектор цены стабильно возвращает
Noneпри HTTP-статусе 200 — страница загрузилась, но это не карточка товара. - В логах воркера растёт доля повторных запросов к одним и тем же URL без итогового результата.
- Число новых записей в таблице цен резко падает при неизменном списке товаров в очереди.
Сколько это стоит
Бюджет зависит от частоты опроса каталога:
| Объём | CAPTCHA в день | Ориентировочная стоимость в день |
|---|---|---|
| 50 товаров каждые 30 мин. | ~2 400 | ~$2–5 |
| 200 товаров каждые 15 мин. | ~19 200 | ~$15–30 |
| 1000 товаров ежечасно | ~24 000 | ~$20–40 |
Не каждая загрузка упирается в CAPTCHA, так что расходы часто на 50–70 % ниже табличных. Тарифы CaptchaAI считаются по потокам, а не по решениям: для точечного мониторинга хватает BASIC ($15/мес, 5 потоков), для крупного каталога — ADVANCE ($90/мес, 50 потоков) или PREMIUM ($170/мес, 100 потоков).
Ориентир по выбору тарифа под нагрузку:
- До 50 товаров с редким опросом — BASIC ($15/мес, 5 потоков) обычно берёт задачу с запасом.
- 100–300 товаров с опросом каждые 15–30 минут — смотрите в сторону STANDARD ($30/мес, 15 потоков).
- Каталог на 1000+ товаров с ежечасным опросом — PREMIUM ($170/мес, 100 потоков) даёт запас по параллельности воркеров.
Архитектура конвейера
Scheduler (every 30 min)
→ URL Queue
→ Scraper Workers (5-10 concurrent)
→ Fetch page
→ CAPTCHA detected?
→ Yes → CaptchaAI → Solve → Retry page
→ No → Parse prices
→ Store in database
→ Alert on price changes
Планировщик раскладывает товары по очереди, воркеры разбирают её параллельно, а решение CAPTCHA включается только там, где защита реально сработала.
Совет: не пытайтесь закрыть весь каталог одним пулом воркеров. При росте нагрузки проще поднять второй пул с отдельной очередью, чем наращивать конкурентность внутри одного — так один медленный решённый CAPTCHA не тормозит остальную очередь.
Код: где именно подключается CaptchaAI
Что проверить перед первым запуском
- Переменная окружения
CAPTCHAAI_API_KEYподставляется в оба скрипта — не хардкодьте ключ прямо в код. - Селекторы цены в
selectorsактуальны для текущей вёрстки карточки товара: маркетплейсы меняют её без предупреждения, и это самая частая причина пустогоpriceпри живом ответе сайта. fetch_with_captchaпроверяет reCAPTCHA и Turnstile по очереди — по логам видно, какой из блоков сработал на конкретном URL.
Монитор цен (Python)
import requests
import time
import re
import json
import os
from datetime import datetime
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
BASE_URL = "https://ocr.captchaai.com"
def solve_captcha(method, params):
params["key"] = API_KEY
params["method"] = method
resp = requests.get(f"{BASE_URL}/in.php", params=params)
if not resp.text.startswith("OK|"):
raise Exception(f"Submit failed: {resp.text}")
task_id = resp.text.split("|")[1]
for _ in range(60):
time.sleep(5)
result = requests.get(f"{BASE_URL}/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)[1]
raise Exception(f"Solve failed: {result.text}")
raise TimeoutError("CAPTCHA solve timed out")
def fetch_with_captcha(url, session):
"""Fetch a page, solving CAPTCHAs if encountered."""
resp = session.get(url)
# Check for reCAPTCHA
match = re.search(r'data-sitekey=["\']([A-Za-z0-9_-]+)["\']', resp.text)
if match:
site_key = match.group(1)
token = solve_captcha("userrecaptcha", {
"googlekey": site_key,
"pageurl": url,
})
resp = session.post(url, data={"g-recaptcha-response": token})
# Check for Turnstile
match = re.search(
r'class="cf-turnstile"[^>]*data-sitekey=["\']([^"\']+)', resp.text
)
if match:
site_key = match.group(1)
token = solve_captcha("turnstile", {
"sitekey": site_key,
"pageurl": url,
})
resp = session.post(url, data={"cf-turnstile-response": token})
return resp
def extract_price(html, selectors):
"""Extract price from HTML using regex patterns."""
for pattern in selectors:
match = re.search(pattern, html)
if match:
price_str = match.group(1).replace(",", "")
return float(price_str)
return None
def monitor_prices(products):
"""Monitor prices for a list of products."""
session = requests.Session()
session.headers["User-Agent"] = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/120.0.0.0"
)
results = []
for product in products:
try:
resp = fetch_with_captcha(product["url"], session)
price = extract_price(resp.text, product["selectors"])
results.append({
"name": product["name"],
"url": product["url"],
"price": price,
"timestamp": datetime.utcnow().isoformat(),
"status": "ok",
})
print(f" {product['name']}: ${price}")
except Exception as e:
results.append({
"name": product["name"],
"url": product["url"],
"price": None,
"timestamp": datetime.utcnow().isoformat(),
"status": f"error: {e}",
})
print(f" {product['name']}: ERROR - {e}")
return results
# Define products to monitor
products = [
{
"name": "Wireless Headphones",
"url": "https://example.com/product/headphones",
"selectors": [
r'class="price"[^>]*>\$?([\d,]+\.?\d*)',
r'itemprop="price" content="([\d.]+)"',
],
},
{
"name": "Bluetooth Speaker",
"url": "https://example.com/product/speaker",
"selectors": [
r'class="price"[^>]*>\$?([\d,]+\.?\d*)',
],
},
]
print("Starting price check...")
results = monitor_prices(products)
# Save results
with open("prices.json", "w") as f:
json.dump(results, f, indent=2)
Реализация Node.js
const axios = require("axios");
const cheerio = require("cheerio");
const API_KEY = process.env.CAPTCHAAI_API_KEY;
async function solveCaptcha(method, params) {
params.key = API_KEY;
params.method = method;
const submit = await axios.get("https://ocr.captchaai.com/in.php", {
params,
});
const taskId = String(submit.data).split("|")[1];
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const poll = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "get", id: taskId },
});
const text = String(poll.data);
if (text === "CAPCHA_NOT_READY") continue;
if (text.startsWith("OK|")) return text.split("|").slice(1).join("|");
throw new Error(text);
}
throw new Error("Timeout");
}
async function monitorPrice(url) {
const resp = await axios.get(url);
const $ = cheerio.load(resp.data);
// Check for reCAPTCHA
const siteKey = $(".g-recaptcha").attr("data-sitekey");
if (siteKey) {
const token = await solveCaptcha("userrecaptcha", {
googlekey: siteKey,
pageurl: url,
});
// Re-fetch with token
const formResp = await axios.post(url, { "g-recaptcha-response": token });
return cheerio.load(formResp.data);
}
const price = $('[itemprop="price"]').attr("content") || $(".price").text();
return parseFloat(price.replace(/[^0-9.]/g, ""));
}
Расписание проверок
Запускайте проверки каждые 30 минут через cron:
# crontab -e
*/30 * * * * cd /opt/monitor && python price_monitor.py >> /var/log/prices.log 2>&1
Или через библиотеку Python schedule, если хочется держать планировщик внутри самого процесса:
import schedule
schedule.every(30).minutes.do(lambda: monitor_prices(products))
while True:
schedule.run_pending()
time.sleep(60)
Слишком частый опрос — самый быстрый способ получить CAPTCHA на каждом втором запросе. 30 минут — рабочий компромисс для большинства каталогов.
Ориентир по интервалу опроса:
- Ценовые войны, дефицитный товар — каждые 10–15 минут, закладывайте больший бюджет на решение CAPTCHA.
- Обычный каталог без резких скачков спроса — 30 минут, вариант по умолчанию.
- Долгосрочный трекинг тренда цены — раз в 2–4 часа достаточно и почти не упирается в защиту.
Частые вопросы о мониторинге цен
Какой тариф CaptchaAI выбрать для мониторинга каталога?
Отталкивайтесь от числа воркеров, а не товаров: 5–10 запросов обычно укладываются в BASIC (5 потоков) или STANDARD ($30/мес, 15 потоков).
Как снизить риск блокировки при частом мониторинге?
Ротируйте прокси и User-Agent, распределяйте запросы по времени вместо пакетной отправки — решение CAPTCHA снимает блокировку, но не отменяет смысл вести себя как обычный трафик.
Нужно ли учитывать закон о персональных данных при сборе цен конкурентов?
Публичные цены сами по себе под 152-ФЗ и GDPR не подпадают. Вопрос возникает, если вместе с ценой вы начинаете собирать данные о пользователях — отзывы с именами, аккаунты. Собирайте только то, что нужно для мониторинга цен.
Что делать, если price пустой, хотя страница вернула 200?
Сначала проверьте лог: если fetch_with_captcha вообще не находил data-sitekey, значит защита не сработала и дело в селекторе — вёрстка карточки поменялась. Если токен решён, а цена всё равно пустая, сверьте регулярку в selectors с актуальным HTML страницы.
Нужен ли прокси вместе с CaptchaAI, или решения CAPTCHA достаточно?
Это разные задачи. Прокси снижают шанс, что защита вообще покажется при высокой частоте запросов с одного IP. CaptchaAI решает саму защиту, когда она уже показалась — вне зависимости от того, ротируете вы адреса или нет. Для стабильного конвейера обычно нужны оба слоя, а не один вместо другого.