Use Cases

Surveillance des stocks de détail avec gestion CAPTCHA

Un moniteur de stocks qui reçoit un défi CAPTCHA ne lève aucune exception : statut 200, parsing normal, et une ligne de plus avec un prix vide. Le problème n'apparaît que des jours plus tard, dans un tableau de bord figé. Repérez la page de vérification dans le HTML, envoyez-la à l'API CaptchaAI, rejouez la requête dans la même session.

Le reste — cadence, proxys, sélecteurs — sert surtout à ce que ce défi apparaisse le moins souvent possible. Tout ce qui suit tient en Python standard : requests, BeautifulSoup et un proxy résidentiel.


Périmètre : lire des pages publiques, rien de plus

Le code ci-dessous lit des informations publiques : titre de la fiche, prix affiché, mention de disponibilité. Il ne crée pas de compte, ne remplit pas de panier et ne déclenche aucun paiement.

  • Conditions d'utilisation : respectez le robots.txt et les limites de débit publiées par le site.
  • RGPD : une fiche produit ne contient pas de données personnelles, un avis client en contient — ne gardez que les champs réellement utiles.
  • Charge : une vérification toutes les 30 minutes sur 200 références reste marginale ; descendre à la minute multiplie les défis.

Quels sites marchands renvoient quel type de CAPTCHA

Les observations ci-dessous varient selon l'environnement, le volume et le moment de la journée.

Enseigne Type de CAPTCHA rencontré Déclencheur habituel Données visées
Amazon reCAPTCHA v2 Volume d'accès élevé Prix, stock, avis
Walmart reCAPTCHA v3 + Cloudflare Détection de trafic automatisé Stock, prix
Best Buy reCAPTCHA v2 Contrôle à l'ajout au panier Stock, prix
Target Cloudflare Turnstile Accès automatisé Disponibilité
Boutiques Shopify Cloudflare Turnstile Limitation de débit Données produit
eBay reCAPTCHA v2 Recherche et pages d'annonces Annonces, prix

CaptchaAI prend en charge ces trois familles : Turnstile en moins de 10 s, reCAPTCHA v3 en moins de 4 s, reCAPTCHA v2 en moins de 60 s, avec un taux de réussite élevé.


Choisir la cadence de vérification avant d'écrire le code

Cette décision commande tout le reste : budget proxy, nombre de threads, fréquence des défis. Calez-la sur la volatilité réelle de la catégorie.

Catégorie de produit Cadence raisonnable Type de proxy
Électronique Toutes les 30 minutes Résidentiel rotatif
Alimentaire et drive Toutes les 4 heures Résidentiel rotatif
Mode Toutes les 2 heures Résidentiel
Éditions limitées Toutes les 1 à 5 minutes Mobile
Équipement de la maison Toutes les 6 heures Résidentiel
Produits de fond de rayon Toutes les 12 heures Datacenter (souvent suffisant)

Prenez une équipe de veille tarifaire lyonnaise qui suit 800 références sur six enseignes, avec des workers chez OVHcloud ou Scaleway. En passant d'une cadence uniforme de 15 minutes à une cadence par catégorie, elle divise son volume de requêtes par quatre, et les défis CAPTCHA suivent. Pour des cibles européennes, hébergez les workers en eu-west-3 (Paris) : vous économisez l'aller-retour transatlantique à chaque cycle.


Le moniteur de stocks e-commerce en Python

Le cœur du script est _solve_and_retry : cette méthode extrait le data-sitekey de la page bloquée, choisit le mode de résolution selon le marqueur présent dans le HTML, puis rejoue la requête avec le token dans le champ attendu — cf-turnstile-response pour Turnstile, g-recaptcha-response pour reCAPTCHA. Le polling sur res.php interroge le résultat toutes les 5 secondes. Remplacez YOUR_API_KEY par votre clé.

import requests
import time
import re
import json
from datetime import datetime
from bs4 import BeautifulSoup

CAPTCHAAI_KEY = "YOUR_API_KEY"
CAPTCHAAI_URL = "https://ocr.captchaai.com"


def solve_captcha(method, sitekey, pageurl, **kwargs):
    data = {
        "key": CAPTCHAAI_KEY, "method": method,
        "googlekey": sitekey, "pageurl": pageurl, "json": 1,
    }
    data.update(kwargs)
    resp = requests.post(f"{CAPTCHAAI_URL}/in.php", data=data)
    task_id = resp.json()["request"]
    for _ in range(60):
        time.sleep(5)
        result = requests.get(f"{CAPTCHAAI_URL}/res.php", params={
            "key": CAPTCHAAI_KEY, "action": "get",
            "id": task_id, "json": 1,
        })
        r = result.json()
        if r["request"] != "CAPCHA_NOT_READY":
            return r["request"]
    raise TimeoutError("Timeout")


class RetailMonitor:
    def __init__(self, proxy=None):
        self.session = requests.Session()
        if proxy:
            self.session.proxies = {"http": proxy, "https": proxy}
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 Chrome/126.0.0.0 Safari/537.36",
            "Accept": "text/html,application/xhtml+xml,*/*;q=0.8",
            "Accept-Language": "en-US,en;q=0.9",
        })

    def check_product(self, url):
        """Check single product's price and availability."""
        resp = self.session.get(url, timeout=30)

        # Handle CAPTCHA
        if self._has_captcha(resp.text):
            resp = self._solve_and_retry(resp.text, url)

        soup = BeautifulSoup(resp.text, "html.parser")
        return {
            "url": url,
            "title": self._text(soup, "h1, .product-title, #productTitle"),
            "price": self._text(soup, ".price, .a-price .a-offscreen, .prod-price"),
            "availability": self._text(soup, "#availability, .stock-status, .fulfillment"),
            "in_stock": self._check_stock(soup),
            "timestamp": datetime.now().isoformat(),
        }

    def monitor_products(self, product_urls, interval_sec=1800):
        """Continuously monitor products for changes."""
        history = {}

        while True:
            for url in product_urls:
                try:
                    current = self.check_product(url)

                    # Check for changes
                    prev = history.get(url)
                    if prev:
                        changes = self._detect_changes(prev, current)
                        if changes:
                            self._alert(current["title"], changes)

                    history[url] = current
                    time.sleep(3)

                except Exception as e:
                    print(f"Error checking {url}: {e}")

            print(f"Cycle complete: {len(product_urls)} products checked")
            time.sleep(interval_sec)

    def track_prices(self, product_urls, output_file="prices.json"):
        """Single price check across all products."""
        results = []
        for url in product_urls:
            try:
                data = self.check_product(url)
                results.append(data)
                time.sleep(3)
            except Exception as e:
                results.append({"url": url, "error": str(e)})

        with open(output_file, "w") as f:
            json.dump(results, f, indent=2)
        print(f"Tracked {len(results)} products → {output_file}")
        return results

    def _has_captcha(self, html):
        return any(tag in html.lower() for tag in [
            'data-sitekey', 'g-recaptcha', 'cf-turnstile', 'captcha',
        ])

    def _solve_and_retry(self, html, url):
        match = re.search(r'data-sitekey="([^"]+)"', html)
        if not match:
            return self.session.get(url)

        sitekey = match.group(1)
        if 'cf-turnstile' in html:
            token = solve_captcha("turnstile", sitekey, url)
            return self.session.post(url, data={"cf-turnstile-response": token})
        else:
            token = solve_captcha("userrecaptcha", sitekey, url)
            return self.session.post(url, data={"g-recaptcha-response": token})

    def _text(self, soup, selector):
        el = soup.select_one(selector)
        return el.get_text(strip=True) if el else ""

    def _check_stock(self, soup):
        stock_el = soup.select_one("#availability, .stock-status")
        if stock_el:
            text = stock_el.get_text(strip=True).lower()
            return "in stock" in text or "available" in text
        return None

    def _detect_changes(self, prev, current):
        changes = []
        if prev["price"] != current["price"]:
            changes.append(f"Price: {prev['price']} → {current['price']}")
        if prev["in_stock"] != current["in_stock"]:
            status = "In Stock" if current["in_stock"] else "Out of Stock"
            changes.append(f"Stock: → {status}")
        return changes

    def _alert(self, title, changes):
        print(f"ALERT [{title}]: {', '.join(changes)}")


# Usage
monitor = RetailMonitor(
    proxy="http://user:pass@residential.proxy.com:5000"
)

products = [
    "https://store.example.com/product/abc123",
    "https://store.example.com/product/def456",
    "https://store.example.com/product/ghi789",
]

# One-time price check
results = monitor.track_prices(products)

# Or continuous monitoring (every 30 min)
# monitor.monitor_products(products, interval_sec=1800)

Le time.sleep(3) espace les requêtes ; le dictionnaire history transforme la collecte en alertes.


Balayer une catégorie entière

Pour cartographier une gamme complète, parcourez les pages de listing et arrêtez-vous dès qu'une page ne renvoie plus d'articles.

def scan_category(base_url, category, max_pages=20):
    """Scan an entire product category for stock status."""
    monitor = RetailMonitor(
        proxy="http://user:pass@residential.proxy.com:5000"
    )

    all_products = []
    for page in range(1, max_pages + 1):
        url = f"{base_url}/{category}?page={page}"
        resp = monitor.session.get(url, timeout=30)

        if monitor._has_captcha(resp.text):
            resp = monitor._solve_and_retry(resp.text, url)

        soup = BeautifulSoup(resp.text, "html.parser")
        items = soup.select(".product-card, .s-result-item")

        if not items:
            break

        for item in items:
            all_products.append({
                "name": monitor._text(item, ".product-name, .a-text-normal"),
                "price": monitor._text(item, ".price, .a-price"),
                "stock": monitor._text(item, ".stock, .a-color-success"),
                "url": item.select_one("a")["href"] if item.select_one("a") else "",
            })

        time.sleep(3)

    return all_products

Une page de listing affiche souvent des prix arrondis : traitez ce balayage comme une découverte de références, puis repassez sur les fiches qui vous intéressent.


Comparer les prix entre enseignes

Chaque enseigne reçoit ici son propre proxy : une IP signalée sur un site ne contamine pas les autres collectes.

def compare_product_across_stores(product_name, stores):
    """Compare prices across retailers for the same product."""
    results = []

    for store in stores:
        monitor = RetailMonitor(proxy=store.get("proxy"))
        search_url = f"{store['base_url']}/search?q={product_name}"

        try:
            resp = monitor.session.get(search_url, timeout=30)
            if monitor._has_captcha(resp.text):
                resp = monitor._solve_and_retry(resp.text, search_url)

            soup = BeautifulSoup(resp.text, "html.parser")
            first_result = soup.select_one(".product-card, .s-result-item")

            if first_result:
                results.append({
                    "store": store["name"],
                    "price": monitor._text(first_result, ".price"),
                    "in_stock": "in stock" in first_result.get_text().lower(),
                })
        except Exception as e:
            results.append({"store": store["name"], "error": str(e)})

        time.sleep(5)

    results.sort(key=lambda x: x.get("price", "zzzz"))
    return results

Attention au tri final : il compare des chaînes, pas des nombres. Normalisez le prix en nombre avant tout classement, sinon « 129,00 » passera devant « 9,99 ».


Dimensionner vos threads CaptchaAI

La facturation porte sur le nombre de threads simultanés, pas sur le nombre de résolutions : chaque plan inclut des résolutions illimitées par thread, sans plafond journalier ni surcoût par type de CAPTCHA. Un cycle de 1 000 références espacées de 3 secondes dure environ 50 minutes et ne déclenche un défi que sur une fraction des pages : BASIC ($15/mois, 5 threads) suffit. Passez à STANDARD ($30/mois, 15 threads) si vous parallélisez plusieurs enseignes, à ADVANCE ($90/mois, 50 threads) pour les balayages de catégorie massifs. Facturation en dollars US.


Dépannage

Problème Cause probable Correctif
Un défi sur chaque fiche produit IP signalée ou débit dépassé Augmentez le délai, activez la rotation des proxys
Prix vide ou erroné Tarification rendue en JavaScript Passez sur Selenium ou Playwright pour cette enseigne
Page de vérification du trafic En-têtes incohérents Alignez User-Agent, Accept-Language et proxy résidentiel
Stock « indisponible » à tort Disponibilité géo-restreinte Alignez la région du proxy sur le marché ciblé
Champs soudainement vides Structure HTML modifiée Mettez à jour les sélecteurs CSS

FAQ

Pourquoi le CAPTCHA apparaît-il sur chaque requête alors qu'hier tout passait ?

Le plus souvent parce que votre IP de sortie a été signalée, ou parce que le site a resserré son seuil de débit. Ralentissez de moitié pendant 24 heures et changez de pool de proxys résidentiels.

Quel plan CaptchaAI faut-il pour surveiller 1 000 références ?

BASIC ($15/mois, 5 threads) couvre une collecte séquentielle. Le besoin en threads augmente avec le parallélisme, pas avec le nombre de références suivies.

Faut-il un navigateur headless ou de simples requêtes HTTP ?

Commencez par requests : c'est nettement plus rapide et moins gourmand. Réservez le navigateur headless aux enseignes dont le prix est injecté en JavaScript.

CaptchaAI prend-il en charge hCaptcha ?

Non — hCaptcha n'est pas pris en charge, pas plus que FunCaptcha (Arkose Labs), et GeeTest v4 est seulement annoncé comme à venir. Côté couvert : reCAPTCHA v2 et v3, Cloudflare Turnstile et Challenge, GeeTest v3, CAPTCHA image et grilles d'images.

Le prix récupéré est vide alors que la page s'affiche : que faire ?

Vérifiez d'abord que la réponse n'est pas une page de vérification déguisée en HTTP 200. Si le HTML est bien celui de la fiche, le sélecteur CSS est obsolète.


Guides connexes


Prêt à lancer un premier cycle ? Récupérez votre clé API CaptchaAI et branchez-la sur le script ci-dessus.

Les commentaires sont désactivés pour cet article.