Ir al contenido

Scraping y Recolección de Datos

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ámaraClienteDificultadRazón
DiputadoshttpxBajaPortal de datos abiertos, sin protección
Senadocurl_cffiAltaWAF Incapsula con TLS fingerprinting
FuenteCámaraURL BaseMétodoVolumen
Datos AbiertosDiputadosdatos.abiertos.diputados.gob.mxhttpx + delay~4,600 eventos de votación
Portal LXVISenadosenado.gob.mx/66/curl_cffi + TLS impersonation5,047 eventos de votación
Directorio XLSSenadoArchivos XLS oficialespandas read_excelLegislaturas LVIII-LXV

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.

Scraper del Senado — Caso de Estudio Anti-WAF

Sección titulada «Scraper del Senado — Caso de Estudio Anti-WAF»

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:

  1. TLS fingerprinting: El hash JA3 del handshake TLS identifica clientes que no son navegadores reales
  2. Desafíos JavaScript: Incapsula sirve JS que los navegadores reales ejecutan automáticamente
  3. 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.

El SenadoLXVIClient en senado/scrapers/shared/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

Seis objetivos de impersonación rotan entre sesiones. Cada objetivo presenta un hash JA3 distinto al WAF:

ObjetivoPerfil
chromeChrome desktop (última versión)
safariSafari desktop
chrome116Chrome 116 desktop
chrome131Chrome 131 desktop
edgeEdge desktop
chrome_androidChrome mobile

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:

  1. Sesión activa: fingerprint fijo del pool, cookies persistentes compartidas entre solicitudes dentro de la sesión
  2. Bloqueo del WAF detectado: cerrar la sesión inmediatamente, descartar todas las cookies (las cookies quemadas cargan flags del WAF)
  3. Nueva sesión: rotar al siguiente fingerprint del pool, realizar una solicitud GET de warm-up para poblar cookies frescas antes de scrapear
  4. Rotación proactiva: rotar la sesión cada 10 solicitudes (MAX_REQUESTS_PER_SESSION) antes de que el WAF pueda detectar el patrón

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.

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.

MétricaCantidad
Eventos de votación scrapeados5,047
Perfiles de senadores scrapeados1,754
Iteraciones hasta funcionar3
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.

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.

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

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.

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.

  1. 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_cffi que pueden impersonar stacks TLS de navegadores reales son esenciales.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.