¿Se puede mantener en marcha un recolector de listados inmobiliarios cuando el portal interpone un CAPTCHA cada pocas páginas? Sí, siempre que trates la verificación como un paso normal del pipeline y no como un error que rompe la ejecución. Un portal de vivienda protege sus listados porque los precios son su producto: en cuanto detecta un patrón automatizado, muestra un desafío.
Que el scraper muera el segundo día o siga alimentando tu panel de precios meses después depende de qué hace el código cuando aparece el widget. Aquí tienes qué protege cada tipo de portal, qué campos guardar, un recolector en Python que resuelve reCAPTCHA y Cloudflare Turnstile, y cómo dimensionar los threads.
Qué protección usa cada tipo de portal inmobiliario
No todos los sitios del sector defienden igual sus datos. Identifica primero con qué te vas a encontrar:
| Tipo de plataforma | Protección | Tipo CAPTCHA |
|---|---|---|
| Agregadores de la MLS | Cloudflare Challenge | Desafío completo + proxy |
| Portales tipo Zillow | reCAPTCHA v3 | Invisible, conductual |
| Directorios de agentes inmobiliarios | reCAPTCHA v2 | Casilla de verificación o invisible |
| Registros de impuestos a la propiedad | CAPTCHA de imagen | Reconocimiento de texto |
| Sitios de subasta | Cloudflare Turnstile | Desafío de widgets |
| Listados comerciales | reCAPTCHA v2 Enterprise | Verificación mejorada |
Los tiempos de resolución marcan el ritmo de la recolección. Según las cifras publicadas por CaptchaAI, un CAPTCHA de imagen se resuelve en menos de 0,5 s, reCAPTCHA v3 en menos de 4 s, Cloudflare Turnstile en menos de 10 s, Cloudflare Challenge en menos de 15 s y reCAPTCHA v2 en menos de 60 s, con una alta tasa de éxito en los tipos compatibles. Un agregador con Cloudflare Challenge exige, por tanto, más threads que un portal con reCAPTCHA v3 para cerrar la misma ronda nocturna.
Campos que conviene guardar en cada listado
Recolectar de más complica el cumplimiento; de menos, te obliga a repetir la extracción. La base mínima útil:
| Campo | Fuente | Para qué sirve |
|---|---|---|
| Precio publicado | Ficha de la propiedad | Valoración de mercado |
| Dirección o zona | Ficha de la propiedad | Geoanálisis por barrio |
| Habitaciones, baños y superficie | Detalles del inmueble | Comparables |
| Días en el mercado | Metadatos del listado | Velocidad de venta |
| Historial de precios | Registro de cambios | Análisis de tendencias |
| Impuesto o cargas asociadas | Registros públicos | Análisis de inversión |
| Cuotas de comunidad | Detalles del listado | Coste total de propiedad |
Guarda también la marca de tiempo: sin ella no puedes calcular deltas de precio ni detectar retiradas.
Recolector de listados en Python con resolución automática
import requests
import time
import re
import json
import csv
import os
from datetime import datetime
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
def solve_captcha(params):
params["key"] = API_KEY
resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
if not resp.text.startswith("OK|"):
raise Exception(f"Submit: {resp.text}")
task_id = resp.text.split("|")[1]
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get", "id": task_id,
})
if result.text == "CAPCHA_NOT_READY":
continue
if result.text.startswith("OK|"):
return result.text.split("|", 1)[1]
raise Exception(f"Solve: {result.text}")
raise TimeoutError()
class PropertyCollector:
def __init__(self):
self.session = requests.Session()
self.session.headers["User-Agent"] = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/120.0.0.0"
)
def fetch(self, url):
"""Fetch page with automatic CAPTCHA handling."""
resp = self.session.get(url)
# reCAPTCHA
match = re.search(
r'data-sitekey=["\']([A-Za-z0-9_-]+)["\']', resp.text
)
if match:
# Detect v3
is_v3 = "recaptcha/api.js?render=" in resp.text
params = {
"method": "userrecaptcha",
"googlekey": match.group(1),
"pageurl": url,
}
if is_v3:
params["version"] = "v3"
params["action"] = "search"
token = solve_captcha(params)
resp = self.session.post(url, data={
"g-recaptcha-response": token,
})
# Turnstile
if "cf-turnstile" in resp.text:
match = re.search(r'data-sitekey=["\']([^"\']+)', resp.text)
if match:
token = solve_captcha({
"method": "turnstile",
"sitekey": match.group(1),
"pageurl": url,
})
resp = self.session.post(url, data={
"cf-turnstile-response": token,
})
return resp.text
def collect_listings(self, urls):
"""Collect property listings from multiple pages."""
listings = []
for url in urls:
try:
html = self.fetch(url)
page_listings = self._parse_listings(html)
listings.extend(page_listings)
print(f" {len(page_listings)} listings from {url}")
time.sleep(3)
except Exception as e:
print(f" Error: {url} - {e}")
return listings
def _parse_listings(self, html):
"""Extract property data from HTML."""
listings = []
# Price extraction
prices = re.findall(r'\$\s*([\d,]+)', html)
# Address extraction
addresses = re.findall(
r'class="address"[^>]*>(.*?)</(?:div|span|p)', html
)
# Bed/Bath extraction
beds = re.findall(r'(\d+)\s*(?:bed|br|bedroom)', html, re.I)
baths = re.findall(r'(\d+)\s*(?:bath|ba|bathroom)', html, re.I)
# Sqft extraction
sqft = re.findall(r'([\d,]+)\s*(?:sq\s*ft|sqft)', html, re.I)
# Combine available data
count = max(len(prices), len(addresses), 1)
for i in range(min(count, 50)): # Cap at 50 per page
listing = {
"price": prices[i] if i < len(prices) else None,
"address": (
addresses[i].strip() if i < len(addresses) else None
),
"beds": beds[i] if i < len(beds) else None,
"baths": baths[i] if i < len(baths) else None,
"sqft": sqft[i] if i < len(sqft) else None,
"collected_at": datetime.utcnow().isoformat(),
}
if listing["price"] or listing["address"]:
listings.append(listing)
return listings
def export_csv(self, listings, filename):
if not listings:
print("No listings to export")
return
keys = ["price", "address", "beds", "baths", "sqft", "collected_at"]
with open(filename, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=keys)
writer.writeheader()
writer.writerows(listings)
print(f"Exported {len(listings)} listings to {filename}")
# Usage
collector = PropertyCollector()
search_urls = [
"https://example-realty.com/search?city=austin&type=sale&page=1",
"https://example-realty.com/search?city=austin&type=sale&page=2",
"https://example-realty.com/search?city=austin&type=sale&page=3",
]
listings = collector.collect_listings(search_urls)
collector.export_csv(listings, "austin_listings.csv")
Cómo encaja la resolución dentro del recolector
El código anterior hace tres cosas que conviene entender antes de adaptarlo a tu portal:
- Detecta el desafío en la propia respuesta HTML. Busca
data-sitekeyy decide si es reCAPTCHA v2 o v3 según aparezcarecaptcha/api.js?render=; luego comprueba si hay un widgetcf-turnstile. Así evitas enviar la tarea con el método equivocado, el error más común. - Envía la tarea a
in.phpy consulta el resultado enres.php. Mientras la respuesta seaCAPCHA_NOT_READY, sigue sondeando cada 5 segundos. Con el token en mano, reenvía el formulario con el campo correspondiente:g-recaptcha-responseocf-turnstile-response. - Aísla los fallos por URL. Si una página falla, el bucle registra el error y sigue con la siguiente. En una ronda de 800 fichas, perder 3 no es un incidente; perder las 800 por una excepción sin capturar, sí.
Del dato crudo al análisis de mercado
Daily Collection
→ Property listings (500-1000 per market)
→ Price changes (delta from previous day)
→ New listings vs delisted
Weekly Analysis
→ Median price trends
→ Inventory levels
→ Days-on-market averages
→ Price-per-sqft by neighborhood
Monthly Report
→ Market heat map
→ Competitive pricing analysis
→ Investment opportunity scoring
Ese esquema convierte una extracción puntual en una serie temporal: el valor está en la diferencia entre la ficha de hoy y la de hace treinta días.
Escenario: seguimiento de precio por metro cuadrado en dos mercados
Una consultora con oficinas en Madrid y Ciudad de México publica cada lunes el precio medio por metro cuadrado por barrio. Su recolección nocturna recorre unos 40 municipios y 6.000 fichas entre portales de anuncios y registros públicos de tasación; cerca del 15% de las páginas devuelve un desafío, casi siempre reCAPTCHA v3 o CAPTCHA de imagen.
El cuello de botella no es el ancho de banda, sino cuántas verificaciones caben en vuelo: unas 900 resoluciones por noche, la mayoría por debajo de 4 s, se despachan con unas decenas de threads. Detalle nada menor para agencias que facturan en pesos o euros: el plan es un importe fijo mensual en USD, no un pago por resolución.
Cuántos threads necesitas según tu volumen
CaptchaAI factura por threads concurrentes, con resoluciones ilimitadas dentro del plan. Un thread es un CAPTCHA en vuelo: al terminar, queda libre para el siguiente.
- BASIC ($15/mes, 5 threads) — un par de portales; sirve para validar el flujo.
- STANDARD ($30/mes, 15 threads) — una ciudad con varias fuentes.
- ADVANCE ($90/mes, 50 threads) — el punto habitual del escenario anterior: varios mercados, miles de fichas por noche.
- PREMIUM ($170/mes, 100 threads) y CORPORATE ($240/mes, 150 threads) — carteras nacionales con varias rondas al día.
Si tu portal principal usa Cloudflare Challenge, calcula con su tiempo y no con el mejor caso: eso separa una ronda que acaba a las 4:00 de otra que sigue a las 9:00.
Buenas prácticas y cumplimiento
Trabaja sobre listados publicados y evita recopilar datos personales de vendedores o agentes. Revisa los términos de servicio del portal y respeta la normativa de protección de datos aplicable — RGPD y LOPDGDD en España, LFPDPPP en México — antes de almacenar cualquier campo identificativo. Añade un retardo razonable entre solicitudes y conserva la marca temporal de cada extracción.
Preguntas frecuentes
¿Necesito un navegador headless o me basta con requests?
Depende del portal. Si el listado llega en el HTML inicial, una sesión con requests es más rápida y barata. Si el contenido se pinta con JavaScript, usa Playwright o Selenium e inyecta el token antes de enviar el formulario.
¿Cuántos threads necesito para 5.000 fichas por noche?
Calcula cuántas de esas fichas devuelven CAPTCHA (rara vez son todas) y multiplica por el tiempo de resolución del tipo dominante. Con reCAPTCHA v3 por debajo de 4 s, unas 800 resoluciones en 6 horas caben de sobra en ADVANCE ($90/mes, 50 threads).
¿Qué hago si el token se acepta pero la página sigue bloqueada?
Casi siempre es un desajuste de sesión, o un action de reCAPTCHA v3 que no coincide con el que espera el portal. Reutiliza la misma sesión HTTP para cargar, resolver y reenviar.
¿Qué tipos cubre el servicio en este flujo?
reCAPTCHA v2 y v3 (incluidas las variantes Enterprise), Cloudflare Turnstile y Cloudflare Challenge, GeeTest v3 y los CAPTCHA de imagen y texto. hCaptcha y FunCaptcha no son tipos compatibles; GeeTest v4 figura como próximamente.