Archivos de texto en Python — ejercicios con contadores de líneas, filtros de carreras y un diario que hay que normalizar (y la línea vacía que paró el bucle a la mitad)
El último día de octubre quise ver todas mis sesiones del mes con un programa que copié del esquema de lectura: linea = f.readline().rstrip() y un while linea != ''. Me salieron dos sesiones. Dos. Pensé que se me había olvidado apuntar casi todo el mes, hasta que abrí el archivo y vi las otras cuatro ahí, tan tranquilas, después de una línea en blanco. El programa no tenía ningún error de sintaxis: simplemente se paraba en la primera línea vacía. Este es el fragmento 3 de «¿Qué falla aquí?», y te lo explico con calma.
Estos ejercicios cierran el tema de archivos de texto y, con él, el temario de FP1. Antes, la teoría y la práctica. Ahora te toca escribir el código a ti.
Tabla de Contenidos
Cómo usar este artículo
- Lee el enunciado y la salida esperada de cada nivel. Crea los archivos de datos copiándolos tal cual (las líneas vacías también cuentan).
- Intenta resolverlo sin mirar la solución. Si te atascas más de diez minutos, abre la pista 🔵.
- Haz los tres fragmentos de «¿Qué falla aquí?»: predice qué sale antes de leer la explicación.
- Compara tu solución con la mía y lee el «por qué está hecho así». Al final tienes el chuletario para repasar.
Los niveles 1 y 2 usan este octubre.txt:
2026-10-01;carrera;30 2026-10-02;natación;45 2026-10-04;carrera;25 2026-10-05;bici;60 2026-10-07;carrera;40 2026-10-09;yoga;20
Nivel 1 · Básico: contar el diario
Escribe una función contar(nombre) que lea el archivo y devuelva cuatro números: las líneas totales, las líneas vacías, las sesiones y los minutos totales. El programa principal los imprime con su etiqueta. Una línea es vacía si, tras quitarle el salto y los espacios, no queda nada.
Salida esperada:
Líneas: 8 Vacías: 2 Sesiones: 6 Minutos: 220
Recorre con for linea in f, suma 1 a lineas en todas las vueltas y usa continue para que las vacías no lleguen a la parte que suma minutos. Los minutos son el tercer campo: split(';')[2].
Nivel 2 · Intermedio: solo las carreras largas
Escribe filtrar_carreras(origen, destino, minimo), que escriba en destino las sesiones de carrera de al menos minimo minutos, numeradas (1. fecha (minutos min)), y una última línea con el total. Devuelve cuántas ha copiado. Llámala con octubre.txt, carreras.txt y 30, y después muestra carreras.txt con el esquema de readline().
Salida esperada:
Carreras copiadas: 2 1. 2026-10-01 (30 min) 2. 2026-10-07 (40 min) Total: 2 sesiones, 70 min
Abre los dos archivos en el mismo with, uno en 'r' y otro en 'w'. El bucle recorre el origen. La línea vacía, tras strip() y split(';'), es ['']: tiene un solo campo.
Nivel 3 · Desafío: normalizar un diario escrito a mano
Mi compañero de piso apunta sus entrenos como le da la gana: a veces separa con ;, otras con , o con | , con o sin espacios, y a veces escribe 45min o 25 min. Este es su sucio.txt:
2026-10-01;carrera;30 2026-10-02, natación, 45min 2026-10-04 | carrera | 25 min sin datos hoy 2026-10-05;bici;60 2026-10-07,carrera,40
Escribe normalizar(origen, destino). Para cada línea que sea una sesión, escribe en destino la versión normalizada fecha;deporte;minutos, seguida de <- y la línea original. Las líneas que no sean sesiones se ignoran. Al final, una línea con diez > y otra con Normalizadas: y el número de líneas que han cambiado, y , Total: y el número de sesiones, los dos números en un campo de 3 caracteres. La función devuelve la tupla (cambiadas, total). Imprime esa tupla y el archivo resultante.
Salida esperada:
(3, 5) 2026-10-01;carrera;30 <- 2026-10-01;carrera;30 2026-10-02;natación;45 <- 2026-10-02, natación, 45min 2026-10-04;carrera;25 <- 2026-10-04 | carrera | 25 min 2026-10-05;bici;60 <- 2026-10-05;bici;60 2026-10-07;carrera;40 <- 2026-10-07,carrera,40 >>>>>>>>>> Normalizadas: 3, Total: 5
Es el momento de las expresiones regulares: un patrón con tres grupos (fecha, deporte, minutos), un separador [;,|] rodeado de \s* y un (?:min)? opcional al final. Con fullmatch la línea entera tiene que encajar. Para el ancho de 3, f'{n:3d}'.
¿Qué falla aquí?
Tres programas cortos que parecen correctos. Lee cada uno y predice qué sale antes de seguir leyendo.
Fragmento 1
Queremos copiar tres.txt (tres sesiones) en copia.txt:
# Copiar un archivo línea a línea
origen = open('tres.txt', 'r', encoding='utf-8')
destino = open('copia.txt', 'w', encoding='utf-8')
for linea in origen:
print(linea, file=destino)
origen.close()
destino.close()
f = open('copia.txt', 'r', encoding='utf-8')
print(repr(f.read()))
f.close()
¿Qué contendrá la copia? Esto es lo que sale:
'2026-10-05;carrera;35\n\n2026-10-06;bici;50\n\n2026-10-08;natación;40\n\n'
Hay un '\n' doble detrás de cada sesión: la copia tiene líneas en blanco que el original no tenía. Cada linea ya trae su '\n', y print() añade otro al escribir en el archivo, igual que cuando imprime en pantalla.
Copiar con print(linea, file=destino). El salto de la línea más el de print() dan dos saltos.
El arreglo es escribir la línea tal cual, con write():
# Copiar un archivo línea a línea (arreglado)
origen = open('tres.txt', 'r', encoding='utf-8')
destino = open('copia.txt', 'w', encoding='utf-8')
for linea in origen:
destino.write(linea) # la línea ya trae su '\n': se escribe tal cual
origen.close()
destino.close()
f = open('copia.txt', 'r', encoding='utf-8')
print(repr(f.read()))
f.close()
Ahora la copia es idéntica al original:
'2026-10-05;carrera;35\n2026-10-06;bici;50\n2026-10-08;natación;40\n'
print(linea, end='', file=destino) también vale. Quédate con la idea: lo que lees ya lleva su salto, así que no añadas otro.
Fragmento 2
Apuntamos la sesión de hoy en hoy.txt:
# Apuntar la sesión de hoy
fecha, deporte, minutos = '2026-10-20', 'bici', 55
with open('hoy.txt', 'w', encoding='utf-8') as f:
f.write(fecha + ';' + deporte + ';')
f.write(minutos)
¿Qué crees que pasa? Esto:
Traceback (most recent call last):
File "apuntar.py", line 5, in <module>
f.write(minutos)
~~~~~~~^^^^^^^^^
TypeError: write() argument must be str, not int
write() solo acepta strings, y minutos es el entero 55. El mensaje lo dice con todas las letras: el argumento tiene que ser str y es int. Fíjate en los ^^^ del traceback: señalan justo el argumento minutos. Y hay un segundo fallo escondido, que no llega a verse porque el programa revienta antes: aunque fuera una string, no hay '\n' al final.
Pasarle a write() un número, o una línea sin su salto. print() hace las dos cosas por ti; write(), ninguna.
Arreglado, con la conversión y el salto, y leyendo después lo que ha quedado:
# Apuntar la sesión de hoy (arreglado)
fecha, deporte, minutos = '2026-10-20', 'bici', 55
with open('hoy.txt', 'w', encoding='utf-8') as f:
f.write(fecha + ';' + deporte + ';')
f.write(str(minutos) + '\n') # write solo acepta str, y el '\n' no lo pone él
with open('hoy.txt', 'r', encoding='utf-8') as f:
print(repr(f.read()))
El archivo queda con una línea completa:
'2026-10-20;bici;55\n'
Con una f-string no hay que acordarse de str(): f.write(f'{fecha};{deporte};{minutos}\n').
Fragmento 3
El de mi anécdota. Queremos mostrar octubre.txt sin los saltos de línea:
# Mostrar el diario sin los saltos de línea
def mostrar(nombre):
f = open(nombre, 'r', encoding='utf-8')
linea = f.readline().rstrip()
while linea != '':
print(linea)
linea = f.readline().rstrip()
f.close()
mostrar('octubre.txt')
¿Cuántas sesiones salen? Solo dos:
2026-10-01;carrera;30 2026-10-02;natación;45
El rstrip() va pegado al readline(), así que la condición del while ya no mira lo que se leyó, sino lo que queda después de limpiarlo. La tercera línea del archivo es una línea vacía, '\n', y '\n'.rstrip() es '': exactamente el valor que significa «fin del archivo». El bucle se para y las cuatro sesiones siguientes nunca se leen.
Limpiar la línea en la misma instrucción que la lee. Con archivos sin líneas vacías funciona, y por eso engaña tanto: el día que aparece una línea en blanco, el programa deja de leer sin dar ningún error.
El arreglo: la condición mira la línea original y se limpia una copia dentro del bucle.
# Mostrar el diario sin los saltos de línea (arreglado)
def mostrar(nombre):
f = open(nombre, 'r', encoding='utf-8')
linea = f.readline()
while linea != '': # la condición mira la línea CON su '\n'
limpia = linea.rstrip() # limpio una copia para imprimirla
if limpia != '':
print(limpia)
linea = f.readline()
f.close()
mostrar('octubre.txt')
Ahora salen las seis sesiones, y la línea vacía no se imprime:
2026-10-01;carrera;30 2026-10-02;natación;45 2026-10-04;carrera;25 2026-10-05;bici;60 2026-10-07;carrera;40 2026-10-09;yoga;20
Con for linea in f este problema no existe: el bucle termina cuando se acaba el archivo, no cuando una línea parece vacía. Si no necesitas parar a mitad, usa for.
Soluciones
Mira las soluciones solo después de intentarlo. Cada una lleva los comentarios dentro del código y una explicación de por qué está hecha así.
Solución del Nivel 1
# Nivel 1 · Contar lo que hay en el diario
def contar(nombre):
lineas = vacias = sesiones = minutos = 0
with open(nombre, 'r', encoding='utf-8') as f:
for linea in f:
lineas += 1 # toda línea cuenta, también las vacías
limpia = linea.strip()
if limpia == '': # '\n' a secas (o solo espacios)
vacias += 1
continue
sesiones += 1
minutos += int(limpia.split(';')[2]) # el tercer campo son los minutos
return lineas, vacias, sesiones, minutos
lineas, vacias, sesiones, minutos = contar('octubre.txt')
print('Líneas:', lineas)
print('Vacías:', vacias)
print('Sesiones:', sesiones)
print('Minutos:', minutos)
Da exactamente la salida del enunciado. Por qué está hecho así:
lineas += 1va antes delif, para contar también las vacías. Si lo pusieras después delcontinue, las vacías no contarían como líneas y saldría 6.limpia = linea.strip()y norstrip('\n'): una línea con solo espacios también es vacía para nosotros.lineas = vacias = sesiones = minutos = 0pone las cuatro a cero en una línea.- Devolver una tupla y desempaquetarla fuera separa el cálculo de la impresión.
La traza de las ocho líneas:
| Línea | limpia | vacias | sesiones | minutos |
|---|---|---|---|---|
| 1 | '2026-10-01;carrera;30' | 0 | 1 | 30 |
| 2 | '2026-10-02;natación;45' | 0 | 2 | 75 |
| 3 | '' | 1 | 2 | 75 |
| 4 | '2026-10-04;carrera;25' | 1 | 3 | 100 |
| 5 | '2026-10-05;bici;60' | 1 | 4 | 160 |
| 6 | '2026-10-07;carrera;40' | 1 | 5 | 200 |
| 7 | '' | 2 | 5 | 200 |
| 8 | '2026-10-09;yoga;20' | 2 | 6 | 220 |
Cuentas: 30 + 45 + 25 + 60 + 40 + 20 = 220, y 6 sesiones + 2 vacías = 8 líneas.
Solución del Nivel 2
# Nivel 2 · Un archivo solo con las carreras largas
def filtrar_carreras(origen, destino, minimo):
"""Escribe en destino las sesiones de carrera de al menos 'minimo' minutos. Devuelve cuántas."""
n = total = 0
with open(origen, 'r', encoding='utf-8') as fin, open(destino, 'w', encoding='utf-8') as fout:
for linea in fin: # el bucle recorre el ORIGEN; el destino solo recibe
partes = linea.strip().split(';')
if len(partes) != 3: # línea vacía: [''] tiene un solo campo
continue
fecha, deporte, minutos = partes
if deporte == 'carrera' and int(minutos) >= minimo: # >=: 30 minutos justos también valen
n += 1
total += int(minutos)
fout.write(f'{n}. {fecha} ({minutos} min)\n')
fout.write(f'Total: {n} sesiones, {total} min\n')
return n
cuantas = filtrar_carreras('octubre.txt', 'carreras.txt', 30)
print('Carreras copiadas:', cuantas)
f = open('carreras.txt', 'r', encoding='utf-8')
linea = f.readline()
while linea != '':
print(linea, end='')
linea = f.readline()
f.close()
Por qué está hecho así:
- Los dos archivos en un solo
with: se cierran los dos al salir, y la línea final de totales se escribe dentro del bloque, cuandofoutaún está abierto. Si la sacaras fuera delwith, daríaValueError: I/O operation on closed file. len(partes) != 3descarta las líneas vacías, que trassplit(';')son[''].>= minimoy no>: la sesión del día 1 dura justo 30 minutos y el enunciado dice «al menos 30».int(minutos)se usa para comparar y sumar, pero en el archivo escribominutostal cual (ya es texto).write()con el'\n'explícito en cada línea, para no depender deprint().
Las carreras del archivo son tres: 30 (entra), 25 (no llega) y 40 (entra). Por eso sale Total: 2 sesiones, 70 min, con 30 + 40 = 70.
Solución del Nivel 3
# Nivel 3 · Normalizar un diario escrito "a mano"
import re
# fecha, separador (; , o |) con espacios opcionales, deporte, separador, minutos y 'min' opcional
PATRON = re.compile(r'(\d{4}-\d{2}-\d{2})\s*[;,|]\s*(\w+)\s*[;,|]\s*(\d+)\s*(?:min)?')
def normalizar(origen, destino):
cambiadas = total = 0
with open(origen, 'r', encoding='utf-8') as fin, open(destino, 'w', encoding='utf-8') as fout:
for linea in fin:
original = linea.strip()
m = PATRON.fullmatch(original) # fullmatch: la línea ENTERA tiene que encajar
if m is None: # vacías y frases sueltas no son sesiones
continue
limpia = ';'.join(m.groups()) # fecha;deporte;minutos
total += 1
if limpia != original:
cambiadas += 1
fout.write(f'{limpia} <- {original}\n')
fout.write('>' * 10 + '\n')
fout.write(f'Normalizadas:{cambiadas:3d}, Total:{total:3d}\n') # campos de 3 caracteres
return cambiadas, total
print(normalizar('sucio.txt', 'limpio.txt'))
with open('limpio.txt', 'r', encoding='utf-8') as f:
print(f.read(), end='')
Por qué está hecho así:
- El patrón se compila una vez, fuera de la función, como viste en el tema de regex.
\s*[;,|]\s*: el separador es uno de los tres caracteres, con espacios opcionales a los lados. Así encajan;,,y|.(\w+)atrapa el deporte;\wincluye las letras con tilde, así quenataciónencaja.\s*(?:min)?acepta45min,25 mino nada. El(?:…)agrupa sin crear un cuarto grupo, para quem.groups()siga dando tres.fullmatchy nosearch: consearch, una línea con basura delante o detrás también se daría por buena.';'.join(m.groups())monta la línea normalizada con los tres grupos.- Una línea «ha cambiado» si la versión limpia es distinta de la original sin el salto. Por eso se compara con
original = linea.strip(). f'{cambiadas:3d}'reserva 3 caracteres: el 3 sale como3, con dos espacios delante, igual que pide el formato.
El resultado, línea a línea:
Cuentas: 7 líneas en sucio.txt; 2 no son sesiones (la vacía y «sin datos hoy»), así que el total es 7 − 2 = 5. De esas 5, las de los días 2, 4 y 7 cambian: 3 normalizadas.
Este ejercicio tiene la misma forma que los exámenes de FP1 que juntan archivos, regex y formato: leer un archivo, buscar con un patrón, escribir otro archivo y terminar con líneas de resumen de ancho fijo.
Míralo paso a paso en Python Tutor
En Python Tutor (elige Python 3) vamos a ver en directo el fallo del fragmento 3. Python Tutor no abre archivos de tu disco, así que el archivo es una string y next(f, '') hace de readline().
# El fragmento 3 con el "archivo" simulado: mira cuándo se para el while
texto = '2026-10-01;carrera;30\n2026-10-02;natación;45\n\n2026-10-04;carrera;25\n'
f = iter(texto.splitlines(keepends=True)) # next(f, '') hace de readline()
linea = next(f, '').rstrip()
while linea != '':
print(linea)
linea = next(f, '').rstrip() # la línea vacía '\n' se convierte en '' y el bucle termina
print('Fin del bucle')
Sale lo mismo que en el fragmento:
2026-10-01;carrera;30 2026-10-02;natación;45 Fin del bucle
Qué observar:
- Mira la lista que crea
iter(...): la tercera línea es'\n', una línea vacía, pero no una string vacía. - En las dos primeras vueltas,
lineaya llega sin'\n'porque elrstrip()va pegado anext(). - En la tercera lectura,
next()devuelve'\n'y elrstrip()lo convierte en''. Fíjate en el valor delineajusto antes de evaluar la condición. - La condición es
False, el bucle termina y se imprimeFin del bucle, aunque en el iterador aún quedaba una línea con la carrera de 25 minutos.
Chuletario: Archivos de texto
Lo esencial en 30 segundos
- Abre siempre con
with open(nombre, modo, encoding='utf-8') as f:. Se cierra solo. 'r'lee (el archivo debe existir),'w'vacía y escribe,'a'añade al final.- Cada línea leída trae su
'\n'. Una línea vacía es'\n';''significa fin del archivo. print(..., file=f)convierte y añade el salto;f.write(s)solo admite strings y no añade nada.- Un manejador se recorre una vez: para volver a leer, vuelve a abrir.
Leer
| Quiero… | Código | Resultado |
|---|---|---|
| recorrer línea a línea | for linea in f: | cada línea con su '\n' |
| numerar las líneas | for n, linea in enumerate(f, start=1): | 1, 2, 3… |
| quitar el salto y espacios | linea.strip() | '2026-10-01;carrera;30' |
| solo el salto | linea.rstrip('\n') | conserva los espacios |
| leer una línea | f.readline() | '' al final del archivo |
| todo en una string | f.read() | '...\n...\n' |
| n caracteres | f.read(4) | '2026' |
| lista de líneas sin salto | f.read().splitlines() | ['…', '…'] |
| ver espacios y saltos | print(repr(linea)) | '… ;30 \t\n' |
Escribir
| Quiero… | Código | Resultado |
|---|---|---|
| una línea con print | print(a, b, c, sep=';', file=f) | a;b;c + '\n' |
| una línea con write | f.write(f'{a};{b};{c}\n') | lo mismo |
| un número | f.write(str(55) + '\n') | '55\n' |
| copiar una línea leída | fout.write(linea) | sin salto extra |
| añadir sin borrar | open(nombre, 'a', encoding='utf-8') | escribe al final |
Módulo os
| Quiero… | Código |
|---|---|
| ¿existe? | os.path.exists(ruta) |
| crear carpetas | os.makedirs('a/b', exist_ok=True) |
| unir una ruta | os.path.join('a', 'b', 'x.txt') |
| listar una carpeta | os.listdir(carpeta) |
| ¿es carpeta? | os.path.isdir(ruta) |
| borrar un archivo | os.remove(ruta) |
| carpeta de trabajo | os.getcwd() |
Plantillas de los ejercicios
Contar y saltar líneas vacías (Nivel 1):
with open(nombre, 'r', encoding='utf-8') as f:
for linea in f:
limpia = linea.strip()
if limpia == '':
continue
campos = limpia.split(';')
Filtrar de un archivo a otro (Nivel 2):
with open(origen, 'r', encoding='utf-8') as fin, open(destino, 'w', encoding='utf-8') as fout:
for linea in fin:
if cumple(linea):
fout.write(linea)
fout.write('línea final\n') # dentro del with
Normalizar con regex y cerrar con un resumen de ancho fijo (Nivel 3):
m = PATRON.fullmatch(linea.strip())
if m is not None:
fout.write(';'.join(m.groups()) + '\n')
fout.write('>' * 10 + '\n')
fout.write(f'Normalizadas:{cambiadas:3d}, Total:{total:3d}\n')
Errores que más se repiten
| Síntoma | Causa probable |
|---|---|
| El archivo ha quedado vacío o con una sola línea | lo abriste con 'w' en lugar de 'a' |
FileNotFoundError | nombre mal escrito o carpeta de trabajo distinta |
| Líneas en blanco entre las líneas | print(linea) con la línea que ya trae '\n' |
| Todo sale en una sola línea | write() sin '\n' |
TypeError: write() argument must be str | le pasaste un número a write() |
| El bucle se para antes de tiempo | readline().rstrip() en la misma instrucción |
| La segunda lectura no da nada | recorriste el mismo manejador dos veces |
Gáldar y caracteres raros | codificación distinta; usa encoding='utf-8' |
ValueError: I/O operation on closed file | escribiste fuera del with |
Antes de entregar, pregúntate
- ¿He elegido el modo pensando en lo que ya hay en el archivo?
- ¿Quito el
'\n'donde hace falta y no añado otro al escribir? - ¿Mi bucle sobrevive a líneas vacías, comentarios y campos de más o de menos?
- ¿Todo se escribe dentro del
withy conencoding='utf-8'?
Con esto terminas los ejercicios de archivos de texto y todo el temario de FP1 en SergioLearns. Si te ha servido, vuelve a la teoría cuando lo necesites: el chuletario está aquí para eso.
Sigue aprendiendoTodos los artículos de Python¿Te ha servido este artículo? Así puedes apoyar SergioLearns →
