Cómo filtrar 50k licitaciones de PLACSP en Python sin timeouts ni XML CODICE
Procesar las licitaciones públicas de la Plataforma de Contratación del Sector Público (PLACSP) en España suele convertirse rápidamente en un cuello de botella técnico. El volumen diario de datos agregados supera decenas de miles de expedientes distribuidos en grandes volcados XML basados en el estándar CODICE (formato UBL/XML con esquemas complejos y anidados).
Para desarrolladores y equipos técnicos que necesitan extraer oportunidades o monitorizar contrataciones sin bloquear sus servidores ni agotar la memoria, estructurar un pipeline de ingesta optimizado es fundamental.
A continuación, detallamos la arquitectura técnica de un pipeline en Python diseñado para procesar más de 50.000 expedientes públicos de forma eficiente y fiable.
1. El reto técnico del feed PLACSP
La contratación pública en España publica sus datos en canales de distribución Atom y paquetes masivos en formato XML CODICE. Trabajar con estos volcados plantea varios retos críticos:
- Tamaño y estructura XML anidada: Analizar árboles DOM completos con librerías estándar en memoria (
xml.etree.ElementTreeominidom) agota la memoria RAM al parsear archivos de cientos de megabytes. - Timeouts en peticiones HTTP: Las conexiones directas contra los puntos de publicación del sector público pueden experimentar latencias elevadas o cierres inesperados durante horas punta.
- Complejidad del esquema CODICE: La información clave (presupuesto base, CPVs, solvencia técnica, fecha límite de presentación y pliegos) se encuentra dispersa en nodos profundamente ramificados.
2. Streaming parsing con iterparse
Para evitar cargar el archivo XML completo en memoria, la técnica recomendada es el parseo iterativo por eventos (streaming parsing) utilizando iterparse de lxml o xml.etree.ElementTree. De esta forma, cada expediente o entrada de licitación (<ContractFolderStatus>) se procesa y libera inmediatamente de la memoria.
import xml.etree.ElementTree as ET def procesar_feed_licitaciones(xml_stream): """ Parsea de forma iterativa las licitaciones liberando memoria tras cada nodo. """ contexto = ET.iterparse(xml_stream, events=("end",)) _, root = next(contexto) # Obtener el elemento raíz para limpiar referencias for event, elem in contexto: # Etiqueta base de expediente en esquema CODICE if elem.tag.endswith("ContractFolderStatus"): licitacion = extraer_datos_expediente(elem) if licitacion and filtrar_criterios(licitacion): yield licitacion # Liberar memoria del nodo procesado y sus predecesores elem.clear() root.clear() 3. Normalización y aplanado a estructuras limpias
En lugar de propagar estructuras XML o árboles heterogéneos por la aplicación, normalizamos cada expediente a un diccionario o modelo plano con los campos esenciales para indexación y filtrado:
def extraer_datos_expediente(elem): """ Extrae identificador, objeto, presupuesto, fecha límite y CPV principal. """ ns = {"cac": "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2", "cbc": "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"} id_elem = elem.find(".//cbc:ContractFolderID", ns) titulo_elem = elem.find(".//cbc:Name", ns) importe_elem = elem.find(".//cbc:TaxExclusiveAmount", ns) fecha_limite = elem.find(".//cbc:EndDate", ns) return { "id": id_elem.text.strip() if id_elem is not None else None, "titulo": titulo_elem.text.strip() if titulo_elem is not None else "", "importe": float(importe_elem.text) if importe_elem is not None and importe_elem.text else 0.0, "fecha_limite": fecha_limite.text.strip() if fecha_limite is not None else None, } 4. Filtrado eficiente por criterios de negocio
El filtrado en tiempo de ingesta permite descartar inmediatamente el 90% de expedientes irrelevantes (por ejemplo, aquellos fuera de presupuesto, sin plazo de presentación activo o sin encaje geográfico o temático) antes de persistir en base de datos:
| Criterio de filtro | Estrategia técnica | Beneficio en rendimiento |
|---|---|---|
| Importe / Presupuesto | Comprobación de rango numérico (min <= importe <= max) | Descarta contratos desproporcionados o fuera de alcance |
| Fecha de cierre | Comparación directa contra timestamp UTC actual | Evita persistir licitaciones ya expiradas |
| Códigos CPV | Prefiltro por conjunto de códigos (set lookup O(1)) | Segmentación temáticamente precisa sin escaneo de texto completo |
5. Arquitectura de alertas y automatización
Mantener esta infraestructura en producción requiere configurar tareas programadas de sincronización incremental, gestión de reintentos con exponential backoff para evitar cortes por timeouts, y canalización de eventos hacia sistemas de notificación.
Para empresas y profesionales que buscan recibir oportunidades cualificadas directamente en su bandeja de entrada sin necesidad de mantener su propio scraper y pipeline de datos, en Licitaciones Alerta (licitacionesalerta.es) ofrecemos alertas personalizadas y segmentadas de la PLACSP con planes desde 29 €/mes adaptados a la capacidad de cada pyme.

