Use Cases

Surveiller des sites d'enchères malgré les CAPTCHA

Pour surveiller un site d'enchères sans blocage, la résolution du reCAPTCHA v2 doit vivre à l'intérieur de votre boucle de monitoring, pas à côté. Dès que votre script enchaîne les recherches ou recharge une fiche de lot toutes les quelques secondes, le défi s'affiche — et une surveillance qui s'interrompt à chaque challenge ne vous apprend rien sur l'évolution des prix.

L'enjeu n'est donc pas de résoudre un CAPTCHA isolé, mais de tenir une surveillance stable dans la durée : session persistante, rythme de requête crédible et nouvelle tentative propre juste avant la soumission du formulaire.

Où les CAPTCHA se déclenchent sur un site d'enchères

Avant d'écrire la moindre ligne, repérez les points où le défi va tomber. Ce sont presque toujours les mêmes actions qui déclenchent reCAPTCHA v2 : une recherche répétée, une fiche de lot consultée en boucle, ou un historique rechargé trop souvent depuis la même IP.

Action Type de CAPTCHA Déclencheur courant
Recherche d'annonces reCAPTCHA v2 Requêtes rapides et répétitives
Fiche d'un lot reCAPTCHA v2 Grand volume depuis une même IP
Historique des enchères reCAPTCHA v2 Rechargements répétés de la même page
Navigation par catégorie Cloudflare Turnstile Vitesse de navigation trop mécanique
Pages d'alerte reCAPTCHA v2 Rafraîchissements fréquents

Résoudre reCAPTCHA v2 dans votre boucle de monitoring

La classe ci-dessous encapsule tout le cycle : détection du défi, extraction du sitekey, envoi à CaptchaAI, interrogation du résultat, puis renvoi du formulaire avec le token g-recaptcha-response. Une seule requests.Session() conserve les cookies d'une vérification à l'autre, ce qui évite de redéclencher un CAPTCHA à chaque appel.

import requests
import time
import re
from datetime import datetime

class AuctionMonitor:
    def __init__(self, api_key):
        self.api_key = api_key
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
        })

    def search_listings(self, auction_url, query, category=None):
        """Search auction listings, solving CAPTCHAs when triggered."""
        params = {"q": query}
        if category:
            params["category"] = category

        response = self.session.get(
            f"{auction_url}/search", params=params
        )

        if self._has_captcha(response.text):
            site_key = self._extract_site_key(response.text)
            token = self._solve_recaptcha(site_key, f"{auction_url}/search")
            response = self.session.post(
                f"{auction_url}/search",
                data={**params, "g-recaptcha-response": token}
            )

        return self._parse_listings(response.text)

    def monitor_listing(self, auction_url, listing_id):
        """Get current bid and listing details."""
        url = f"{auction_url}/item/{listing_id}"
        response = self.session.get(url)

        if self._has_captcha(response.text):
            site_key = self._extract_site_key(response.text)
            token = self._solve_recaptcha(site_key, url)
            response = self.session.post(url, data={
                "g-recaptcha-response": token
            })

        return self._parse_listing_detail(response.text)

    def track_bids(self, auction_url, listing_ids, interval=60):
        """Track bid changes across multiple listings."""
        history = {lid: [] for lid in listing_ids}

        while True:
            for listing_id in listing_ids:
                try:
                    detail = self.monitor_listing(auction_url, listing_id)
                    previous = history[listing_id]

                    if previous and detail["current_bid"] != previous[-1]["current_bid"]:
                        print(f"Bid change on {listing_id}: "
                              f"${previous[-1]['current_bid']} → ${detail['current_bid']}")

                    history[listing_id].append(detail)
                except Exception as e:
                    print(f"Error checking {listing_id}: {e}")

            time.sleep(interval)

    def _has_captcha(self, html):
        return "g-recaptcha" in html or "recaptcha" in html.lower()

    def _extract_site_key(self, html):
        match = re.search(r'data-sitekey="([^"]+)"', html)
        if match:
            return match.group(1)
        match = re.search(r"sitekey['\"]?\s*[:=]\s*['\"]([^'\"]+)", html)
        if match:
            return match.group(1)
        raise ValueError("Could not find reCAPTCHA site key")

    def _solve_recaptcha(self, site_key, page_url):
        resp = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": self.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": self.api_key,
                "action": "get",
                "id": task_id,
                "json": 1
            })
            data = result.json()
            if data["status"] == 1:
                return data["request"]

        raise TimeoutError("reCAPTCHA solve timed out")

    def _parse_listings(self, html):
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(html, "html.parser")

        def text_of(node):
            return node.get_text(strip=True) if node else None

        listings = []
        for item in soup.select(".listing-item, .auction-item"):
            link = item.select_one("a")
            listings.append({
                "title": text_of(item.select_one(".title")),
                "current_bid": text_of(item.select_one(".price, .bid")),
                "time_left": text_of(item.select_one(".time-left")),
                "url": link.get("href") if link else None,
            })
        return listings

    def _parse_listing_detail(self, html):
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(html, "html.parser")

        def text_of(node):
            return node.get_text(strip=True) if node else None

        return {
            "title": text_of(soup.select_one("h1, .item-title")),
            "current_bid": text_of(soup.select_one(".current-bid, .price")),
            "bid_count": text_of(soup.select_one(".bid-count")),
            "time_left": text_of(soup.select_one(".time-remaining")),
            "checked_at": datetime.now().isoformat(),
        }

# Usage
monitor = AuctionMonitor("YOUR_API_KEY")
listings = monitor.search_listings(
    "https://auctions.example.com",
    "vintage electronics",
    category="collectibles"
)

Alerte de prix et suivi des offres en JavaScript

Côté navigateur ou Node.js, la même logique devient une watchlist : chaque lot porte un prix plafond, et une alerte se déclenche quand l'offre s'en approche. La résolution reCAPTCHA vit dans solveAndFetch, appelée seulement si la page renvoie un g-recaptcha.

class AuctionTracker {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.watchList = new Map();
  }

  addWatch(listingId, url, maxPrice) {
    this.watchList.set(listingId, { url, maxPrice, history: [] });
  }

  async checkAll() {
    const alerts = [];

    for (const [id, watch] of this.watchList) {
      try {
        const detail = await this.fetchListing(watch.url);
        watch.history.push(detail);

        const price = parseFloat(detail.currentBid.replace(/[^0-9.]/g, ''));
        if (price >= watch.maxPrice * 0.9) {
          alerts.push({
            listing: id,
            price,
            threshold: watch.maxPrice,
            message: `Price approaching limit: $${price} / $${watch.maxPrice}`
          });
        }
      } catch (error) {
        alerts.push({ listing: id, error: error.message });
      }
    }

    return alerts;
  }

  async fetchListing(url) {
    const response = await fetch(url);
    const html = await response.text();

    if (html.includes('g-recaptcha')) {
      return this.solveAndFetch(url, html);
    }

    return this.parseDetail(html);
  }

  async solveAndFetch(url, html) {
    const siteKeyMatch = html.match(/data-sitekey="([^"]+)"/);
    if (!siteKeyMatch) throw new Error('No reCAPTCHA site key found');

    const submitResp = await fetch('https://ocr.captchaai.com/in.php', {
      method: 'POST',
      body: new URLSearchParams({
        key: this.apiKey,
        method: 'userrecaptcha',
        googlekey: siteKeyMatch[1],
        pageurl: url,
        json: '1'
      })
    });

    const { request: taskId } = await submitResp.json();

    for (let i = 0; i < 60; i++) {
      await new Promise(r => setTimeout(r, 3000));
      const result = await fetch(
        `https://ocr.captchaai.com/res.php?key=${this.apiKey}&action=get&id=${taskId}&json=1`
      );
      const data = await result.json();
      if (data.status === 1) {
        // Resubmit with token
        const response = await fetch(url, {
          method: 'POST',
          body: new URLSearchParams({ 'g-recaptcha-response': data.request })
        });
        return this.parseDetail(await response.text());
      }
    }

    throw new Error('reCAPTCHA solve timed out');
  }

  parseDetail(html) {
    // Parse auction listing details from HTML
    return {
      currentBid: html.match(/current.?bid[^>]*>([^<]+)/i)?.[1]?.trim(),
      bidCount: html.match(/(\d+)\s*bids?/i)?.[1],
      timeLeft: html.match(/time.?(?:left|remaining)[^>]*>([^<]+)/i)?.[1]?.trim(),
      checkedAt: new Date().toISOString()
    };
  }
}

// Usage
const tracker = new AuctionTracker('YOUR_API_KEY');
tracker.addWatch('item-123', 'https://auctions.example.com/item/123', 500);
tracker.addWatch('item-456', 'https://auctions.example.com/item/456', 200);
const alerts = await tracker.checkAll();

Le seuil à 90 % du prix plafond (maxPrice * 0.9) vous laisse le temps de réagir avant que le lot ne dépasse votre limite ; resserrez-le à l'approche de la clôture.

Choisir la bonne fréquence de contrôle

Un moniteur d'enchères arbitre entre fraîcheur des données et discrétion : plus vous contrôlez souvent, plus vous captez tôt un changement d'offre, mais plus vous exposez votre trafic à un rythme mécanique et donc à davantage de CAPTCHA.

Les chiffres ci-dessous reposent sur des mesures observées et des retours d'utilisateurs. Les résultats varient selon l'environnement, le volume et le moment de la journée.

Fréquence de contrôle Cas d'usage Taux de CAPTCHA attendu
Toutes les 30 secondes Fin d'enchère très sensible Élevé, avec proxys quasi obligatoires
Toutes les 5 minutes Suivi actif Modéré
Toutes les 15 minutes Watchlist classique Faible
Toutes les heures Veille longue durée Minimal

Réduire le nombre de CAPTCHA rencontrés

Le meilleur CAPTCHA reste celui que vous ne déclenchez pas : avant d'augmenter votre débit de résolution, réduisez d'abord la fréquence à laquelle les défis apparaissent.

Technique Effet
Réutiliser les cookies de session Conserve un état plus cohérent
Faire tourner des proxys résidentiels Répartit les requêtes sur plusieurs IP
Randomiser les intervalles Évite un rythme trop prévisible
Utiliser des comptes authentifiés Réduit parfois le niveau de suspicion

Surveillance d'enchères et RGPD

Si vous surveillez des places de marché francophones — d'Interencheres à Drouot Digital — pensez à la conformité en amont. Une fiche de lot mélange souvent des données publiques (prix, montant de l'offre, temps restant) et des données personnelles (pseudo ou coordonnées d'un enchérisseur). Ne collectez que ce dont vous avez besoin pour votre suivi de prix, et n'archivez pas les identifiants d'enchérisseurs si votre objectif est l'évolution du montant.

En pratique : limitez-vous à current_bid, time_left et un horodatage, et vérifiez les conditions d'utilisation de chaque plateforme. Le RGPD n'interdit pas la veille tarifaire, mais attend une collecte proportionnée.

Dimensionner votre plan CaptchaAI

CaptchaAI facture au thread simultané, pas à la résolution : un thread traite un défi à la fois, puis passe au suivant sans limite de volume dans le mois. Un reCAPTCHA v2 se résout généralement en moins de 60 secondes avec un taux de réussite élevé, ce qui fixe le débit par thread.

Pour une watchlist contrôlée toutes les 5 à 15 minutes, le plan BASIC ($15/mois, 5 threads) traite les défis en parallèle sans file d'attente. Pour surveiller des dizaines de lots pendant une clôture serrée, où plusieurs pages déclenchent un CAPTCHA en même temps, le plan STANDARD ($30/mois, 15 threads) absorbe mieux les pics. La facturation reste en dollars US, quel que soit le type de CAPTCHA.

Dépannage

Problème Cause probable Correctif
CAPTCHA à chaque requête Session non persistante Réutilisez requests.Session()
Token reCAPTCHA rejeté Token expiré (durée de vie ~2 min) Résolvez juste avant l'envoi du formulaire
La page montre 0 résultat Le défi filtre la réponse sans bruit Recherchez la présence d'éléments CAPTCHA cachés
IP bloquée après plusieurs contrôles Rate limit dépassé Faites tourner les proxys et espacez les contrôles

FAQ

Combien de threads faut-il pour surveiller plusieurs enchères en parallèle ?

Comptez un thread par défi résolu simultanément. Une watchlist contrôlée toutes les 5 à 15 minutes tient sur le plan BASIC ($15/mois, 5 threads) ; pour une clôture serrée où de nombreux lots déclenchent un CAPTCHA en même temps, le plan STANDARD ($30/mois, 15 threads) évite la file d'attente.

reCAPTCHA v2 est-il le seul CAPTCHA sur les sites d'enchères ?

Non. La recherche et les fiches de lot affichent surtout reCAPTCHA v2, mais la navigation par catégorie déclenche parfois Cloudflare Turnstile. Prévoyez de détecter les deux familles sur un même domaine.

La surveillance d'enchères est-elle conforme au RGPD ?

Elle peut l'être si votre collecte reste proportionnée. Limitez-vous aux données de prix et d'horodatage utiles à votre suivi, évitez de stocker les identifiants d'enchérisseurs, et vérifiez les conditions d'utilisation de la plateforme. Ce guide ne constitue pas un conseil juridique.

Pourquoi mon token est-il refusé au moment de la soumission ?

Le plus souvent, il a expiré : un token reCAPTCHA v2 ne vit que deux minutes environ. Déclenchez la résolution juste avant d'envoyer le formulaire, plutôt que de résoudre à l'ouverture puis d'attendre.

Articles connexes

Prochaines étapes

Pour suivre les prix et les offres sur plusieurs lots sans interruption, récupérez votre clé API CaptchaAI et intégrez la résolution reCAPTCHA dans votre logique de monitoring, plutôt que de la greffer après coup.

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