Un CAPTCHA sur une fiche produit suffit à interrompre toute une campagne de veille tarifaire : la page ne se charge pas, le prix n'est pas relevé et votre historique se retrouve troué. La parade consiste à détecter ces défis à la volée et à les résoudre automatiquement. Voici comment bâtir un pipeline de surveillance des prix e-commerce qui gère reCAPTCHA v2 et Cloudflare Turnstile via l'API CaptchaAI, du premier relevé à l'alerte de changement de prix.
Pourquoi la surveillance des prix e-commerce déclenche des CAPTCHA
La veille tarifaire repose sur des relevés fréquents des mêmes fiches produit. C'est justement le profil de trafic que les défenses anti-bot cherchent à ralentir : au-delà d'un certain rythme, la page renvoie un CAPTCHA au lieu du prix attendu.
| Plateforme | Type de CAPTCHA observé | Déclencheur habituel |
|---|---|---|
| Amazon | Image CAPTCHA, reCAPTCHA | Volume de requêtes élevé |
| Walmart | Cloudflare Turnstile | Détection de trafic automatisé |
| eBay | reCAPTCHA v2 | Motifs de navigation suspects |
| Best Buy | Cloudflare Challenge | Tout le trafic automatisé |
| Boutiques Shopify | reCAPTCHA v3 | Selon la configuration du marchand |
Ces associations sont indicatives : un même site peut changer de protection selon la région ou la charge. Sans gestion des CAPTCHA, les conséquences s'enchaînent vite :
- des trous dans vos séries de prix, là où un défi a remplacé le tarif ;
- une analyse concurrentielle faussée, calculée sur des données incomplètes ;
- des échecs silencieux : le script tourne sans signaler qu'il ne relève plus rien.
Architecture d'un pipeline de veille résilient
Le principe est simple : chaque relevé passe par une détection, et le CAPTCHA n'est résolu que lorsqu'il apparaît. Les pages qui se chargent normalement ne coûtent rien de plus.
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
Le pipeline enchaîne quatre étapes :
- un planificateur alimente une file d'URL à intervalle régulier ;
- des workers récupèrent les pages en parallèle (cinq à dix au départ) ;
- seul le chemin « CAPTCHA détecté » appelle l'API, le reste part au parsing ;
- les prix extraits partent en base, puis leur comparaison avec les valeurs précédentes déclenche les alertes.
Estimer le budget avec la facturation par threads
CaptchaAI facture au thread, pas à la résolution : un thread traite un CAPTCHA à la fois et se libère dès qu'une résolution se termine, avec un nombre illimité de résolutions par thread sur le mois. Ce qui compte pour une veille n'est donc pas le nombre total de CAPTCHA par jour, mais la concurrence : combien de résolutions tournent en même temps au pic.
| Charge de surveillance | Concurrence au pic | Plan adapté |
|---|---|---|
| 50 produits, toutes les 30 min | quelques résolutions simultanées | BASIC ($15/mois, 5 threads) |
| 200 produits, toutes les 15 min | pics modérés | STANDARD ($30/mois, 15 threads) |
| 1 000 produits, toutes les heures | forte concurrence | ADVANCE ($90/mois, 50 threads) |
Gardez deux repères en tête avant de choisir un plan :
- ces charges sont indicatives et varient selon l'environnement et le volume ;
- toutes les pages ne déclenchent pas un CAPTCHA : en résolvant seulement à la détection, la concurrence réelle reste bien inférieure au volume brut.
Mise en œuvre
Moniteur de prix en Python
Le cœur du script est solve_captcha : elle envoie le défi à in.php, puis interroge res.php toutes les cinq secondes tant que la réponse reste CAPCHA_NOT_READY. fetch_with_captcha repère ensuite un sitekey reCAPTCHA ou Turnstile et rejoue la requête avec le champ g-recaptcha-response ou cf-turnstile-response.
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)
Version Node.js
La même logique se transpose avec axios et cheerio : chargez la page, lisez l'attribut data-sitekey de l'élément .g-recaptcha, puis rejouez la requête une fois le token obtenu. Le polling garde le rythme de cinq secondes de Python.
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, ""));
}
Planifier les relevés
Un intervalle de 30 minutes suffit pour la plupart des campagnes : il capte les changements sans marteler les sites cibles. En production, une entrée cron reste la plus robuste.
# crontab -e
*/30 * * * * cd /opt/monitor && python price_monitor.py >> /var/log/prices.log 2>&1
Pour un ordonnancement piloté depuis Python, la bibliothèque schedule fait le travail sans dépendance système :
import schedule
schedule.every(30).minutes.do(lambda: monitor_prices(products))
while True:
schedule.run_pending()
time.sleep(60)
Côté hébergement, rapprochez vos workers des marchands ciblés pour réduire la latence :
- un petit worker sur OVHcloud ou Scaleway suffit dans la plupart des cas ;
- une fonction planifiée en région européenne (eu-west-3, Paris) reste au plus près des sites francophones.
Conformité RGPD et collecte responsable
Les prix affichés sont des données publiques, mais un pipeline de surveillance reste soumis à quelques garde-fous, surtout pour un public francophone :
- limitez la collecte aux seules données tarifaires et évitez d'aspirer des données personnelles (avis nominatifs, comptes clients) — c'est la minimisation posée par le RGPD ;
- respectez les conditions d'utilisation et le
robots.txtdes sites, et restez sur votre périmètre de veille autorisé ; - espacez les requêtes : un rythme régulier est plus fiable qu'une rafale ponctuelle.
Dépannage
| Problème | Cause probable | Correctif |
|---|---|---|
| Le CAPTCHA n'est jamais détecté | Le motif data-sitekey ne correspond pas au balisage réel de la page |
Inspectez le HTML rendu et ajustez l'expression régulière |
CAPCHA_NOT_READY en boucle jusqu'au timeout |
La résolution dépasse la fenêtre de polling | Augmentez le nombre d'itérations ou l'intervalle entre deux interrogations |
Prix relevé à None |
Les sélecteurs ne correspondent plus après une refonte du site | Mettez à jour la liste selectors du produit concerné |
| Blocages malgré la résolution | Même IP et même User-Agent sur trop de requêtes | Faites tourner les proxys résidentiels et espacez les relevés |
FAQ
Quels types de CAPTCHA CaptchaAI résout-il pour la veille tarifaire ?
Les plus courants : reCAPTCHA v2 et v3, Cloudflare Turnstile et Cloudflare Challenge, GeeTest v3, plus les CAPTCHA image/OCR et en grille. En revanche, hCaptcha et FunCaptcha ne sont pas pris en charge.
La surveillance des prix est-elle conforme au RGPD ?
Les prix sont des données publiques, donc leur relevé est généralement admis. Restez néanmoins prudent : minimisez les données personnelles collectées, respectez les conditions d'utilisation des sites et vérifiez vos obligations RGPD avant toute collecte à grande échelle.
Comment réduire le coût de résolution des CAPTCHA ?
Jouez sur trois leviers :
- ne résolvez qu'à la détection, jamais de façon systématique ;
- mettez en cache les pages qui ne changent pas d'un relevé à l'autre ;
- espacez vos relevés pour lisser les pics de concurrence.
Comme la facturation est par thread avec résolutions illimitées, un plan dimensionné sur votre concurrence coûte moins cher qu'une tarification à la pièce.
Vais-je être bloqué même en résolvant les CAPTCHA ?
La résolution lève le défi, mais pas les autres signaux de détection. Faites tourner proxys résidentiels et User-Agents, et répartissez les requêtes dans le temps.