Scraping de boletines oficiales en España: BOE, BDNS y 20 boletines autonómicos
Cuando decidí construir un sistema que procesara los boletines oficiales españoles de forma automatizada, subestimé por completo la diversidad de formatos. España tiene un boletín estatal (BOE), una base de datos de subvenciones (BDNS), el boletín mercantil (BORME) y un boletín oficial por cada comunidad autónoma. Son más de 20 fuentes, cada una con su propio formato, sus propias reglas de publicación y sus propias peculiaridades técnicas.
En este artículo explico cómo funciona el servicio de extracción que alimenta los datos de la sección de boletines de Boletín Claro.
El BOE: XML bien estructurado con PDFs problemáticos
El Boletín Oficial del Estado es, con diferencia, la fuente más limpia. Publica un sumario diario en XML con una estructura predecible:
<sumario>
<diario nbo="58">
<seccion num="2A" nombre="Autoridades y personal">
<departamento nombre="Ministerio de Hacienda">
<epigrafe nombre="Nombramientos">
<item id="BOE-A-2026-1234">
<titulo>Resolución de 20 de marzo...</titulo>
<urlPdf>/boe/dias/2026/03/24/pdfs/BOE-A-2026-1234.pdf</urlPdf>
</item>
</epigrafe>
</departamento>
</seccion>
</diario>
</sumario>
El sumario se descarga con un simple GET a https://boe.es/diario_boe/xml.php?id=BOE-S-20260324. Parsear el XML con lxml es trivial. El problema empieza cuando necesitas el contenido real de cada entrada, que está en PDF.
Los PDFs del BOE varían enormemente en calidad. Las disposiciones generales suelen tener texto seleccionable limpio. Pero las convocatorias de subvenciones a menudo incluyen tablas complejas, columnas múltiples o texto escaneado. Uso pdfplumber como extractor principal porque maneja bien las tablas, y aplico heurísticas de postprocesamiento para reconstruir párrafos que se rompen entre páginas.
El BDNS: una API REST con paginación errática
La Base de Datos Nacional de Subvenciones es un caso aparte. A diferencia del BOE, el BDNS expone una API REST pública. En teoría esto debería ser más sencillo. En la práctica, tiene sus propias trampas.
La API permite filtrar por fecha de publicación y devuelve resultados paginados. El problema es que la paginación no es consistente: el total de resultados puede cambiar entre peticiones si se publican nuevas entradas. Mi solución es paginar hasta que reciba una página vacía, en lugar de confiar en el campo totalRegistros.
async def fetch_bdns_page(session, date, offset):
params = {
"fechaDesde": date.strftime("%d/%m/%Y"),
"fechaHasta": date.strftime("%d/%m/%Y"),
"numPagina": offset // PAGE_SIZE + 1,
"tamPagina": PAGE_SIZE,
}
resp = await session.get(BDNS_API_URL, params=params)
data = resp.json()
return data.get("convocatorias", [])
Cada convocatoria del BDNS viene con campos estructurados: órgano convocante, presupuesto, plazo, beneficiarios. Esto es oro comparado con tener que extraerlo de un PDF. Lo convierto directamente a un formato interno con los campos normalizados. Para más detalles sobre la estructura del BDNS, escribí una guía completa sobre el BDNS.
Los boletines autonómicos: el salvaje oeste
Y aquí es donde la cosa se pone interesante. Cada comunidad autónoma publica su propio boletín oficial con su propio formato. Algunos ejemplos de lo que me he encontrado:
- BOCM (Madrid): publica HTML razonablemente limpio con secciones identificables por IDs de CSS. Se puede parsear con
beautifulsoup4sin demasiado sufrimiento. - DOGC (Cataluña): ofrece un XML del sumario bien estructurado, similar al BOE. De los mejores en términos de accesibilidad técnica.
- BOJA (Andalucía): HTML con estructura inconsistente entre secciones. Los títulos de las disposiciones no siguen un patrón uniforme.
- DOG (Galicia): tiene una API interna para el sumario que devuelve JSON. No está documentada públicamente pero funciona de forma estable.
- BOPV (País Vasco): publica en tres idiomas (castellano, euskera, francés en algunos casos). Hay que seleccionar la versión correcta.
Para cada boletín, implemento un cliente HTTP específico que hereda de una clase base. La interfaz es simple: dada una fecha, devuelve una lista de entradas con título, texto, sección y metadata. La complejidad queda encapsulada dentro de cada cliente.
Idempotencia: la clave para un sistema robusto
El reader se ejecuta cada mañana a las 7:00 vía Cloud Scheduler. Pero a veces falla: el BOE tarda en publicar, una comunidad autónoma tiene su web caída, la red tiene un problema transitorio. Por eso la idempotencia es fundamental.
Cada entrada se identifica con un hash compuesto por la fuente, la fecha y el identificador interno del boletín. Antes de insertar en Firestore, compruebo si ya existe. Si ya se procesó, se salta sin error. Esto permite re-ejecutar el reader las veces que haga falta sin riesgo de duplicados.
entry_id = hashlib.sha256(
f"{source}:{date}:{internal_id}".encode()
).hexdigest()[:20]
if await firestore_client.document_exists("bulletin_entries", entry_id):
logger.info(f"Skipping duplicate: {entry_id}")
return None
Gestión de errores y reintentos
Los sitios web del gobierno no son exactamente famosos por su fiabilidad. Implemento un sistema de reintentos con backoff exponencial para cada fuente. Si una fuente falla tres veces consecutivas, se marca como fallida para esa fecha y se genera una alerta interna.
También manejo el caso de los boletines que se publican tarde. Algunas comunidades autónomas no publican hasta las 10:00 o las 11:00. El scheduler tiene una ejecución principal a las 7:00 y una de recuperación a las 12:00 que solo procesa las fuentes que fallaron en la primera ronda.
Conversión a markdown
Una vez extraído el contenido, todo se convierte a markdown estructurado. El markdown sirve como formato intermedio entre la extracción y el procesamiento de IA. Es legible, compacto y se tokeniza bien para los LLMs.
La estructura es siempre la misma independientemente de la fuente: un header con metadata (fuente, fecha, sección, departamento) y el cuerpo del texto. Esto permite que el interpreter trabaje con un formato uniforme sin importar si la entrada original era XML del BOE o HTML del BOJA.
Métricas y monitorización
Cada ejecución del reader genera métricas que registro en Firestore: número de entradas por fuente, tasa de errores, tiempo de ejecución. Un día normal del BOE tiene entre 50 y 100 entradas. El BDNS puede tener entre 100 y 300. Los autonómicos varían mucho: el BOCM puede tener 70 entradas un lunes y 15 un viernes.
Si el número de entradas cae por debajo de un umbral para una fuente en un día laborable, se genera una alerta. Esto ha detectado varias veces que una fuente ha cambiado su formato HTML y el parser ha dejado de funcionar correctamente.
Todo este sistema de extracción es lo que alimenta los datos que luego se pueden consultar en los buscadores de boletines de Boletín Claro. El reto no es tanto técnico como operacional: mantener 20+ parsers funcionando contra sitios web que cambian sin previo aviso.