Scraper de empleo para hoteles suizos
Un sistema operativo para buscar oportunidades en el sector hotelero suizo: ejecuta scrapers en cuatro portales, extrae emails de contacto, deduplica ofertas, guarda histórico y permite operar todo desde un dashboard propio.
portales conectados
ejecución programada
reintentos por fallo
vista unificada
Pipeline
De una búsqueda dispersa a datos listos para accionar.
01
Configurar la búsqueda
Ciudad, radio, país, portal y ventana de ejecución quedan definidos antes de lanzar el proceso. La operativa empieza con parámetros visibles, no con constantes escondidas en un script.
02
Recolectar listados
Cada fuente navega sus resultados, pagina cuando hace falta y guarda las URLs candidatas con su estado de progreso, para poder auditar dónde se quedó una ejecución.
03
Entrar al detalle
El scraper abre cada oferta, extrae los campos útiles y busca emails por varias rutas: enlaces mailto, texto visible, expresiones regulares y lectura del código fuente.
04
Normalizar y deduplicar
Los resultados aterrizan en PostgreSQL, se cruzan por email y quedan listos para revisar, exportar o reutilizar sin repetir contactos entre portales.
El problema
Buscar a mano no escalaba.
Para contactar con hoteles en Suiza no bastaba con mirar una bolsa de empleo de vez en cuando. Había que revisar varias fuentes, repetir filtros por ciudades o regiones, abrir las ofertas una a una, localizar los emails, evitar duplicados y conservar un histórico fiable.
El verdadero cuello de botella no era encontrar una oferta concreta, sino convertir cientos de resultados dispersos en una base de contactos útil, limpia y accionable.
La solución
Una herramienta interna, no un script suelto.
Construí una aplicación modular con FastAPI y Playwright que centraliza cuatro scrapers, registra cada ejecución, muestra logs, permite parar o reanudar procesos y exporta los resultados para campañas posteriores.
La parte importante fue diseñarla para el uso real: fallos de red, selectores que cambian, ejecuciones largas, contactos repetidos y la necesidad de operar sin entrar al servidor.
Fuentes conectadas
Cuatro portales, una misma operativa.
HotelCareer
hotelcareer.ch
Búsqueda por ciudad, radio y país, con historial de ejecuciones y exportación por ciudad.
Hogapage
hogapage.ch
Scraper adaptado a categorías y radios más amplios para cubrir más ofertas de hostelería.
Gastrojob
gastrojob.ch
Extracción por regiones, categorías y tipos de empleo del sector gastro-hotelero.
Hoteljob
hoteljob-schweiz.de
Fuente adicional para ampliar la cobertura y no depender de un único portal.
Datos
El valor no está en extraer HTML, sino en dejar una base fiable.
El sistema separa lo que ocurre durante la ejecución de lo que queda como resultado comercial. Esa diferencia permite auditar fallos sin ensuciar la base de contactos.
Datos de ejecución
Estado, portal, ciudad, radio, timestamps, progreso, error, intento actual y ruta de logs. Sirven para saber qué pasó sin abrir el servidor.
Datos de oferta
Título, empresa, ubicación, URL, email, fuente y relación con la ejecución. El objetivo es convertir páginas dispersas en contactos verificables.
Vista operativa
Una vista SQL unificada permite consultar los resultados de todos los portales con la misma forma, aunque cada scraper mantenga sus tablas y matices.
Modelo de datos
Tablas por fuente, una sola vista para consultar.
Cada portal conserva sus propias tablas de ejecuciones y ofertas, porque sus webs no son iguales. Encima, una vista SQL unifica todo y deduplica por email, que es la unidad que de verdad importa para contactar.
scrape_executions (× 4 fuentes)Metadata de cada ejecución por portal. Es lo que permite auditar una corrida sin abrir el servidor y lo que consume el auto-retry.
job_offers_<fuente> (× 4)Ofertas extraídas por portal, con la oferta atada a su ejecución. Cada fuente conserva sus matices (p. ej. industria en tres de ellas).
job_offers (VIEW)Vista que cruza las cuatro fuentes con UNION ALL y deduplica por email (DISTINCT ON LOWER(email)), conservando la oferta más reciente de cada contacto.
<fuente>_daily_config (× 4)Configuración diaria por portal: qué ciudades, con qué radio y filtros, y en qué ventana horaria se ejecuta de forma automática.
Funcionalidades
Lo que lo hace operable.
Extracción multifuente
Cada portal tiene su propio módulo de listados, filtros y detalle. El sistema evita una abstracción falsa: comparte la operativa común, pero respeta las diferencias reales de cada web.
Emails accionables
No se queda en capturar URLs. Busca emails con mailto, expresiones regulares y lectura del código fuente, e incluso reconstruye direcciones ofuscadas («hr [at] hotel [dot] com») para convertir ofertas dispersas en contactos listos.
Persistencia y deduplicación
PostgreSQL guarda ejecuciones, ofertas y resultados. La deduplicación por email reduce el ruido y evita insistir sobre el mismo contacto entre fuentes distintas.
Scheduler diario
APScheduler lanza extracciones automáticas en ventanas configurables, recalcula la hora cada día con un re-scheduler a las 00:01 y permite ejecutar manualmente cuando hace falta.
Reintentos automáticos
Un servicio revisa cada 10 minutos las ejecuciones fallidas o pendientes, respeta las paradas manuales y reintenta hasta tres veces antes de dar el caso por agotado.
Operativa exportable
El dashboard permite revisar el historial, abrir los logs, descargar CSV y limpiar ejecuciones sin tocar la base de datos ni ejecutar scripts a mano.
Ejecución
Pensado para correr muchas veces, no para una demo.
La ejecución combina control manual, programación diaria y recuperación automática. Así el scraper puede vivir como herramienta interna: visible cuando se opera, silencioso cuando funciona y trazable cuando algo falla.
Cada corrida tiene su propio estado, logs, progreso, exportación y reglas claras para parar, reanudar o reintentar. Esa capa es la que convierte el scraping en operativa.
Ciclo de ejecución
- →Lanzamiento manual desde el dashboard para búsquedas puntuales.
- →Programación diaria con APScheduler para mantener el inventario fresco.
- →Eventos SSE y logs persistidos para seguir las ejecuciones largas.
- →Parada y reanudación manual cuando una fuente se atasca o hay que ajustar parámetros.
- →Auto-retry cada 10 minutos para errores transitorios, con un límite de tres intentos.
Arquitectura
Separar bien las responsabilidades fue la clave.
El proyecto pasó de ser una automatización puntual a una aplicación con capas claras: dashboard, API, scheduler, scrapers, persistencia, exportaciones y recuperación ante errores.
FastAPI como centro operativo
La aplicación expone endpoints para lanzar, parar, reanudar, consultar el historial, descargar CSV, programar ejecuciones y leer los logs. Las mutaciones exigen una API key.
Playwright por fuente
Los scrapers navegan los portales, aplican filtros, recopilan URLs y entran al detalle de cada oferta para extraer los campos útiles.
PostgreSQL como memoria
Cada fuente tiene sus propias tablas de ejecuciones y ofertas. Una vista SQL unificada permite consultar todo el inventario sin duplicar datos.
Dashboard estático
HTML, CSS y JavaScript modular sirven una interfaz rápida para la extracción manual, la programación diaria, el historial, los logs y las exportaciones.
Scheduler y auto-retry
APScheduler se encarga de los trabajos diarios y un servicio de retry recupera los errores transitorios de red, timeouts o bloqueos temporales.
Lo difícil
El scraping falla justo donde el producto necesita estabilidad.
Las webs cambian, los banners molestan, los emails se esconden, una ejecución puede tardar más de lo previsto y algunas respuestas llegan incompletas. Por eso el proyecto se diseñó alrededor de la trazabilidad y la recuperación, no solo de la extracción.
La interfaz muestra estado, progreso, logs y descargas; la base de datos conserva lo que pasó; y el auto-retry absorbe los fallos transitorios sin convertir cada incidencia en una tarea manual.
Robustez real
- →Estados separados: pending, running, completed, failed y stopped.
- →Logs persistidos para revisar las ejecuciones terminadas.
- →Reintentos con límite y espera configurable (30 min entre intentos).
- →Descarga CSV por ejecución y vista histórica filtrable.
- →CORS, API key y cabeceras de seguridad para el despliegue.
Decisiones técnicas
Decisiones pequeñas que cambian el resultado.
Breadth-first antes que detalle
Primero recopila las URLs de los resultados y después entra en cada oferta. Así reduce la navegación redundante y mantiene un mejor control del progreso.
Deduplicar por email, no por URL
Una misma empresa puede aparecer en varios portales o publicar ofertas similares. El email es la unidad operativa que importa para contactar.
Respetar las paradas manuales
El sistema distingue entre un fallo técnico y una parada solicitada. Si alguien detiene una ejecución, el auto-retry no la resucita por su cuenta.
Anti-ban pragmático
Rotación de user-agents, pausas aleatorias (jitter) y ejecuciones escalonadas, además de ventanas horarias aleatorias cada día, reducen la fricción sin convertir el scraper en una caja negra.
Stack con contexto
Tecnología elegida por utilidad, no por escaparate.
Python + FastAPI
API asíncrona, routers por fuente y una base limpia para el dashboard, el scheduler y las operaciones.
Playwright
Navegación real para portales con contenido dinámico, formularios, cookies y páginas de detalle.
SQLAlchemy 2.0 + PostgreSQL
Modelos por fuente, persistencia histórica, deduplicación y una vista unificada de ofertas. Pool con pre-ping y reciclado de conexiones.
APScheduler
Ejecuciones diarias en ventanas configurables, re-scheduler diario y trabajos de recuperación.
SSE + logs
Feedback en tiempo real desde el dashboard durante las ejecuciones largas.
Docker
Despliegue reproducible con volumen persistente para datos, logs y exportaciones.