Ir al contenido
wannacode

Expresiones regulares: grupos y reemplazo

En resumen: Los grupos de una expresión regular, partes entre paréntesis, extraen trozos de cada coincidencia; en Python los encuentras con re.finditer y los usas en un reemplazo con \1.

Problema

El extracto del banco

Tu banco te envía un texto con los pagos del mes. Quieres cada importe por separado para sumarlos:

import re
statement = "Alquiler 450,00 € el 01.10, luz 87,35 € el 05.10, agua 23,10 € el 12.10"
print(re.fullmatch(r".*\d+,\d{2} €.*", statement) is not None)
Salida
True
fullmatch solo dice «sí, hay un importe en algún sitio». El .* a ambos lados está ahí porque fullmatch quiere que el patrón cubra todo el texto. re.sub cambiaría los importes, pero no te los da. Necesitas una forma de encontrar cada coincidencia y tomarla.

La idea

re.compile y finditer

Para buscar, el módulo re tiene un objeto patrón y un objeto coincidencia:

import re
statement = "Alquiler 450,00 € el 01.10, luz 87,35 € el 05.10, agua 23,10 € el 12.10"
amount = re.compile(r"\d+,\d{2} €")
for m in amount.finditer(statement):
print(m.group())
Salida
450,00 €
87,35 €
23,10 €
  • re.compile(...) convierte el patrón en un objeto Pattern. Hazlo una vez y úsalo tantas veces como quieras.
  • amount.finditer(statement) da las coincidencias una a una, de izquierda a derecha. Por eso está en un for: cada vuelta recibe la siguiente, hasta que se acaban.
  • m es un objeto Match: una coincidencia encontrada. m.group() es su texto.
Más precisamente, para curiosos
  • re.findall te da directamente una lista con los textos de las coincidencias, sin los objetos Match.
  • re.search da solo la primera coincidencia, esté donde esté, o None. re.match comprueba solo si el texto empieza con una coincidencia: para «Mesa 12» da None, aunque el 12 está en el texto.
  • re.sub, re.split y re.fullmatch compilan el patrón ellos mismos y guardan los últimos patrones a mano. re.compile es útil cuando un mismo patrón se usa en muchos sitios.
import re
statement = "Alquiler 450,00 € el 01.10, luz 87,35 € el 05.10, agua 23,10 € el 12.10"
print(re.findall(r"\d+,\d{2} €", statement))
print(re.match(r"\d+", "Mesa 12"))
print(re.match(r"\d+", "12 mesas").group())
Salida
['450,00 €', '87,35 €', '23,10 €']
None
12

Míralo

Dónde está la coincidencia

Además del texto, la coincidencia sabe también dónde se encontró:

import re
log = "ERROR disco; INFO inicio; ERROR red"
count = 0
for m in re.finditer("ERROR", log):
count += 1
print(f"{m.group()} de {m.start()} a {m.end()}")
print(f"Total: {count}")
Salida
ERROR de 0 a 5
ERROR de 26 a 31
Total: 2
m.start() es el índice del primer carácter de la coincidencia, y m.end() es el índice después del último, como con un slice. Por eso log[m.start():m.end()] es lo mismo que m.group().

Pruébalo

¿Un dígito o un número?

¿Qué imprimirá el programa?

import re
for m in re.finditer(r"\d+", "Mesa 12, 4 personas, 2 botellas"):
print(f"[{m.group()}]", end="")
print()
Respuesta

La idea

Grupos: partes de la coincidencia

Los paréntesis en el patrón forman un grupo. La coincidencia recuerda por separado lo que coincidió con cada grupo:

import re
date = re.compile(r"(\d{2})\.(\d{2})\.(\d{4})")
m = date.search("Vence: 15.11.2026")
if m:
print(m.group())
print(m.group(1) + " / " + m.group(2) + " / " + m.group(3))
print(m.group(3) + "-" + m.group(2) + "-" + m.group(1))
Salida
15.11.2026
15 / 11 / 2026
2026-11-15
  • Los grupos se numeran por sus paréntesis de apertura, de izquierda a derecha, empezando por 1.
  • m.group(1) es el grupo 1. group(0) o group() es toda la coincidencia.
  • search sirve cuando solo necesitas la primera coincidencia. Si no hay ninguna, m es None y if m: la salta.

Así tomas por separado el día, el mes y el año de una fecha, y los ordenas como necesites.

import re
date = re.compile(r"(\d{2})\.(\d{2})\.(\d{4})")
m = date.search("Vence: 15.11.2026")
if m:
print(m.group())
print(m.group(1) + " / " + m.group(2) + " / " + m.group(3))
print(m.group(3) + "-" + m.group(2) + "-" + m.group(1))
Paso a paso, en palabras
  1. El patrón de la fecha tiene tres pares de paréntesis: tres grupos.
  2. search busca el patrón en el texto. Si lo encuentra, m es la coincidencia (Match); si no, None.
  3. search encuentra la fecha: del índice 7 al 17.
  4. Cada grupo recuerda su parte: el grupo 1 es 15, el grupo 2 es 11, el grupo 3 es 2026.
  5. Las partes están en m, cada una bajo su número. group(0) es toda la coincidencia.
  6. group() es lo mismo que group(0): 15.11.2026.
  7. Los tres grupos, uno por uno.
  8. En orden inverso, las partes dan 2026-11-15.
Más precisamente, para curiosos
  • (?:...) es un grupo que solo mantiene las partes juntas, sin recordarlas. Por ejemplo, (?:\+359|0)8[789]\d{7} no forma un grupo 1.
  • date.groups da el número de grupos del patrón.
  • Si un grupo no participó en la coincidencia (por ejemplo, (,\d{2})? no estaba), el grupo es None.
import re
date = re.compile(r"(\d{2})\.(\d{2})\.(\d{4})")
print(date.groups)
m = re.search(r"(\d+)(,\d{2})? €", "Propina 5 €")
print(m.group(1), m.group(2))
Salida
3
5 None

La idea

Grupos con nombre y reemplazo

Cuando hay muchos grupos, los números se confunden. Puedes ponerles nombre: (?P<nombre>...), y luego m.group("nombre"). En Python hay una P después del signo de interrogación: la forma (?<nombre>...) de Java y JavaScript es un error en Python 3.12. Aquí tienes una línea de la cuenta de una cafetería:
import re
item = re.compile(r"(?P<qty>\d+) x (?P<name>[A-Za-z ]+) a (?P<price>\d+\.\d{2})")
for m in item.finditer("2 x Chocolate caliente a 3.20; 1 x Tostada a 2.90"):
qty = int(m.group("qty"))
price = float(m.group("price"))
print(f"{m.group('name')}: {qty * price} €")
Salida
Chocolate caliente: 6.4 €
Tostada: 2.9 €
Los grupos también funcionan en re.sub: en el texto de reemplazo, \1 es el grupo 1, y \g<nombre> es el grupo con ese nombre. El texto de reemplazo también es una cadena cruda (raw string), por la \. Aquí van las fechas, de la forma día.mes.año a ISO y a la forma estadounidense:
import re
text = "Plazo: 15.11.2026, recordatorio: 08.11.2026"
print(re.sub(r"(\d{2})\.(\d{2})\.(\d{4})", r"\3-\2-\1", text))
print(re.sub(r"(?P<d>\d{2})\.(?P<m>\d{2})\.(?P<y>\d{4})", r"\g<m>/\g<d>/\g<y>", text))
Salida
Plazo: 2026-11-15, recordatorio: 2026-11-08
Plazo: 11/15/2026, recordatorio: 11/08/2026
Aquí no necesitas re.compile ni finditer: re.sub entiende los grupos por sí solo.
Más precisamente, para curiosos
  • \ en el texto de reemplazo es especial. Si el texto viene del usuario, por ejemplo la ruta de una carpeta, re.sub puede detenerse con un error. Duplica las barras invertidas con replace("\\", "\\\\"):
import re
user = "C:\\Temp"
try:
re.sub("CARPETA", user, "Copia en CARPETA")
except re.error as e:
print(e)
print(re.sub("CARPETA", user.replace("\\", "\\\\"), "Copia en CARPETA"))
Salida
bad escape \T at position 2
Copia en C:\Temp
  • También puedes usar un grupo en el propio patrón: \1 significa «lo mismo que el grupo 1». Así se atrapan palabras repetidas. Mira la tarjeta de las banderas.

Pruébalo

¿Cuánto toma +?

Quieres todas las etiquetas HTML de la línea:

¿Qué imprimirá el programa?

import re
html = "<b>Precio:</b> <i>12 €</i>"
for m in re.finditer(r"<.+>", html):
print(m.group())
Respuesta

La idea

Codicioso y perezoso

Un ? después de un cuantificador lo hace perezoso: toma lo menos que puede y se detiene en la primera oportunidad.

import re
html = "<b>Precio:</b> <i>12 €</i>"
for m in re.finditer(r"<.+?>", html):
print(m.group())
print(re.sub(r"<[^>]+>", "", html))
Salida
<b>
</b>
<i>
</i>
Precio: 12 €
  • <.+?> es «<, al menos un carácter y el primer > que venga después».
  • <[^>]+> hace lo mismo de otra manera: «<, caracteres que no son >, y >». No puede saltarse un >, así que además es más rápido. Con él quitas las etiquetas y solo queda el texto.
Codicioso Perezoso
* *?
+ +?
? ??
{2,5} {2,5}?

Pruébalo

Encuentra el error

El programa debería imprimir los tres números de pedido, pero imprime solo #1042. ¿En qué línea está el error?

Elige la línea con el error.

La idea

Banderas y líneas

Las banderas se pasan como segundo argumento de re.compile, unidas con |. De un archivo de registro con varias líneas tomas los mensajes de error, escritos en mayúsculas o en minúsculas:

import re
log = "INFO: Inicio\nERROR: Sin conexión con la base de datos\nINFO: Reintentando\nerror: Disco 95%"
p = re.compile(r"^error: (.+)$", re.IGNORECASE | re.MULTILINE)
for m in p.finditer(log):
print(m.group(1))
Salida
Sin conexión con la base de datos
Disco 95%
  • re.IGNORECASE es lo mismo que (?i): mayúsculas y minúsculas son iguales.
  • re.MULTILINE hace que ^ y $ sean el inicio y el final de cada línea. Sin ella, son solo el inicio y el final de todo el texto, y el patrón no encontrará nada.
  • re.DOTALL permite que . atrape también un salto de línea.
Más precisamente, para curiosos

Dos cosas que encontrarás en patrones ajenos:

  • Una referencia hacia atrás (backreference) \1: «el mismo texto que el grupo 1». Así se atrapan palabras repetidas.
  • Una búsqueda hacia delante (lookahead) (?=...): «de aquí en adelante hay en algún sitio ...», sin tomar caracteres. Varias seguidas son útiles para contraseñas: «tiene un dígito» y «tiene una mayúscula», y al menos 8 caracteres.
import re
twice = re.compile(r"\b(\w+) \1\b")
for m in twice.finditer("Te espero en el café café, por favor favor sé puntual."):
print("Repetida: " + m.group(1))
strong = r"(?=.*\d)(?=.*[A-Z]).{8,}"
print(re.fullmatch(strong, "secreto12") is not None, re.fullmatch(strong, "Secreto12") is not None)
Salida
Repetida: café
Repetida: favor
False True

(?!...) es lo contrario: «de aquí en adelante no hay ...». También existe la búsqueda hacia atrás (lookbehind): (?<=...) y (?<!...).

Ejercicio

Encontrar, extraer, reemplazar

Resuelve las tareas en tu editor. El texto es una línea del teclado. En la lección «Archivos» las harás también con un archivo.

1. Buscar y reemplazar. Reemplaza la palabra «viejo» por «nuevo» en todos los sitios donde sea una palabra completa, y di cuántos reemplazos has hecho. Las partes de otras palabras («viejos») no se tocan. re.subn hace lo mismo que re.sub, pero devuelve un par: el texto nuevo y el número de reemplazos.

Entrada
Este es contenido viejo. Más contenido viejo, no los viejos.
Salida
Este es contenido nuevo. Más contenido nuevo, no los viejos.
Reemplazos: 2

2. Palabras con prefijo. La primera línea es el prefijo, la segunda es el texto. Imprime cada palabra que empiece con el prefijo.

Entrada
pro
programa producto banana aprobar profesor improvisar
Salida
programa
producto
profesor

3. Direcciones en un texto. Extrae todas las direcciones web que empiezan con http:// o https://. El punto al final de la frase no forma parte de la dirección.

Entrada
Mira el sitio https://www.example.com y este: http://www.test.com. Los documentos están en https://docs.example.com/guide/start.
Salida
https://www.example.com
http://www.test.com
https://docs.example.com/guide/start

4. Fechas a ISO. Convierte cada fecha dd.mm.aaaa del texto a la forma aaaa-mm-dd con un solo re.sub.

Entrada
Contrato del 03.10.2026, pago hasta el 15.10.2026.
Salida
Contrato del 2026-10-03, pago hasta el 2026-10-15.

Más tareas

  • Total del extracto. Toma los importes de la primera tarjeta con un grupo para los euros y otro para los céntimos, súmalos e imprime «Total: 560,45 €».
  • Hashtags. Extrae todas las palabras que empiezan con # de una publicación que mezcla letras latinas sin tilde y letras como «é» o «ñ». Piensa qué pasaría con la bandera re.ASCII.
  • Sin etiquetas. Quita las etiquetas HTML de <p>El plazo es el <b>15.11</b>.</p> e imprime el texto limpio.
  • Teléfonos en un texto. Encuentra todos los números de móvil de un texto e imprímelos en la forma +359 88 812 3456, con grupos y re.sub.

Test

Ponte a prueba

1. ¿Qué da re.finditer(patrón, texto)?
2. El patrón es (\d{2})\.(\d{2})\.(\d{4}) y el texto es 15.11.2026. ¿Qué es m.group(2)?
3. ¿Qué da re.sub(r"(\d+)\.(\d+)\.(\d+)", r"\2/\1", "05.03.2026")?
4. ¿Por qué "<.+>" atrapa todo el texto "<b>Precio</b>" y no solo "<b>"?
5. ¿Qué hace re.MULTILINE?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·