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.
¿Es legal monitorear estos portales de la cadena de suministro?
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
- Salida de red autorizada para resolver CAPTCHA
- Sesiones fijas frente a rotativas
- Persistencia de sesión del navegador
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.