| name | government-data-pipelines |
| version | 1.4.0 |
| description | Patrones para ingerir, estructurar y visualizar datos de fuentes gubernamentales — scraping de webs institucionales, parsers de PDFs con estructura fija, normalización a base de datos, dashboards interactivos. Catálogo de 35+ APIs de datos abiertos españolas (BORME, AEMET, INE, ESIOS, etc.). Incluye casos CIAF (ferroviario), CIAIAC (aviación), CIAIM (marítimo). Patrón Agregador Multi-Fuente para combinar múltiples APIs en dashboards unificados. |
| tags | ["government","scraping","pdf-parsing","data-ingestion","dashboard","geolocation"] |
Government Data Pipelines — Patrones de ingestión de datos gubernamentales
Resumen
Procedimiento sistémico para convertir documentos públicos gubernamentales (informes, estadísticas, registros) en bases de datos estructuradas + visualizaciones interactivas. Los documentos oficiales españoles suelen tener estructura fija y URLs predecibles.
Casos de uso
1. Informes de la CIAF (ferroviario)
- Fuente: https://www.transportes.gob.es/organos-colegiados/ciaf
- URLs PDF: Varios patrones (ver
references/ciaf-scraping.md — 4 patrones según año)
- Estructura fija: Resumen → Descripción → Análisis → Conclusiones → Recomendaciones
- Schema: Ver
templates/ciaf-report-schema.json en skill liteparse-document-ai-parsing
- Scraping: Ver
references/ciaf-scraping.md (dentro de este skill)
- ⚠️ Disponibilidad: 2007-2025 están publicados. 277 informes (ya descargados en
/root/workspace/CIAF/) + 17 memorias + 7 normativas = 301 PDFs total.
⚠️ PATRÓN DE URL POR AÑOS (CRÍTICO — verificado 2026-06-26):
- 2007-2016:
/MFOM/LANG_CASTELLANO/ORGANOS_COLEGIADOS/CIAF/INFORMES/YYYY/
- Ej:
/MFOM/LANG_CASTELLANO/ORGANOS_COLEGIADOS/CIAF/INFORMES/2009/
- Los PDFs están en el HTML estático (no AJAX)
- Patrón de PDF:
href="(/recursos_mfom/pdf/UUID/ID/FILENAME.pdf)" (comillas dobles)
- Total: ~181 PDFs (2009-2016; 2007-2008 vacíos)
- 2017-2025:
/organos-colegiados/ciaf/informes-finales-de-sucesos-investigados/infofin-YYYY
- Ej:
/infofin-2025
- PDFs en
/recursos_mfom/paginabasica/recursos/XXXX-YY-ZZZZ-if-*.pdf
- Total: ~38 PDFs (ya descargados)
- NO usar filtros GET (
?field_ciaf_anyo_value=2015) — no funcionan, solo devuelven menú.
- TOTAL REAL: 277 informes (ya descargados en
/root/workspace/CIAF/YYYY/ desde 2007 hasta 2025). Distribución: 2007:4, 2008:53, 2009:43, 2010:28, 2011:24, 2012:22, 2013:23, 2014:14, 2015:10, 2016:11, 2017:12, 2018:2, 2019:3, 2020:3, 2021:6, 2022:5, 2023:3, 2024:3, 2025:1.
⚠️ TRES ERAS DE FORMATO (CRÍTICO para el parser):
Los informes tienen 3 formatos distintos según la normativa vigente:
- Pre-RD 810/2007 (2007-2008): Formato libre, secciones variables (Antecedentes/Hechos/Análisis)
- RD 810/2007 (2009-2013): Secciones 1-5 (Resumen, Descripción, Análisis, Conclusiones, Recomendaciones)
- RD 623/2014 (2014-2025): Secciones 0-6 (Abreviaturas, Resumen, Descripción, Análisis, Conclusiones, Recomendaciones, Anexos)
- Normativa de referencia:
/root/workspace/CIAF/normativa/04-RD_623_2014_ciaf.pdf — Art. 15 define la estructura obligatoria del informe
- Detectar era por año antes de parsear. Los 2007-2008 son los más irregulares.
📐 Calidad profesional — requisito mínimo para herramientas de equipo (VERIFICADO 2026-06-26):
Cuando el usuario dice "tengo que enviárselo al equipo y crearles una herramienta que puedan usar ellos", el estándar es calidad de producción, no prototype. Checklist mínimo:
- 100% de informes con título descriptivo (no "Informe NN/YYYY")
- 0% HTML como texto plano en el frontend (
<strong> se renderiza como bold, no como literal)
- 0% etiquetas duplicadas en el panel de detalle
- Nombres de campos consistentes entre parser→index→frontend (mismo idioma, mismo nombre)
- Estaciones limpias (sin frases del PDF como nombre)
- Fechas completas (sin campos vacíos en informes con PDF fuente)
- Enlaces funcionales (PDF local + enlace oficial CIAF)
Si algún dato es visible para el usuario final, debe ser correcto y presentable. No vale "más o menos" — el usuario lo envía a su equipo y se juega su credibility.
📊 Memorias anuales:
- URL:
/organos-colegiados/ciaf/memorias-anuales/memoriasanuales
- 17 memorias (2008-2024), guardadas en
/root/workspace/CIAF/memorias/
- PDFs en el HTML directamente (no AJAX) — buscar
href='(/recursos_mfom/[^']+\\.pdf)'
- Pattern de PDFs: múltiples rutas (
/recursos_mfom/, /recursos_mfom/pdf/UUID/, /recursos_mfom/listado/recursos/)
- NO hay memoria de 2011 — sí existe, pero se saltó en script anterior. Verificar siempre.
- 📄 Normativa: 7 PDFs en
/root/workspace/CIAF/normativa/ — incluye RD 623/2014 que define estructura de informes
📐 Preferencia de arquitectura (verificado):
- JSON como fuente de verdad (no MD, no SQLite): GitHub Pages sirve JSON estático
- JSON particionado por año:
data/reports/YYYY.json + data/index.json ligero (~50KB para 500 registros)
- relations.json para cruzar entidades × informes × recomendaciones
- Imágenes extraídas de PDFs con
pdfimages (poppler) + pdftoppm como fallback
- Coherencia entre memorias anuales e informes individuales — verificar que los datos coinciden
- Auto-import: pipeline
sync.py que detecta nuevos PDFs en la web y los procesa automáticamente
2. Informes de la CIAIAC (aviación)
3. Informes de la CIAIM (marítimo)
Arquitectura para datasets grandes (>100 registros, GitHub Pages)
Cuando hay 200+ registros, NO usar un solo JSON gigante. Usar JSON particionado por año:
data/
├── index.json ← Índice ligero (todos los IDs, metadatos mínimos, ~50KB)
├── reports/
│ ├── 2009.json ← Registros del 2009
│ ├── 2010.json ← Registros del 2010
│ └── ...
├── relations.json ← Entidades × registros × relaciones
└── images/
├── 2009/
│ └── IF-001-2009-fig01.png
└── ...
Por qué funciona:
index.json se carga una vez para mapa + filtros (50KB para 500 registros)
reports/YYYY.json se carga solo al hacer clic en un registro de ese año
- GitHub Pages sirve JSON estático sin backend
- Escalable a 500+ registros sin problemas de rendimiento
Por qué NO SQLite en navegador:
- GitHub Pages no sirve SQLite — necesitarías sql.js (500KB) + WebAssembly
- Para <1000 registros, JSON con fetch es suficiente
- JSON es más fácil de mantener y versionar con git
Pipeline genérico (paso a paso)
Paso 1: Descubrimiento de fuentes
curl -sL 'https://www.transportes.gob.es/organos-colegiados/ciaf/informes-finales-de-sucesos-investigados' \
-H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'
curl ... | grep -oP 'href="[^"]*\.pdf"' | sort -u
⚠️ 4 patrones de URL para PDFs en transportes.gob.es:
recursos_mfom/paginabasica/recursos/XXXX-YY-ZZZZ-if-*.pdf (2017-2025)
recursos_mfom/pdf/UUID-UUID/UUID/FILENAME.pdf (2015-2016)
recursos_mfom/YYMMDD-YYMMYY-if-*.pdf (2015-2016)
recursos_mfom/comodin/recursos/YYMMDDYYMMYYif*.pdf (2016)
⚠️ 2 patrones de URL por año:
- 2015-2016:
/informes-finales-de-sucesos-investigados/AÑO
- 2017-2025:
/informes-finales-de-sucesos-investigados/infofin-AÑO
- NO usar filtros GET (
?field_ciaf_anyo_value=2015) — no funcionan.
Paso 2: Scraping masivo
- Frecuencia: Cron semanal (domingo 06:00 UTC)
- Detección de nuevos: Comparar hashes de URLs con base de datos local
- Velocidad: 1 PDF/sec es suficiente para cientos de informes
Paso 3: Extracción de texto
Paso 3b: Extracción por páginas vs regex completo (VERIFICADO 2026-06-26)
⚠️ PATRÓN CRÍTICO: Extraer por páginas, NO por regex sobre texto completo.
El enfoque regex sobre texto completo falla porque:
- El TOC (índice con puntos
.....39) se confunde con contenido real
- Headers/footers de cada página se mezclan con el contenido
- Secciones de diferentes idiomas (inglés al final) contaminan la extracción
Enfoque correcto — extracción por páginas:
def extract_pages(pdf_path: str) -> list[str]:
"""Extrae texto de cada página por separado."""
doc = fitz.open(pdf_path)
pages = [page.get_text() for page in doc]
doc.close()
return pages
def find_section_pages(pages: list[str]) -> dict[int, int]:
"""Encuentra en qué página empieza cada sección (saltando TOC)."""
section_pages = {}
for i, text in enumerate(pages):
if i < 2:
continue
if re.search(r'\.{10,}', text):
continue
for m in re.finditer(r'(?:^|\n)\s*(\d+)\.\s+([A-ZÁÉÍÓÚÑ][A-ZÁÉÍÓÚÑ\s]{5,40})', text):
num = int(m.group(1))
if num not in section_pages:
section_pages[num] = i
return section_pages
def get_pages_text(pages, start_page, end_page):
"""Concatena páginas limpiando headers/footers individuales."""
text = ""
for i in range(start_page, min(end_page, len(pages))):
page_text = pages[i]
page_text = re.sub(r'Comisión de Investigación de\s*Accidentes Ferroviarios', '', page_text)
page_text = re.sub(r'Informe Final de la CIAF\s+\d+/\d{4}', '', page_text)
page_text = re.sub(r'^\s*\d{1,2}\s*$', '', page_text, flags=re.MULTILINE)
page_text = re.sub(r'^.*\.{10,}.*$', '', page_text, flags=re.MULTILINE)
text += page_text + "\n"
return text.strip()
Ventajas comprobadas:
- 2024: 3/3 títulos, 3/3 conclusiones, 3/3 recomendaciones (vs 0/3 con regex completo)
- 2009: 43/43 títulos, 38/43 conclusiones, 25/43 recomendaciones
- Maneja 3 eras de formato automáticamente
Detección de TOC: Líneas con 10+ puntos consecutivos (.{10,}) → saltar página completa
Manejo de bilingüismo: Los informes 2014+ tienen sección en inglés al final. Cortar extracción de recomendaciones antes de "SAFETY RECOMMENDATIONS" o "English summary":
for i in range(start + 1, min(start + 5, len(pages))):
if re.search(r'SAFETY\s+RECOMMENDATIONS|English\s+summary', pages[i], re.IGNORECASE):
end = i
break
Script completo: /root/workspace/CIAF-visor/scripts/parse_year_v2.py — parser funcional con extracción por páginas, geocoding local, y manejo de 3 eras. Ver también: references/page-based-pdf-extraction.md, references/station-coords-geocoding.md, y references/ciaf-memoria-parsing.md (parseo de memorias anuales, diferente de informes individuales).
Paso 3c: Extracción semántica de campos
El mayor reto NO es extraer texto del PDF (PyMuPDF lo hace bien), sino estructurar el texto libre en campos JSON.
⚠️ PARADIGMA SHIFT (2026-06): Para PDFs digitales con texto seleccionable, el enfoque LLM (pdf-llm-extraction) supera cualitativamente a los regex:
- LLM: 270/270 conclusiones (100%), 268/270 recomendaciones (99%), 381 trenes
- Regex: 194/270 conclusiones (72%), 149/270 recomendaciones (55%), 0 trenes
Usar pdf-llm-extraction para batch processing de PDFs digitales. Los regex de abajo quedan como referencia para entender la estructura de los informes, pero no son la herramienta principal recomendada.
Patrones verificados con CIAF (regex, referencia histórica):
Expediente/informe — 4 patrones de título según era (VERIFICADO 2026-06-26):
Los informes CIAF tienen 4 formatos de título distintos. Detectar por orden de prioridad:
m = re.search(r'Investigaci[oó]n del accidente.*?ocurrido.*?(\d{1,2})[/.-](\d{1,2})[/.-](\d{4})', text[:2000])
m = re.search(r'(?:CIAF|Informe)\s+(?:N[º°]|n[º°])\s*(\d+/\d{4})', text[:2000])