Casos de Uso

Manejo de CAPTCHA para el monitoreo del sitio de subastas

¿Quieres seguir el precio de un listado sin que un reCAPTCHA v2 detenga tu scraper a mitad de la consulta? La respuesta corta: resuelve el CAPTCHA por API en cuanto aparece, inyecta el token en la misma solicitud y sigue adelante. Las plataformas de subastas —desde casas de pujas internacionales hasta marketplaces tipo MercadoLibre— protegen la búsqueda, las páginas de detalle y el historial de pujas con reCAPTCHA v2, y lo disparan justo cuando más molesta: durante consultas rápidas y recargas frecuentes. Esta guía muestra cómo mantener un seguimiento estable resolviendo esos desafíos de forma automática, con ejemplos listos para copiar en Python y JavaScript.

Por qué aparecen los CAPTCHA en los sitios de subastas

Antes de escribir código conviene saber dónde salta el desafío. En la mayoría de las plataformas, el reCAPTCHA v2 se activa por el ritmo y el volumen de las solicitudes, no por la acción en sí. Estos son los puntos más habituales:

Acción Tipo de CAPTCHA Qué lo dispara
Buscar o navegar listados reCAPTCHA v2 Búsquedas rápidas y secuenciales
Ver el detalle de un listado reCAPTCHA v2 Mucho volumen desde la misma IP
Consultar el historial de pujas reCAPTCHA v2 Recargas repetidas de la página de detalle
Navegar por categorías Cloudflare Turnstile Velocidad de navegación propia de un bot
Páginas de alerta de precio reCAPTCHA v2 Actualizaciones frecuentes

Ten en cuenta que un mismo sitio puede combinar reCAPTCHA v2 en la búsqueda con Cloudflare Turnstile en la navegación por categorías. Conviene detectar ambos y resolver el que corresponda en cada paso.

Monitorear subastas resolviendo el reCAPTCHA v2 en Python

El patrón es sencillo: haces la solicitud normal, compruebas si la respuesta trae un CAPTCHA, extraes el sitekey, lo envías a CaptchaAI y reenvías la petición con el token g-recaptcha-response. La clase siguiente encapsula ese flujo y añade un seguimiento continuo de las pujas.

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_or_none(node):
            return node.text.strip() if node and node.text else None

        def attr_or_none(node, attr):
            return node.get(attr) if node else None

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

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

        def text_or_none(node):
            return node.text.strip() if node and node.text else None

        return {
        "title": text_or_none(soup.select_one("h1, .item-title")),
        "current_bid": text_or_none(soup.select_one(".current-bid, .price")),
        "bid_count": text_or_none(soup.select_one(".bid-count")),
        "time_left": text_or_none(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"
)

El token de reCAPTCHA v2 tiene una vida útil de unos dos minutos, así que resuélvelo justo antes de reenviar la solicitud protegida. Reutilizar la misma requests.Session() conserva las cookies y reduce cuántas veces vuelve a aparecer el desafío.

Sistema de alertas de precio en JavaScript

Si prefieres Node.js o quieres integrar el monitor en un backend JavaScript, el mismo patrón funciona con fetch. Esta versión mantiene una lista de vigilancia y avisa cuando una puja se acerca al límite que definiste.

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();

Cada cuánto conviene consultar los listados

La frecuencia de consulta define casi todo: cuanto más rápido sondeas, más CAPTCHA verás. Elige el intervalo según la urgencia real del caso.

Frecuencia de consulta Caso de uso CAPTCHA esperados
Cada 30 segundos Pujas de último minuto Muchos: reparte la carga entre varias salidas
Cada 5 minutos Seguimiento activo de una subasta Moderados
Cada 15 minutos Vigilancia de una lista de seguimiento Pocos
Cada hora Investigación de precios a largo plazo Mínimos

Aquí es donde el modelo de precios importa. CaptchaAI factura por threads concurrentes con resoluciones ilimitadas, no por CAPTCHA resuelto: un thread es un desafío en curso y, en cuanto termina, queda libre para el siguiente. Para una agencia o un freelance que factura en una moneda local volátil, un costo mensual predecible en USD suele encajar mejor que el pago por resolución. Si sigues unas pocas subastas, el plan BASIC ($15/mes, 5 threads) basta; cuando vigilas cientos de listados en paralelo, STANDARD ($30/mes, 15 threads) o ADVANCE ($90/mes, 50 threads) amplían los threads disponibles.

Cómo reducir la frecuencia de CAPTCHA

Menos desafíos significan menos latencia y menos gasto de threads. Estas técnicas atacan el origen del problema: el patrón de las solicitudes.

Técnica Efecto
Reutilizar las cookies de sesión Conserva el estado autenticado
Repartir la carga entre salidas de red autorizadas Distribuye las solicitudes entre varias IP
Aleatorizar los intervalos entre solicitudes Evita patrones periódicos fáciles de detectar
Usar cuentas autenticadas Reduce el umbral que dispara el CAPTCHA

Resolución de problemas frecuentes

Problema Causa Solución
CAPTCHA en cada solicitud No se conserva la sesión Reutiliza requests.Session()
Token reCAPTCHA rechazado Token caducado (vive 2 minutos) Resuélvelo justo antes de enviar
La página de listados devuelve 0 resultados El CAPTCHA filtró los resultados en silencio Comprueba si hay elementos CAPTCHA ocultos
IP bloqueada tras muchos CAPTCHA Límite de solicitudes superado Reparte la carga y aumenta los intervalos

Preguntas frecuentes

¿Qué plan de CaptchaAI necesito para vigilar varias subastas a la vez?

Depende de cuántos listados sigas en paralelo. CaptchaAI factura por threads concurrentes con resoluciones ilimitadas: el plan BASIC ($15/mes, 5 threads) cubre un seguimiento modesto, y STANDARD ($30/mes, 15 threads) o ADVANCE ($90/mes, 50 threads) amplían los threads cuando rastreas cientos de listados a la vez.

¿Cómo reduzco los bloqueos de IP al rastrear un sitio de subastas?

Reutiliza la sesión, aleatoriza los intervalos entre solicitudes y reparte la carga entre varias salidas de red autorizadas. El bloqueo casi siempre viene del ritmo de las solicitudes, no de resolver el CAPTCHA.

¿CaptchaAI resuelve también el Cloudflare Turnstile que aparece al navegar por categorías?

Sí. Además del reCAPTCHA v2 de la búsqueda y las páginas de detalle, CaptchaAI resuelve Cloudflare Turnstile, así que puedes cubrir ambos desafíos con la misma integración.

Depende del sitio. Revisa siempre los términos de servicio de la plataforma y la normativa de protección de datos aplicable —por ejemplo, el RGPD y la LOPDGDD en España o la LFPDPPP en México— antes de automatizar el seguimiento.

Artículos relacionados

Empieza a seguir subastas sin interrupciones: crea tu cuenta en CaptchaAI y deja que los reCAPTCHA v2 se resuelvan solos mientras tu monitor trabaja.

Los comentarios están deshabilitados para este artículo.