Todo el mundo construye primero el scraper de agregadores. Cars.com, CarGurus, AutoTrader: tres sitios, un parser para cada uno, y al final de la semana tienes un feed que parece el mercado de autos usados.
Luego alguien pregunta dónde está el resto.
El Desafío
NADA contabiliza 16,972 concesionarios franquiciados de vehículos ligeros en los Estados Unidos según su informe de mediados de 2025, y eso es antes de contar los lotes independientes, que nadie contabiliza de manera consistente. Artículos secundarios sobre la misma cifra de NADA se sitúan entre 15,720 y 16,990, lo que te da una idea de qué tan bien medido está este mercado.
Cada una de esas ubicaciones opera su propio sitio web. El inventario que aparece allí es el stock de ese concesionario, con el precio de hoy, días antes de que cualquiera de ellos llegue a un sitio de listados de terceros. Si estás fijando precios de vehículos usados, pronosticando valores residuales o vendiendo una herramienta competitiva a los concesionarios, el sitio web del concesionario es el dato que buscas. Los agregadores son una copia retrasada y filtrada del mismo.
Así que los equipos van tras los sitios web de los concesionarios y descubren tres cosas en este orden.
No son únicos. Casi todos funcionan sobre un conjunto reducido de plataformas de sitios web para concesionarios: Dealer.com, DealerOn, Dealer Inspire, CDK, Reynolds, Sincro, Lotlinx (la lista exacta depende de quién la elabore). Cualquiera que venda estos datos construye un parser por plataforma, no uno por concesionario. El scraper de inventario de sitios de concesionarios de Apify lo dice claramente: detecta primero la plataforma y luego extrae. Un puñado de plantillas que cubren decenas de miles de concesionarios. Esa es la buena noticia de esta historia.
El precio generalmente no está en el HTML que descargaste. Estas plataformas renderizan el bloque de precios en el cliente, y las estimaciones de pago y los incentivos a menudo llegan en una segunda llamada posterior. Una simple solicitud HTTP te da el año, la marca, el modelo, el kilometraje y el VIN. El campo del precio regresa vacío.
Y luego la parte que arruina silenciosamente los datasets: en el sitio de un concesionario, un fallo se ve exactamente como un dato real. Un servicio de detección de bots responde a una request sospechosa con HTTP 200 y una página intersticial. Un bloque de precios que nunca se renderizó deja "Call for Price" en el DOM, algo que los concesionarios también escriben allí a propósito. Ambas filas llegan a tu data warehouse con un aspecto igualmente limpio.
Cubrimos ese patrón en un vertical diferente, donde un bloqueo parece un punto de datos. El sector automotriz es la versión más compleja, porque "sin precio" es un estado de negocio legítimo en lugar de una anomalía evidente.
Qué Cambia Cuando Divides por Costo
Los pipelines que resisten no se organizan por sitio. Se organizan según el costo de cada request.
La request costosa es la primera contra un concesionario: la que tiene que ejecutar un navegador, superar lo que la plataforma del concesionario haya puesto enfrente y regresar con una sesión. Todo lo que sigue es una llamada HTTP económica que reutiliza lo que la primera obtuvo.
import requests
FOURA = "https://api.foura.ai/api"
AUTH = {"Authorization": "Bearer pk_live_..."}
# First page of a rooftop: render it, clear the defense, keep the session.
first = requests.post(f"{FOURA}/browser", headers=AUTH, json={
"url": "https://example-motors.com/used-inventory/index.htm",
"unblocker": True,
"timeout_ms": 45000,
}).json()
listings = first["body"]
jar = "; ".join(f'{c["name"]}={c["value"]}' for c in first["cookies"])
agent = first["userAgent"]
exit_id = first["proxy"] # opaque proxy ID, send it back to stay on the same exit
Luego recorre el resto del inventario de ese concesionario sin volver a pagar por un navegador:
page = requests.post(f"{FOURA}/single", headers=AUTH, json={
"method": "GET",
"url": "https://example-motors.com/used-inventory/index.htm?start=20",
"proxy": exit_id,
"headers": [["Cookie", jar], ["User-Agent", agent]],
"validate": {
"status": {"accept": [200]},
"data": {
"accept": ["vehicle-card"],
"fail": ["Just a moment", "Access Denied"]
}
}
}).json()
Hay dos detalles que tienen más peso del que parece.
La sesión viaja como una sola unidad. Una cookie de autorización queda vinculada a la IP de salida con la que se obtuvo y al User-Agent utilizado. Si la reproduces desde otro lugar o con un User-Agent distinto, el sitio te devolverá al challenge. Por eso el jar, el string del agente y el proxy ID se mueven juntos. Es el error que vemos con más frecuencia: conservan la cookie, descartan la salida y luego se preguntan por qué la ruta económica dejó de serlo.
El bloque validate es lo que elimina el problema de los fallos silenciosos. Es la request indicando cómo luce una página real: aceptar un marcador que solo existe cuando la cuadrícula de listados ya se renderizó y fallar ante strings de páginas intermedias. Una response que no cumple estas reglas no es una fila con precio nulo. Es un fallo, clasificado como tal, y no se cuenta como un éxito. En el sector automotriz, define tanto el marcador positivo como la lista negativa, ya que "Llamar para consultar precio" es ambiguo por naturaleza, mientras que "la tarjeta del vehículo nunca se renderizó" nunca lo es.
Cuando aún no sabes qué ruta necesita una plataforma determinada, Auto lo averiguará en una sola llamada y te devolverá la sesión que funcionó. Trata esto como una fase de reconocimiento y no como la ruta de producción. Una vez que sepas que una plataforma requiere el navegador y sus plataformas vecinas no, fija cada una al motor directo y deja de pagarle a un orquestador para que redescubra la misma respuesta todas las noches.
Resultados
Calcula los números para un trabajo de tamaño mediano (escenario ilustrativo basado en benchmarks del sector, no en un cliente específico): 4.000 concesionarios, aproximadamente 180 vehículos usados cada uno, actualizados cada noche.
- 4.000 páginas renderizadas en lugar de 720.000. Un render por concesionario abre la sesión; las otras 716.000 páginas van por la ruta económica usando esa misma sesión. Esa proporción, y no el parser, es lo que determina si la cobertura nocturna es viable económicamente.
- Dos tipos de precios faltantes, en dos tablas diferentes. Con las reglas de validación activadas, "el concesionario no publica el precio" y "nunca obtuvimos la página" dejan de compartir la misma estructura de fila. Tu modelo solo llega a ver el primer caso.
- Un parser por plataforma, no por concesionario. Detecta la plataforma a partir de la response y entrega el HTML al parser correspondiente. Integrar un nuevo concesionario en una plataforma ya cubierta tiene un coste de cero.
- Un concesionario que cambia de plataforma falla de forma evidente. La detección de plataforma no coincide, la fila nunca se escribe y alguien recibe un ticket de alerta en lugar de acumular seis semanas de precios erróneos sin que nadie lo note.
Donde el proceso se complica: los grandes grupos de concesionarios utilizan cada vez más sitios web personalizados fuera de plataforma, y estos todavía requieren parsers creados a mano con todo el mantenimiento que eso implica. La frescura de los datos por concesionario tampoco es uniforme. Algunas plataformas aplican un caché muy agresivo a sus páginas de inventario, por lo que "el precio de hoy" puede tener un día de antigüedad sin importar con qué frecuencia lo extraigas. Si tu modelo asume que el timestamp de cada concesionario está igual de actualizado en tiempo real, tendrá un sesgo que ninguna infraestructura de recolección podrá corregir.
Conclusión clave
La parte difícil de los datos automotrices nunca fueron los tres agregadores con los que todo el mundo se compara. Son los diecisiete mil sitios pequeños que por sí solos nunca justificaron un scraper dedicado y que, juntos, valen muchísimo.
Ese patrón aparece mucho más allá de los autos. Farmacias, distribuidores de equipamiento, tiendas de comestibles regionales, cualquier tipo de franquicia: la 'long tail' solo parece costosa mientras tratas a cada miembro de ella como un sitio único. Por lo general, no lo es. Acierta con el primer request, haz que la sesión sea reutilizable, y lo que queda deja de ser un problema de recolección para convertirse en un problema de parsing, que es, por un amplio margen, el más económico de los dos.