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

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;11Salida 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
| Caso | Entrada | Salida esperada | Salida real | Resultado |
|---|---|---|---|---|
| Ejemplo del enunciado | 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 | 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 | 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 | OK |
| Entrada vacía | (vacía) | VALIDAS: 0
RECHAZADAS: 0 | VALIDAS: 0
RECHAZADAS: 0 | OK |
| Solo líneas en blanco |
| VALIDAS: 0
RECHAZADAS: 0 | VALIDAS: 0
RECHAZADAS: 0 | OK |
| Todas rechazadas | texto sin formato
12345678;Sin Letra, Ana;PRG-101;5 | VALIDAS: 0
RECHAZADAS: 2
RECHAZADA 1
RECHAZADA 2 | VALIDAS: 0
RECHAZADAS: 2
RECHAZADA 1
RECHAZADA 2 | OK |
| Empate en la mejor nota y límites | 12345678Z;Núñez Gil, Íñigo;BIO-300;9.99
87654321X;Vidal Roca, Marta-Luz;BIO-300;9.99
00000000T;Ortega Sanz, Lola;BIO-300;0 | VALIDAS: 3
RECHAZADAS: 0
BIO-300: alumnos=3 media=6.66
MEJOR: 12345678Z Íñigo Núñez Gil 9.99 | VALIDAS: 3
RECHAZADAS: 0
BIO-300: alumnos=3 media=6.66
MEJOR: 12345678Z Íñigo Núñez Gil 9.99 | OK |
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.matchore.searchsin anclas: una línea con basura al final (por ejemplo...;7.5;xx) se acepta porque la coincidencia parcial tiene éxito. Confullmatchno 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
\wo[A-z]:\wadmite 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 pasar7.123o10.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
- 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.
- Publicación original.
Artículo generado mediante AI.larebelion · Revisado por Kernel


