Saltar al contenido
ES EN
Tema: automático

Ejercicio resuelto: validar y extraer datos con regex en Python

Ejercicio resuelto en Python: leer un registro de matrículas desde STDIN, validar cada línea con expresiones regulares y resumir los datos válidos.

Actualizado el

Código de Examen Python Fundamentos de programación Nivel examen Código probado

Código de Examen: Ejercicio resuelto: validar y extraer datos con regex en Python
Fundamentos de Programación · Examen finalEjercicio 4 · 3,0 puntos · 30 min

La secretaría de una facultad exporta cada noche un fichero de texto con las calificaciones de una convocatoria. Cada línea debería tener la forma DNI;Apellidos, Nombre;ASIGNATURA;nota, pero el fichero se edita a mano y contiene errores. Debes escribir un programa que lea todas las líneas de la entrada estándar, valide cada una y produzca un resumen.

Una línea es válida si cumple todo lo siguiente. (1) El DNI son 8 dígitos seguidos de una letra mayúscula, y la letra es la correcta: se calcula como la posición número % 23 en la cadena TRWAGMYFPDXBNJZSQVHLCKE. (2) El nombre tiene el formato Apellidos, Nombre, con una coma seguida de un espacio, y solo letras (incluidas tildes y ñ), espacios y guiones. (3) La asignatura es un código de tres letras mayúsculas, un guion y tres dígitos (por ejemplo PRG-101). (4) La nota es un número entre 0 y 10 con como máximo dos decimales, usando punto como separador decimal.

Se ignoran los espacios al principio y al final de cada línea, y las líneas vacías no cuentan ni como válidas ni como rechazadas. Las líneas rechazadas se numeran empezando en 1 sobre el total de líneas de la entrada (las vacías también ocupan número).

Salida, exactamente en este orden: la línea VALIDAS: n; la línea RECHAZADAS: m; y, si hay rechazadas, una línea RECHAZADA k por cada una, con su número de línea en orden creciente. Después, para cada asignatura con al menos una línea válida, ordenadas alfabéticamente, una línea ASIGNATURA: alumnos=a media=x.xx con la media aritmética de sus notas válidas con dos decimales. Por último, la línea MEJOR: DNI Nombre Apellidos nota con el alumno de mayor nota entre las válidas (en caso de empate, el primero que aparece en la entrada), donde Nombre y Apellidos se escriben invirtiendo el orden original y la nota con dos decimales. Si no hay ninguna línea válida, no se escribe ni la sección de asignaturas ni la línea MEJOR.

Entrada de ejemplo

12345678Z;García López, María;PRG-101;8.5
  87654321X;Pérez Ruiz, Juan;PRG-101;6.25  

12345678A;Gómez Díaz, Luis;MAT-200;7
00000000T;Martín Vega, Elena;MAT-200;10
11111111H;Ruiz,Ana;PRG-101;5
23456789D;Soler Mas, Pau;PRG-101;11

Salida esperada

VALIDAS: 3
RECHAZADAS: 3
RECHAZADA 4
RECHAZADA 6
RECHAZADA 7
MAT-200: alumnos=1 media=10.00
PRG-101: alumnos=2 media=7.38
MEJOR: 00000000T Elena Martín Vega 10.00
Expresión regular correcta para el formato de la línea (grupos con nombre, anclas, rangos)
1,0
Validación de las reglas que la regex no puede expresar (letra de control del DNI, rango de nota)
0,75
Lectura de STDIN línea a línea, tratando líneas vacías y espacios sobrantes
0,5
Agregación y formato exacto de la salida (orden, redondeo, listado de rechazadas)
0,75

Pistas

Pista 1 · ¿Qué parte del problema es de la regex y cuál no?

Una expresión regular describe formas, no cálculos. Compila una sola regex con re.compile y grupos con nombre ((?P<dni>...)) para la estructura de la línea, y comprueba aparte, en código normal, que la letra del DNI coincide con int(numero) % 23.

Pista 2 · ¿Cómo anclar y cómo tratar los caracteres acentuados?

Usa re.fullmatch (o anclas ^ y $) para que no valga una coincidencia parcial. Para letras con tilde, la clase [A-Za-zÁÉÍÓÚÜÑáéíóúüñ] es explícita y predecible. Para la nota, piensa en alternativas: 10 con ceros opcionales, o un dígito del 0 al 9 con decimales opcionales.

Pista 3 · ¿Qué estructuras de datos necesitas para el resumen?

Un diccionario de asignatura a lista de notas te da recuento y media. Para MEJOR, guarda una tupla con la mejor nota vista y compara con > estricto, de modo que un empate no sustituya al primero. Recuerda que el número de línea se cuenta con enumerate(..., 1) antes de descartar las vacías.

Solución

Solución explicada

La solución separa dos responsabilidades. La función parsear recibe una línea ya recortada y devuelve una tupla con los campos si es válida, o None si no lo es. El programa principal recorre la entrada, acumula resultados y formatea la salida.

La regex captura cuatro grupos con nombre y está compilada una sola vez a nivel de módulo. La letra de control se verifica fuera de la regex, porque depende de un cálculo aritmético sobre el número.

import re
import sys

LETRAS = "TRWAGMYFPDXBNJZSQVHLCKE"
LETRA_NOMBRE = "A-Za-zÁÉÍÓÚÜÑáéíóúüñ"

PATRON = re.compile(
    r"(?P<num>\d{8})(?P<letra>[A-Z]);"
    r"(?P<apellidos>[" + LETRA_NOMBRE + r"][" + LETRA_NOMBRE + r" -]*), "
    r"(?P<nombre>[" + LETRA_NOMBRE + r"][" + LETRA_NOMBRE + r" -]*);"
    r"(?P<asig>[A-Z]{3}-\d{3});"
    r"(?P<nota>10(?:\.0{1,2})?|\d(?:\.\d{1,2})?)"
)


def parsear(linea):
    m = PATRON.fullmatch(linea)
    if m is None:
        return None
    if LETRAS[int(m.group("num")) % 23] != m.group("letra"):
        return None
    dni = m.group("num") + m.group("letra")
    return dni, m.group("apellidos"), m.group("nombre"), m.group("asig"), float(m.group("nota"))


def main():
    notas = {}
    rechazadas = []
    validas = 0
    mejor = None
    for numero, cruda in enumerate(sys.stdin.read().splitlines(), 1):
        linea = cruda.strip()
        if not linea:
            continue
        datos = parsear(linea)
        if datos is None:
            rechazadas.append(numero)
            continue
        validas += 1
        dni, apellidos, nombre, asig, nota = datos
        notas.setdefault(asig, []).append(nota)
        if mejor is None or nota > mejor[3]:
            mejor = (dni, nombre, apellidos, nota)
    print(f"VALIDAS: {validas}")
    print(f"RECHAZADAS: {len(rechazadas)}")
    for k in rechazadas:
        print(f"RECHAZADA {k}")
    for asig in sorted(notas):
        lista = notas[asig]
        media = sum(lista) / len(lista)
        print(f"{asig}: alumnos={len(lista)} media={media:.2f}")
    if mejor is not None:
        dni, nombre, apellidos, nota = mejor
        print(f"MEJOR: {dni} {nombre} {apellidos} {nota:.2f}")


main()

La constante PATRON se construye concatenando fragmentos para que sea legible. Los grupos num y letra separan los ocho dígitos de la letra, de modo que parsear pueda calcular int(m.group("num")) % 23 y compararlo con LETRAS. Los grupos apellidos y nombre exigen empezar por letra y admiten espacios y guiones; la coma y el espacio que los separan forman parte literal del patrón, por eso Ruiz,Ana se rechaza.

El grupo nota usa dos alternativas: 10(?:\.0{1,2})? para el diez (con decimales cero opcionales) y \d(?:\.\d{1,2})? para un dígito de 0 a 9 con hasta dos decimales. Así quedan fuera 10.5, 7.123 y 07. Al usar fullmatch no hace falta poner anclas y se evita el error clásico de aceptar una coincidencia parcial.

En main, enumerate(..., 1) numera todas las líneas antes de filtrar las vacías, tal como pide el enunciado. Las válidas alimentan el diccionario notas (asignatura a lista de notas) y la variable mejor, que solo se actualiza con nota > mejor[3]: la comparación estricta garantiza que en un empate se conserve el primero de la entrada. La salida recorre sorted(notas) para el orden alfabético y formatea la media con :.2f.

En el test del enunciado hay seis líneas útiles: tres válidas y tres rechazadas (letra de DNI incorrecta, nota 11 y falta de espacio tras la coma). En el test con solo líneas vacías el programa imprime únicamente los dos contadores a cero, y en el test del empate se comprueba que MEJOR conserva al primer alumno.

Casos de prueba

CasoEntradaSalida esperadaSalida realResultado
Ejemplo del enunciado12345678Z;García López, María;PRG-101;8.5 87654321X;Pérez Ruiz, Juan;PRG-101;6.25 12345678A;Gómez Díaz, Luis;MAT-200;7 00000000T;Martín Vega, Elena;MAT-200;10 11111111H;Ruiz,Ana;PRG-101;5 23456789D;Soler Mas, Pau;PRG-101;11VALIDAS: 3 RECHAZADAS: 3 RECHAZADA 4 RECHAZADA 6 RECHAZADA 7 MAT-200: alumnos=1 media=10.00 PRG-101: alumnos=2 media=7.38 MEJOR: 00000000T Elena Martín Vega 10.00VALIDAS: 3 RECHAZADAS: 3 RECHAZADA 4 RECHAZADA 6 RECHAZADA 7 MAT-200: alumnos=1 media=10.00 PRG-101: alumnos=2 media=7.38 MEJOR: 00000000T Elena Martín Vega 10.00OK
Entrada vacía(vacía)VALIDAS: 0 RECHAZADAS: 0VALIDAS: 0 RECHAZADAS: 0OK
Solo líneas en blanco VALIDAS: 0 RECHAZADAS: 0VALIDAS: 0 RECHAZADAS: 0OK
Todas rechazadastexto sin formato 12345678;Sin Letra, Ana;PRG-101;5VALIDAS: 0 RECHAZADAS: 2 RECHAZADA 1 RECHAZADA 2VALIDAS: 0 RECHAZADAS: 2 RECHAZADA 1 RECHAZADA 2OK
Empate en la mejor nota y límites12345678Z;Núñez Gil, Íñigo;BIO-300;9.99 87654321X;Vidal Roca, Marta-Luz;BIO-300;9.99 00000000T;Ortega Sanz, Lola;BIO-300;0VALIDAS: 3 RECHAZADAS: 0 BIO-300: alumnos=3 media=6.66 MEJOR: 12345678Z Íñigo Núñez Gil 9.99VALIDAS: 3 RECHAZADAS: 0 BIO-300: alumnos=3 media=6.66 MEJOR: 12345678Z Íñigo Núñez Gil 9.99OK

Salidas reales: código compilado con Python 3.13.16 y ejecutado en un contenedor aislado el 3 de noviembre de 2026.

Complejidad

El tiempo es lineal en el tamaño total de la entrada: cada línea se procesa una vez, y la regex, al no tener cuantificadores anidados ni alternativas ambiguas, se evalúa en tiempo lineal sobre la longitud de la línea. La ordenación final de asignaturas cuesta O(k log k) con k asignaturas distintas, despreciable frente a la lectura.

La memoria es O(n) en el peor caso, porque se leen todas las líneas con sys.stdin.read() y se guardan las notas válidas por asignatura. Si el fichero fuera enorme, bastaría iterar sobre sys.stdin y guardar solo suma y recuento por asignatura, con memoria O(k).

Errores típicos

  • Usar re.match o re.search sin anclas: una línea con basura al final (por ejemplo ...;7.5;xx) se acepta porque la coincidencia parcial tiene éxito. Con fullmatch no ocurre.
  • Intentar validar la letra del DNI dentro de la regex, o no validarla en absoluto: la regex solo comprueba que hay 8 dígitos y una mayúscula, no que la letra sea la correcta.
  • Escribir la clase de letras como \w o [A-z]: \w admite dígitos y guion bajo, y [A-z] incluye símbolos como corchetes y acento circunflejo entre la Z y la a.
  • Numerar las líneas rechazadas después de descartar las vacías, con lo que los números no coinciden con la posición real en el fichero.
  • Actualizar MEJOR con >=, lo que hace que un empate sustituya al primer alumno en lugar de conservarlo.
  • Olvidar escapar el punto decimal (. sin barra acepta cualquier carácter) o no limitar los decimales, dejando pasar 7.123 o 10.5.

Variantes

Aceptar coma o punto como separador decimal

Cambia la parte decimal del grupo nota a [.,] y, antes de convertir, sustituye la coma por punto con .replace(',', '.'). Hay que decidir si el fichero puede mezclar ambos formatos; la regex no lo impide, así que el enunciado debe aclararlo.

Leer de un fichero indicado por argumento en lugar de STDIN

Sustituye sys.stdin.read() por open(sys.argv[1], encoding='utf-8') dentro de un with y recorre el fichero con enumerate(f, 1). Al iterar el objeto fichero se lee línea a línea sin cargarlo entero en memoria, y el with garantiza el cierre aunque haya una excepción.

Rechazar además DNI duplicados

Mantén un conjunto vistos con los DNI ya aceptados. Tras validar una línea, si su DNI está en vistos se trata como rechazada; si no, se añade. Pregúntate si el duplicado debe contarse por DNI solo o por pareja DNI y asignatura, que son reglas distintas.

En los temarios

El manejo de archivos y la validación de datos de entrada aparecen en el temario de varios cursos introductorios de programación. El programa de Fundamentos de Programación del Instituto Politécnico Nacional (México) incluye sentencias de control, funciones, apuntadores y archivos entre sus contenidos: programa de la unidad de aprendizaje.

En la Universidad de Chile, el curso CC3301 Programación de Software de Sistemas recoge el uso de la API de Linux para el manejo de archivos y directorios: programa del curso. Este ejercicio trabaja la misma idea (leer y procesar ficheros de texto) desde Python, con expresiones regulares como herramienta de validación.

Historial de cambios

  1. Reescrito con la plantilla de Código de Examen: enunciado de examen, pistas, solución explicada, casos de prueba y código compilado y ejecutado; versión en inglés enlazada.
  2. Publicación original.

Artículo generado mediante AI.larebelion · Revisado por Kernel

Kernel

· Dirección de tecnología · España

«Una regex describe formas; las reglas con aritmética se comprueban en código, y mezclarlas es la manera más rápida de escribir algo que nadie puede mantener.»