Vista General
El Observatorio extrae datos legislativos de dos cámaras, cada una con un enfoque fundamentalmente distinto. La Cámara de Diputados expone un portal de datos abiertos sin protección anti-bot, por lo que un cliente HTTP estándar es suficiente. El Senado está protegido por el WAF Incapsula de Imperva, que detecta y bloquea solicitudes automatizadas mediante TLS fingerprinting. Extraer datos del Senado requirió construir un cliente anti-WAF especializado.
Dos cámaras, dos stacks:
| Cámara | Cliente | Dificultad | Razón |
|---|---|---|---|
| Diputados | httpx | Baja | Portal de datos abiertos, sin protección |
| Senado | curl_cffi | Alta | WAF Incapsula con TLS fingerprinting |
Fuentes de Datos
| Fuente | Cámara | URL Base | Método | Volumen |
|---|---|---|---|---|
| Datos Abiertos | Diputados | datos.abiertos.diputados.gob.mx | httpx + delay | ~4,600 eventos de votación |
| Portal LXVI | Senado | senado.gob.mx/66/ | curl_cffi + TLS impersonation | 5,047 eventos de votación |
| Directorio XLS | Senado | Archivos XLS oficiales | pandas read_excel | Legislaturas LVIII-LXV |
Scraper de Diputados
El scraper de Diputados apunta al portal de datos abiertos en datos.abiertos.diputados.gob.mx. No existe protección anti-bot, por lo que el stack es mínimo:
- Cliente HTTP: httpx con un delay configurable entre solicitudes (por defecto 2.0 segundos)
- Parser: BeautifulSoup para parseo HTML donde no hay endpoints JSON disponibles
- Alcance de datos: registros de votación identificados por SITL IDs, perfiles de legisladores y afiliaciones partidistas
El scraper obtiene aproximadamente 4,600 eventos de votación. Todos los datos se cargan en una base de datos SQLite con deduplicación mediante el campo source_id en cada registro vote_event.
:::tip El portal de datos abiertos de Diputados está bien estructurado y es estable. Si estás extendiendo la cobertura a nuevos tipos de datos, empieza aquí — la ausencia de protección anti-bot permite iterar más rápido. :::
Scraper del Senado — Caso de Estudio Anti-WAF
El Problema
El portal del Senado en senado.gob.mx está protegido por Incapsula (WAF de Imperva). Los clientes HTTP estándar — requests de Python, httpx, incluso curl — son bloqueados inmediatamente. El WAF detecta tráfico automatizado a través de tres mecanismos:
- TLS fingerprinting: El hash JA3 del handshake TLS identifica clientes que no son navegadores reales
- Desafíos JavaScript: Incapsula sirve JS que los navegadores reales ejecutan automáticamente
- Análisis conductual: Se monitorean los patrones de solicitudes, timing y comportamiento de cookies
Las primeras dos iteraciones del scraper del Senado fueron bloqueadas en cuestión de minutos.
La Solución
El SenadoLXVIClient en scraper_congreso/senadores/client.py usa curl_cffi con TLS fingerprint impersonation. Esta biblioteca envuelve libcurl-impersonate, que puede reproducir el handshake TLS exacto de navegadores reales — coincidiendo con los hashes JA3, suites de cifrado y extensiones.
class SenadoLXVIClient:
_IMPERSONATE_TARGETS: tuple[BrowserTypeLiteral, ...] = (
"chrome", "safari", "chrome116", "chrome131", "edge", "chrome_android",
)
MAX_REQUESTS_PER_SESSION: int = 10
WAF_CONSECUTIVE_THRESHOLD = 2
Pool de Fingerprints
Seis objetivos de impersonación rotan entre sesiones. Cada objetivo presenta un hash JA3 distinto al WAF:
| Objetivo | Perfil |
|---|---|
chrome | Chrome desktop (última versión) |
safari | Safari desktop |
chrome116 | Chrome 116 desktop |
chrome131 | Chrome 131 desktop |
edge | Edge desktop |
chrome_android | Chrome mobile |
Estrategia de Gestión de Sesiones
El cliente usa una estrategia de sesiones por capas diseñada para minimizar la detección del WAF mientras se recupera de forma graceful ante bloqueos:
- Sesión activa: fingerprint fijo del pool, cookies persistentes compartidas entre solicitudes dentro de la sesión
- Bloqueo del WAF detectado: cerrar la sesión inmediatamente, descartar todas las cookies (las cookies quemadas cargan flags del WAF)
- Nueva sesión: rotar al siguiente fingerprint del pool, realizar una solicitud GET de warm-up para poblar cookies frescas antes de scrapear
- Rotación proactiva: rotar la sesión cada 10 solicitudes (
MAX_REQUESTS_PER_SESSION) antes de que el WAF pueda detectar el patrón
:::caution La rotación proactiva es crítica. Esperar a que el WAF te bloquee antes de rotar significa que la nueva sesión arranca con un nivel de escrutinio elevado. Rotar antes mantiene todas las sesiones bajo el radar. :::
Circuit Breaker
Un circuit breaker rastrea los bloqueos consecutivos del WAF. Tras WAF_CONSECUTIVE_THRESHOLD (2) bloqueos consecutivos, la sesión se declara quemada. El cliente lanza SessionBurnedError, fuerza una pausa obligatoria y debe reiniciarse con una sesión nueva.
Esto evita que el scraper golpee el WAF con solicitudes que nunca van a succeed.
Procedimiento de Warm-up
Tras crear una nueva sesión, el cliente emite una solicitud GET dummy al portal antes de hacer cualquier solicitud de datos real. Esta solicitud de warm-up permite que Incapsula establezca sus cookies de desafío. Una sesión fría sin cookies es bloqueada mucho más agresivamente que una que ya pasó el desafío JS inicial.
Resultados
| Métrica | Cantidad |
|---|---|
| Eventos de votación scrapeados | 5,047 |
| Perfiles de senadores scrapeados | 1,754 |
| Iteraciones hasta funcionar | 3 |
Diagrama de Estrategia Anti-WAF
Request → Check Cache
├─ Cache Hit → Return cached data
└─ Cache Miss → Send via curl_cffi
├─ Response OK → Cache + Return
└─ WAF Detected (Incapsula markers)
├─ Consecutive < 2 → New session, rotate fingerprint, warm-up, retry
└─ Consecutive ≥ 2 → SessionBurnedError → Pause + restart
La capa de cache no es opcional. Cada página cacheada es una solicitud menos al portal del Senado, lo que significa una oportunidad menos para que el WAF detecte y bloquee el scraper. En ejecuciones repetidas, el cache reduce drásticamente la exposición.
:::tip El cache tiene un TTL (tiempo de vida) configurable para evitar que se acumulen datos obsoletos. Ajusta el TTL según la frecuencia de actualización de los datos fuente — TTL más largos para datos históricos, más cortos para legislaturas activas. :::
Calidad y Procesamiento de Datos
Deduplicación
Cada registro vote_event tiene un campo source_id que mapea al identificador original del portal fuente. Esto permite scraping idempotente: ejecutar el scraper múltiples veces no crea registros duplicados. El patrón INSERT OR IGNORE de SQLite sobre source_id maneja esto a nivel de base de datos.
Enriquecimiento de Perfiles
Los perfiles de legisladores se enriquecen con datos demográficos y electorales:
- Género: 480 mujeres / 598 hombres (en todos los registros cargados)
- Tipo de escaño: MR (mayoría relativa) o PL (representación proporcional)
- Circunscripción: Asignación de distrito electoral para escaños PL
Normalización de Partidos
La función normalize_party() mapea los valores mixtos de vote.group que devuelven los portales a IDs canónicos de organización. Los nombres crudos de partidos en los datos fuente son inconsistentes — las abreviaturas varían, las coaliciones crean nombres compuestos y los partidos históricos tienen múltiples etiquetas. La normalización colapsa todas las variantes a un único ID canónico.
Resolución de Membresías
Algunos legisladores tienen membresías multipartidistas a lo largo de su carrera. El scraper resuelve esto por frecuencia de votación: el legislador se asigna al partido donde emitió más votos. Esta es una heurística pragmática — maneja correctamente cambios de partido y expulsiones sin requerir desambiguación manual.
Lecciones Aprendidas
-
TLS fingerprinting es el mecanismo principal de detección de bots para WAFs como Incapsula. Los headers y user-agent strings son fáciles de spoofear; el hash JA3 del handshake TLS no lo es. Bibliotecas como
curl_cffique pueden impersonar stacks TLS de navegadores reales son esenciales. -
La rotación proactiva supera a la rotación reactiva. Rotar sesiones antes de que el WAF detecte un patrón es mucho más efectivo que rotar después de un bloqueo. El límite de 10 solicitudes por sesión es conservador pero confiable.
-
La gestión de cookies importa. Las cookies quemadas cargan flags del WAF. Descartarlas completamente y empezar de cero es mejor que intentar “reparar” una sesión flaggeada.
-
Las solicitudes de warm-up son esenciales. Una sesión fría sin las cookies de desafío de Incapsula es bloqueada en la primera solicitud real. El GET de warm-up puebla las cookies necesarias.
-
El cache reduce la exposición. Cada página cacheada es una solicitud menos al portal. Para un scraper operando detrás de un WAF, minimizar el total de solicitudes es una estrategia de supervivencia, no solo una optimización de rendimiento.
-
El scraper del Senado requirió tres iteraciones para funcionar correctamente. Las primeras dos fueron bloqueadas en minutos. La tercera iteración introdujo curl_cffi, rotación de fingerprints y gestión proactiva de sesiones — y ha funcionado de forma confiable desde entonces.
Sistema de Build y Entry Points
El proyecto usa hatchling como build system mediante pyproject.toml:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.backends"
Cada scraper es ejecutable como módulo de Python:
# Scraper de Diputados
python -m scraper_congreso.diputados --leg LXVI --all-periods
# Registros de votación del Senado
python -m scraper_congreso.senadores.votaciones --range 1 5070 --delay 2.0
# Perfiles del Senado
python -m scraper_congreso.senadores.perfiles
Dependencias Clave
| Paquete | Versión | Propósito |
|---|---|---|
curl_cffi | >= 0.15.0 | Impersonación de huella TLS (anti-WAF del Senado) |
httpx | >= 0.27 | Cliente HTTP para portales de datos abiertos (Diputados) |
beautifulsoup4 | >= 4.12 | Parseo HTML |
lxml | >= 5.0 | Procesamiento rápido de XML/HTML |
pydantic | >= 2.5 | Validación de modelos de datos |
Logging
scraper_congreso/utils/logging_config.py proporciona configuración centralizada de logging para todo el paquete de scraping. Todos los módulos usan el patrón estándar logging.getLogger(__name__), asegurando formato consistente y niveles de log configurables en todo el proyecto.
Para los runners de análisis, analysis/runner_utils.py proporciona la utilidad setup_logging() que configura el logging con las mismas convenciones.