Casos de Uso

Monitoreo de la cadena de suministro con manejo CAPTCHA

Cuando un portal de proveedores responde con un reCAPTCHA v2 o un Cloudflare Turnstile, tu barrido de inventario se detiene en seco. La solución es delegar ese desafío a una API de resolución como CaptchaAI: el CAPTCHA se resuelve en segundo plano, tu script recibe el token y sigue leyendo stock, tarifas y plazos sin intervención manual.

Una consultora logística en México, por ejemplo, consolida cada día precios de fabricantes, tarifas de transportistas y horarios del puerto de Manzanillo, cada uno tras un desafío distinto. Si haces scraping de portales de terceros, respeta sus términos de servicio y la normativa de protección de datos aplicable.


Dónde aparecen los CAPTCHA en la cadena de suministro

No todas las fuentes usan el mismo desafío. Este es el mapa típico de lo que vas a encontrar:

Tipo de fuente Tipo de CAPTCHA Datos Frecuencia
Portales de proveedores reCAPTCHA v2 Inventario, precios, plazos de entrega Diaria
Transportistas de envío Cloudflare Turnstile Seguimiento, tarifas, ETA de entrega Por hora
Catálogos de fabricantes CAPTCHA de imagen Especificaciones del producto, MOQ Semanal
Portales aduaneros reCAPTCHA v2 Tipos de derechos, códigos arancelarios Diaria
Autoridades portuarias CAPTCHA de imagen Horarios de buques, congestión portuaria Cada 6 horas
Bolsas de materias primas reCAPTCHA v3 Precios al contado, futuros En tiempo real

CaptchaAI resuelve los tipos que dominan la tabla: reCAPTCHA v2 y v3, Cloudflare Turnstile y CAPTCHA de imagen (OCR).


Monitor unificado para varios proveedores

La clase SupplyChainMonitor recorre la lista de proveedores, detecta si la respuesta trae un CAPTCHA y elige el método de resolución según el tipo declarado. Cada proveedor se procesa aislado: si uno falla, la excepción se captura y el resto del barrido continúa. El campo captcha_type le dice al monitor qué método usar, así que agregar una fuente es solo añadir una entrada a la lista.

import requests
import time
import re
import json
import base64
from datetime import datetime

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


def solve_recaptcha(sitekey, pageurl):
    resp = requests.post(f"{CAPTCHAAI_URL}/in.php", data={
        "key": CAPTCHAAI_KEY, "method": "userrecaptcha",
        "googlekey": sitekey, "pageurl": pageurl, "json": 1,
    })
    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,
        })
        data = result.json()
        if data["request"] != "CAPCHA_NOT_READY":
            return data["request"]
    raise TimeoutError("Timeout")


def solve_turnstile(sitekey, pageurl):
    resp = requests.post(f"{CAPTCHAAI_URL}/in.php", data={
        "key": CAPTCHAAI_KEY, "method": "turnstile",
        "sitekey": sitekey, "pageurl": pageurl, "json": 1,
    })
    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,
        })
        data = result.json()
        if data["request"] != "CAPCHA_NOT_READY":
            return data["request"]
    raise TimeoutError("Timeout")


def solve_image(image_bytes):
    img_b64 = base64.b64encode(image_bytes).decode()
    resp = requests.post(f"{CAPTCHAAI_URL}/in.php", data={
        "key": CAPTCHAAI_KEY, "method": "base64",
        "body": img_b64, "json": 1,
    })
    task_id = resp.json()["request"]
    for _ in range(20):
        time.sleep(3)
        result = requests.get(f"{CAPTCHAAI_URL}/res.php", params={
            "key": CAPTCHAAI_KEY, "action": "get",
            "id": task_id, "json": 1,
        })
        data = result.json()
        if data["request"] != "CAPCHA_NOT_READY":
            return data["request"]
    raise TimeoutError("Timeout")


class SupplyChainMonitor:
    def __init__(self, suppliers, proxy=None):
        self.suppliers = suppliers
        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",
        })

    def check_all(self):
        """Check inventory and pricing across all suppliers."""
        report = {
            "timestamp": datetime.now().isoformat(),
            "suppliers": {},
        }

        for supplier in self.suppliers:
            try:
                data = self._check_supplier(supplier)
                report["suppliers"][supplier["name"]] = {
                    "status": "success",
                    "data": data,
                }
            except Exception as e:
                report["suppliers"][supplier["name"]] = {
                    "status": "error",
                    "error": str(e),
                }
            time.sleep(3)

        return report

    def _check_supplier(self, supplier):
        url = supplier["url"]
        resp = self.session.get(url, timeout=30)

        # Handle CAPTCHA based on type
        captcha_type = supplier.get("captcha_type")
        if captcha_type and self._has_captcha(resp.text):
            resp = self._solve_captcha(resp, url, supplier)

        from bs4 import BeautifulSoup
        soup = BeautifulSoup(resp.text, "html.parser")

        return {
            "products": self._extract_inventory(soup),
            "last_updated": self._extract_date(soup),
        }

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

    def _solve_captcha(self, resp, url, supplier):
        captcha_type = supplier.get("captcha_type", "recaptcha")
        sitekey = supplier.get("sitekey", "")

        if not sitekey:
            match = re.search(r'data-sitekey="([^"]+)"', resp.text)
            sitekey = match.group(1) if match else ""

        if captcha_type == "turnstile":
            token = solve_turnstile(sitekey, url)
            return self.session.post(url, data={"cf-turnstile-response": token})
        elif captcha_type == "image":
            match = re.search(r'src="(/captcha[^"]+)"', resp.text)
            if match:
                img_resp = self.session.get(url.rstrip("/") + match.group(1))
                answer = solve_image(img_resp.content)
                return self.session.post(url, data={"captcha": answer})
        else:
            token = solve_recaptcha(sitekey, url)
            return self.session.post(url, data={"g-recaptcha-response": token})

        return resp

    def _extract_inventory(self, soup):
        items = []
        for row in soup.select("table.inventory tr, .product-row"):
            cols = row.select("td, .col")
            if len(cols) >= 3:
                items.append({
                    "sku": cols[0].get_text(strip=True),
                    "stock": cols[1].get_text(strip=True),
                    "price": cols[2].get_text(strip=True),
                })
        return items

    def _extract_date(self, soup):
        date_el = soup.select_one(".last-updated, .update-time")
        return date_el.get_text(strip=True) if date_el else ""


# Configure suppliers
suppliers = [
    {
        "name": "Supplier A",
        "url": "https://supplier-a.example.com/inventory",
        "captcha_type": "recaptcha",
        "sitekey": "6Lc_xxxxxxx",
    },
    {
        "name": "Carrier B",
        "url": "https://carrier-b.example.com/rates",
        "captcha_type": "turnstile",
        "sitekey": "0x4AAAAAAA_xxx",
    },
    {
        "name": "Manufacturer C",
        "url": "https://manufacturer-c.example.com/catalog",
        "captcha_type": "image",
    },
]

monitor = SupplyChainMonitor(
    suppliers=suppliers,
    proxy="http://user:pass@residential.proxy.com:5000",
)
report = monitor.check_all()
print(json.dumps(report, indent=2))

Qué hacer cuando un proveedor falla

Un barrido que vigila decenas de fuentes tiene que degradar con elegancia: en vez de abortar el reporte porque un portal cambió el DOM, marca cada resultado con su estado. Así tu panel de control siempre muestra algo útil —un dato fresco, uno parcial o una alerta— y nunca una página en blanco.

Estado del proveedor Acción Resultado esperado
CAPTCHA resuelto y parser estable Publicar inventario completo El proveedor entra como success
CAPTCHA resuelto pero el DOM cambió Guardar el HTML y marcar partial No pierdes el resto del barrido multiproveedor
CAPTCHA sin resolver tras los reintentos Conservar el último dato conocido y alertar Evita huecos totales en el reporte diario

Seguimiento de tarifas de envío

Los transportistas suelen esconder las tarifas tras un Cloudflare Turnstile. ShippingRateTracker pide la cotización, detecta el sitekey, resuelve el Turnstile y reenvía el formulario con el token en cf-turnstile-response:

class ShippingRateTracker:
    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",
        })

    def get_rates(self, carrier_url, origin, destination, weight):
        """Fetch shipping rates, handling Turnstile CAPTCHA."""
        resp = self.session.get(carrier_url, timeout=30)

        sitekey_match = re.search(r'data-sitekey="([^"]+)"', resp.text)
        if sitekey_match:
            token = solve_turnstile(sitekey_match.group(1), carrier_url)
            resp = self.session.post(carrier_url, data={
                "origin": origin,
                "destination": destination,
                "weight": weight,
                "cf-turnstile-response": token,
            })

        if resp.status_code == 200:
            return resp.json().get("rates", [])
        return []

Alertas ante cambios de stock

Leer los datos es la mitad del trabajo; el valor está en enterarte cuando algo cambia. monitor_with_alerts corre en bucle, compara el stock de cada SKU con la lectura previa y avisa cuando cruza el umbral a la baja:

def monitor_with_alerts(monitor, alert_thresholds, check_interval=3600):
    """Continuously monitor and alert on inventory changes."""
    previous_data = {}

    while True:
        report = monitor.check_all()

        for supplier, info in report["suppliers"].items():
            if info["status"] != "success":
                continue

            for product in info["data"].get("products", []):
                sku = product["sku"]
                stock = product.get("stock", "")

                # Parse stock level
                try:
                    stock_qty = int(re.sub(r'\D', '', stock))
                except ValueError:
                    continue

                key = f"{supplier}:{sku}"
                prev_qty = previous_data.get(key, stock_qty)

                threshold = alert_thresholds.get(sku, 10)
                if stock_qty < threshold and prev_qty >= threshold:
                    print(f"ALERT: {supplier} - {sku} dropped to {stock_qty}")

                previous_data[key] = stock_qty

        time.sleep(check_interval)

Cambia el print por un webhook a Slack y tendrás un vigilante de reposición que trabaja solo. Baja el check_interval de una hora solo para componentes críticos: sondear demasiado seguido dispara más CAPTCHA.


Diagnóstico de problemas frecuentes

Cuando un barrido empieza a devolver datos incompletos, casi siempre es una de estas cinco causas:

Problema Causa Solución
Cambió el diseño de la página del proveedor Rediseño del sitio Actualiza los selectores CSS
Aparece un CAPTCHA en cada consulta Sondeas con demasiada frecuencia Aumenta el intervalo entre revisiones
La sesión caduca a mitad de la consulta Tiempo de espera del portal Usa una sesión fija y consulta más rápido
Faltan datos de tarifas El portal exige iniciar sesión Añade el paso de autenticación
Se muestran precios incorrectos Precios según la geografía Haz coincidir la salida de red con el mercado

Preguntas frecuentes

¿Qué plan de CaptchaAI necesito para vigilar muchos proveedores a la vez?

Como CaptchaAI factura por thread concurrente (no por resolución, con resoluciones ilimitadas por thread), depende de cuántas consultas quieras tener en vuelo a la vez. Para decenas de portales suele bastar ADVANCE ($90/mes, 50 threads); para cientos de fuentes casi en tiempo real, PREMIUM ($170/mes, 100 threads).

¿CaptchaAI resuelve el CAPTCHA de los portales aduaneros y de proveedores?

Sí para los tipos habituales aquí: reCAPTCHA v2 y v3, Cloudflare Turnstile y CAPTCHA de imagen (OCR). No resuelve hCaptcha ni FunCaptcha (Arkose Labs); para esas fuentes necesitarás otra vía.

¿Cómo evito que el sitio me bloquee al revisar tan seguido?

Espacia las revisiones según la volatilidad del dato: diaria para inventario general, por hora solo para lo crítico. Distribuye las solicitudes, reutiliza la sesión mientras siga viva y usa una salida de red autorizada.

Depende del portal. Extrae solo datos a los que tu organización tiene acceso legítimo y respeta los términos de servicio y la normativa aplicable (GDPR y LOPDGDD en España, LFPDPPP en México). No es asesoría legal: ante la duda, consúltalo con tu equipo jurídico.


Guías relacionadas


Mantén tu cadena de suministro siempre visible: obtén tu clave de CaptchaAI y automatiza la recopilación de datos en todos tus portales de proveedores.

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