Pipeline ETL robusto de agregación de datos públicos del sector inmobiliario español: extracción multi-fuente con anti-bot mitigation comercial, normalización heterogénea de campos, enriquecimiento semántico vía LLM y multiprocessing con respectful rate limiting. Entregado a cliente B2B para análisis de mercado (caso cerrado).
Para clientes B2B / fractional CTO — case-study de delivery completo a cliente B2B sectorial inmobiliario: pipeline ETL robusto con anti-bot mitigation comercial multi-señal, multi-worker concurrente con work partitioning (sin race conditions), normalización heterogénea de campos, enriquecimiento semántico vía LLM y respectful pacing con scope GDPR-aware. Entregable cerrado con disciplina institucional: ADRs versionadas, runbooks operativos, postmortems formales, anonimización profesional bajo NDA.
| DATA PIPELINE End-to-end ETL extracción · validación · normalización · enriquecimiento · entrega |
ARQUITECTURA Multi-worker concurrente work partitioning + merge offline · sin race conditions |
COMPLIANCE Solo datos públicos respectful pacing · third-party anti-bot API · scope GDPR-aware |
Este repositorio describe un proyecto entregado y cerrado para un cliente B2B del sector inmobiliario español. El cliente solicitó una pipeline de agregación de datos públicos disponibles en un portal inmobiliario de ámbito nacional, normalizados para análisis comparativo de mercado.
Cliente y portal específicos no se mencionan por consideraciones contractuales y legales. La narrativa técnica aplica al patrón general "data pipeline desde portal público con anti-bot mitigation" — extrapolable a múltiples sectores (real estate, e-commerce, classifieds, public records, regulatory filings).
El cliente operaba en análisis de mercado inmobiliario y necesitaba consolidar datos públicos heterogéneos desde un portal nacional para:
- Comparativa de precios y características entre regiones geográficas.
- Detección de outliers y patrones temporales.
- Enriquecimiento semántico de descripciones libres a campos estructurados.
- Generación de datasets normalizados para alimentar dashboards y modelos analíticos internos.
Los retos técnicos sistémicos al enfrentar este tipo de extracción:
| Antipatrón sistémico de la industria | Patrón aplicado en este proyecto |
|---|---|
| Scrapers monolíticos sin detección de bloqueos → fallos silenciosos | Detección multi-señal de anti-bot (DOM markers + content regex + script inspection) |
| Resolución de CAPTCHAs propia / técnicas en gris | Delegación a API comercial de resolución legítima (servicio de tercero) |
| Pacing agresivo → ban del IP de origen | Respectful rate limiting con delays aleatorios + cap de concurrencia |
| Multiprocessing escribiendo al mismo archivo → race conditions, duplicados | Work partitioning: cada worker su propio CSV; merge offline al final |
| Normalización al final del pipeline → errores propagados N capas | Normalización early (en parsing), formato canónico antes de persistir |
| Sesiones / cookies sin scope correcto → "sigue bloqueado tras resolver" | Cookies inyectadas con dominio + path + secure + sameSite explícitos |
| Bot detection puramente por user-agent → fácilmente detectable | Simulación de comportamiento humano (scroll, mouse, delays) en navegador real |
Pipeline ETL en cinco etapas con responsabilidades estrictamente separadas:
- Ingestion: lectura de listado de URLs públicas → particionado en N lotes (uno por worker).
- Extraction: navegador headless con perfil anti-detección + proxy residencial geográfico + simulación de comportamiento humano. Detección multi-señal de bloqueos anti-bot.
- Anti-bot mitigation: delegación a API comercial de resolución de CAPTCHAs (servicio de tercero legítimo). Inyección de cookies validadas en contexto de navegador con scope correcto.
- Normalization: limpieza de caracteres especiales, formato canónico de campos (teléfonos, direcciones, áreas, precios), validación contra regex de país.
- Enrichment: opcional, análisis semántico de descripciones libres vía LLM (extracción de amenities implícitas, clasificación).
- Output: CSV consolidado tras merge offline de los N lotes parciales, con deduplicación por ID público.
flowchart LR
SRC[Portal inmobiliario<br/>público nacional]
INGEST[Ingestion Layer<br/>URL list → N batches]
BROWSER[Browser headless<br/>anti-detection profile<br/>+ residential proxy]
DETECT{Anti-bot<br/>detected?}
RESOLVE[Third-party<br/>CAPTCHA<br/>resolution API]
PARSE[Parse + Extract<br/>structured fields]
NORM[Normalization<br/>canonical format]
LLM[LLM enrichment<br/>semantic extraction]
MERGE[Merge offline<br/>+ deduplicate]
CSV[(CSV output<br/>normalized dataset)]
SRC --> BROWSER
INGEST --> BROWSER
BROWSER --> DETECT
DETECT -->|yes| RESOLVE
DETECT -->|no| PARSE
RESOLVE -->|cookies injected| PARSE
PARSE --> NORM
NORM --> LLM
LLM --> MERGE
MERGE --> CSV
style DETECT fill:#fee,stroke:#c00
style RESOLVE fill:#ffe,stroke:#cc0
style LLM fill:#eef,stroke:#06c
flowchart TB
subgraph ext["External Systems"]
PORTAL["Public Real-Estate Portal<br/>(national scope)"]
CAPTCHA_API["Third-party CAPTCHA<br/>resolution API"]
PROXY["Residential proxy<br/>service (geo-targeted)"]
LLM_API["LLM API<br/>(semantic enrichment)"]
end
subgraph actors["Actors"]
CLIENT["Client analyst<br/>(market research)"]
OPERATOR["Pipeline operator<br/>(scheduled runs)"]
end
INMO["Inmo Data Pipeline<br/>(ETL anonymized)"]
OPERATOR -->|"trigger · schedule"| INMO
CLIENT -->|"consume normalized CSVs"| INMO
INMO -->|"public listing fetch<br/>respectful rate limiting"| PORTAL
INMO -->|"submit CAPTCHA task<br/>poll for solution"| CAPTCHA_API
INMO -->|"residential IP rotation"| PROXY
INMO -->|"semantic enrichment<br/>(optional)"| LLM_API
classDef system fill:#2962FF,stroke:#1E88E5,color:white
classDef external fill:#00C853,stroke:#00E676,color:white
classDef actor fill:#757575,stroke:#9E9E9E,color:white
class INMO system
class PORTAL,CAPTCHA_API,PROXY,LLM_API external
class CLIENT,OPERATOR actor
flowchart TB
subgraph boundary["Inmo Data Pipeline"]
subgraph ingest_tier["Ingestion Tier"]
CTRL["Master Controller<br/>work partitioning<br/>+ worker dispatch"]
INPUT[("URL list<br/>(input CSV)")]
end
subgraph worker_tier["Worker Tier (N parallel)"]
W1["Worker #1<br/>browser context"]
W2["Worker #2<br/>browser context"]
WN["Worker #N<br/>browser context"]
end
subgraph detect_tier["Anti-bot Mitigation"]
DETECT["Multi-signal Detector<br/>DOM + regex + script"]
RESOLVER["CAPTCHA Resolver Client<br/>polling + timeout"]
end
subgraph transform_tier["Transformation"]
PARSER["Field Parser<br/>structured extraction"]
NORMALIZER["Normalizer<br/>canonical format<br/>+ regex validation"]
ENRICHER["LLM Enricher<br/>(optional)"]
end
subgraph output_tier["Output"]
PARTIAL[("Partial CSV<br/>per worker")]
MERGER["Offline Merger<br/>+ dedup by ID"]
FINAL[("Final CSV<br/>normalized dataset")]
end
end
subgraph external["External"]
PORTAL["Public Portal"]
CAPTCHA["CAPTCHA API"]
PROXY["Residential Proxy"]
LLM["LLM API"]
end
INPUT --> CTRL
CTRL --> W1
CTRL --> W2
CTRL --> WN
W1 -->|via proxy| PROXY
PROXY --> PORTAL
PORTAL -->|response| W1
W2 -->|via proxy| PROXY
WN -->|via proxy| PROXY
W1 --> DETECT
DETECT -->|blocked| RESOLVER
RESOLVER -->|submit + poll| CAPTCHA
CAPTCHA -->|solution| RESOLVER
RESOLVER -->|cookies| W1
DETECT -->|clear| PARSER
PARSER --> NORMALIZER
NORMALIZER --> ENRICHER
ENRICHER -.->|optional API call| LLM
LLM -.->|enriched fields| ENRICHER
ENRICHER --> PARTIAL
PARTIAL --> MERGER
MERGER --> FINAL
classDef ingest fill:#FF6D00,stroke:#FF9100,color:white
classDef worker fill:#7C4DFF,stroke:#651FFF,color:white
classDef detect fill:#D32F2F,stroke:#B71C1C,color:white
classDef transform fill:#2962FF,stroke:#1E88E5,color:white
classDef output fill:#388E3C,stroke:#1B5E20,color:white
classDef external fill:#757575,stroke:#9E9E9E,color:white
class CTRL,INPUT ingest
class W1,W2,WN worker
class DETECT,RESOLVER detect
class PARSER,NORMALIZER,ENRICHER transform
class PARTIAL,MERGER,FINAL output
class PORTAL,CAPTCHA,PROXY,LLM external
Decisiones arquitectónicas clave:
- Multiprocessing.Process con work partitioning en vez de threads / async para evitar locks compartidos sobre I/O de disco.
- Cada worker su propio CSV en vez de escritura compartida → cero race conditions, cero filas perdidas.
- Merge offline al final con deduplicación por ID público → idempotencia de re-runs.
- Anti-bot mitigation vía API comercial en vez de bypass técnico propio → legal, mantenible, separación de responsabilidades.
- Normalización temprana en parsing en vez de al final → errores no se propagan 5 capas hacia abajo.
| Capa | Tecnología | Notas |
|---|---|---|
| Lenguaje | Python 3.10+ | Virtualenv estándar |
| Browser automation | Playwright (sync + async) | Chromium headless con context isolation por worker |
| Anti-detection profile | Browser comercial con perfil anti-detección | Servicio de terceros |
| Proxy infrastructure | Servicio comercial de proxies residenciales geo-targeted | Rotación de IPs por región |
| Anti-bot mitigation | API comercial de resolución de CAPTCHAs | Polling con timeout adaptativo |
| Data manipulation | Pandas + NumPy | DataFrames para normalización + merge |
| HTTP client | requests |
Comunicación con APIs externas |
| LLM enrichment | Commercial LLM API (opcional, feature flag) | Análisis semántico de descripciones |
| Config | python-dotenv |
Secrets fuera del código (.env + .env.example) |
| Concurrency | multiprocessing.Process |
5 workers paralelos con work partitioning |
| Output format | CSV (UTF-8) | Portable, fácil consumo cliente |
1. Detección robusta de anti-bots heterogéneos
- Síntoma: el portal implementaba múltiples mecanismos de bloqueo simultáneos (CAPTCHA visual, slider, script de anti-bot comercial). Los scripts iniciales no los detectaban consistentemente, generando falsos positivos (timeout esperando contenido) o sesiones quemadas silenciosamente.
- Causa raíz: ausencia de predicados de detección multi-señal. Se buscaba un único selector CSS o un único marker textual cuando en realidad había 3+ señales independientes que indicaban bloqueo.
- Fix: matriz de detección con 3 métodos independientes — (a) presencia de selector DOM específico, (b) regex sobre texto visible, (c) inspección de scripts cargados en el HTML. Si cualquiera retorna true, activar estrategia de resolución.
- Lección: anti-bot protection es un sistema, no un endpoint único. En cualquier data pipeline que cruce barreras de fuentes externas, mapear todas las señales observadas (headers, cookies, JavaScript, timing) y codificarlas como predicados combinables. Reduce debugging de horas a minutos.
2. Resolución asíncrona con polling y timeout adaptativo
- Síntoma: la API comercial de resolución responde con un
task_idsin garantía de tiempo. Primeros intentos esperaban respuesta inmediata (2-3s) y caían en timeout falso. Otros esperaban indefinidamente, bloqueando workers. - Causa raíz: no había circuit breaker ni backoff adaptativo. Loop de polling simple sin límite de iteraciones.
- Fix: polling con límite explícito (N iteraciones × intervalo conocido del SLA de la API), logs de estado por intento, escape exponencial. Si timeout real → excepción capturable que reencola el item para distinto worker con nueva sesión.
- Lección: cualquier dependencia de I/O remoto debe documentar explícitamente SLA, timeout máximo aceptable y fallback. El timeout es el "fusible" que evita deadlock. Sin él, un worker lento infecta toda la batch.
3. Normalización heterogénea de datos de contacto
- Síntoma: los campos de contacto venían en N formatos heterogéneos (
tel:+34600...,34600...,600...,+34 600 ...). Los CSVs output mostraban inconsistencia, dificultando el matching downstream con sistemas del cliente. - Causa raíz: el HTML del portal no normalizaba antes de exponer en el DOM. El parser extraía sin aplicar regla consistente.
- Fix: función
normalize_phone()con pipeline determinista — extrae solo dígitos → detecta longitud → infiere si lleva prefijo país → canónico (+34XXXXXXXXX) → valida contra regex de país. Aplicado en el parsing, no al final del pipeline. - Lección: en pipelines heterogéneos, la normalización es tan crítica como la extracción. Codificar early (parsing) no late (consumer side). Un dato "sucio" propagado a 5 fases cuesta 5× más arreglarlo. Schema-first design (definir formato output antes de escribir parsers) ahorra debugging y deuda.
4. Manejo de sesiones y cookies con scope correcto post-resolución
- Síntoma: scripts resolvían el CAPTCHA exitosamente pero la sesión seguía bloqueada. Las cookies se inyectaban en el contexto del navegador pero sin sincronización correcta.
- Causa raíz: cookies con scope incorrecto (
domainvspathvssameSite) no aplicaban. El navegador headless no actualizaba la sesión cuando se mutaban cookies programáticamente sin reload posterior. - Fix: protocolo de inyección estricto — obtener token + URL antes de mutar contexto, inyectar con scope explícito (
domainexacto,path: /,secure: true,sameSite: Lax), forzarreload()y esperar respuesta del portal antes de continuar. - Lección: cookies y sesiones en navegadores headless requieren disciplina de scope. Debuguear con
context.cookies()antes y después. Tests fixtures que validen "sesión OK post-mutación" antes de pasar a producción.
5. Paralelización con multiprocessing y I/O compartido
- Síntoma: el controlador lanzaba N workers en paralelo, todos escribiendo al mismo CSV output sin lock. Resultado: filas duplicadas, parciales, perdidas. Reintento de URLs inconsistente entre workers.
- Causa raíz:
multiprocessing.Processno tiene mutex nativo para acceso a archivos. CSV writing es operación no-atómica. - Fix: work partitioning — dividir URLs en N lotes (uno por worker). Cada worker escribe a su propio CSV (
results_worker_0.csv, etc.). Al final, merge offline: read N files, deduplicate por ID público, write final CSV. - Lección: multiprocessing + shared I/O es un footgun. Mejor: work partitioning (cada process = subset de datos disjuntos) + merge al final. Si hay lock real necesario → switch a
asyncioo ThreadPoolExecutor con Queue. Multiprocessing es para CPU-bound; scraping es I/O-bound, así que el split de partición es más natural.
6. Simulación de comportamiento humano contra heurísticas de timing
- Síntoma: el portal detectaba patrones de acceso automatizado (velocidad de clicks, ausencia de scroll, timing perfecto) y bloqueaba sin mostrar CAPTCHA (solo 403 silencioso).
- Causa raíz: heurísticas anti-bot observan interacciones en timeline real. Un
click()seguido deextract()en 10ms es obviamente bot. - Fix: insertar
simulate_human_behavior()con eventos randomizados — scroll (rango definido), mouse move por pasos pequeños,PageDown, sleeps aleatorios (1-2s). 2-4 acciones por sesión, ejecutadas en momentos no deterministas del flow. - Lección: bot detection es una carrera de armamentos. User-agent + headers no son suficientes; necesitas eventos del navegador (mouse, keyboard, scroll). Cada portal es distinto (algunos ignoran timing, otros usan ML). Medir hit-rate de "bloqueos falsos" y ajustar threshold. No hay solución one-size-fits-all.
Este proyecto operaba sobre datos públicamente visibles del portal (es decir, accesibles a cualquier visitante humano sin login). Las salvaguardas aplicadas:
- Respectful rate limiting: delays aleatorios + cap de concurrencia (workers limitados) → no se generaba carga inusual sobre el origen.
- Solo datos públicos: cero extracción de campos detrás de login, behind paywall, o que requieran autenticación.
- CAPTCHA resolution vía API comercial (no bypass técnico propio): se delega a servicio legítimo de terceros que opera bajo sus propios términos.
- Proxies residenciales legales: servicio comercial con consentimiento de los IP holders.
- Scope GDPR-aware: los datos de contacto presentes en los listados públicos se trataban según las bases legales del cliente (interés legítimo del análisis de mercado vs consentimiento del listing owner). Los outputs entregados al cliente quedaban bajo su responsabilidad como data controller.
No publicado en este case-study (consideración legal + reputacional):
- Nombre real del cliente.
- Identidad del portal específico.
- Selectores CSS / regex propios del portal.
- Cookies y tokens de sesión.
- Detalles de la técnica concreta de mitigación.
- Volúmenes absolutos procesados.
- Datos personales identificables (teléfonos, direcciones, IDs de listings).
Caso cerrado. Entregado al cliente. Proyecto archivado. Este case-study existe como referencia técnica del patrón aplicado, no como producto comercial activo del autor.
- docs/overview.md — visión completa del flujo, lectura 3 min.
- docs/architecture.md — arquitectura por capas con detalles de decisión.
- No es un servicio de scraping. No acepta clientes con este tipo de necesidad bajo este branding (la metodología sí es replicable bajo NDA).
- No publica el código fuente del pipeline original. Es propiedad del proyecto cliente original.
- No publica selectores, regex, ni cookies específicas del portal target.
- No promueve técnicas de bypass de anti-bot ni evasión de rate limiting. La narrativa es de "data pipeline robusto con anti-bot mitigation legítima".
- No es asesoramiento legal. Cada proyecto de extracción de datos públicos requiere validación legal específica del territorio y portal.
El patrón documentado aquí es replicable a otros dominios con extracción de datos públicos heterogéneos:
- E-commerce (catálogos comparados).
- Public records / registros públicos.
- Classifieds (clasificados de cualquier vertical).
- Regulatory filings.
- News / media monitoring (con respeto explícito a robots.txt).
Los retos técnicos resueltos (detección anti-bot multi-señal, polling robusto, normalización temprana, work partitioning, comportamiento humano) son horizontales.
Este proyecto se desarrolló bajo el sistema operativo de ingeniería propio del autor, replicado cross-proyecto en múltiples proyectos del ecosistema:
- Automation Engineering Protocol — stage-gate horizontal de cambio: change-spec → guard layer → release train → rollback playbook.
- Prompt Engineering Protocol — diseño modular: required/optional/exclusion keywords + patterns regex + confidence scoring + anti-echo + whitelist + schema JSON validation.
- Post-Development Verification Protocol — 4 niveles (estática / integración / canarios E2E / observabilidad diaria).
- Post-Development Gates — 94 gates pre-merge / pre-deploy / post-deploy con checklist bloqueante.
- Frozen Zones & Regression Prevention — CONFIG vs HEALTH, auditoría holística periódica.
Cada decisión estructural se documenta como ADR versionada. Cada cambio significativo pasa por verify-findings adversarial. Engineering-playbook propio con 60+ archivos doctrinales cross-proyecto. La documentación es activo de primera clase.
Protocolo de documentación detallado (arc42 + C4 + ADR + runbooks + postmortems) en docs/documentation-protocol.md.
Detalles operativos del playbook bajo acuerdo de confidencialidad.
SatData Solutions — Valencia, España
- Email: solutions.satdata@gmail.com
- LinkedIn: Kamil Slodki
- Portfolio: satdata-portfolio
Conversaciones técnicas sobre data pipelines, anti-bot mitigation legítima, escalado I/O-bound y normalización heterogénea bajo NDA. No se aceptan encargos de scraping abusivo, bypass de mecanismos de seguridad ni extracción de datos privados.
Este repositorio es un case-study anonimizado de un proyecto entregado a cliente B2B y cerrado. No contiene código fuente del producto original, datos del cliente, ni elementos identificables del portal target. Su propósito es documentar el patrón técnico aplicable a futuros proyectos similares y mostrar madurez en compliance + arquitectura.
Para conversaciones técnicas detalladas o evaluación de aplicabilidad a un caso concreto del cliente potencial: contacto por los canales arriba bajo NDA.