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)Truefullmatch 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())450,00 €87,35 €23,10 €re.compile(...)convierte el patrón en un objetoPattern. 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 unfor: cada vuelta recibe la siguiente, hasta que se acaban.mes un objetoMatch: una coincidencia encontrada.m.group()es su texto.
Más precisamente, para curiosos
re.findallte da directamente una lista con los textos de las coincidencias, sin los objetosMatch.re.searchda solo la primera coincidencia, esté donde esté, oNone.re.matchcomprueba solo si el texto empieza con una coincidencia: para «Mesa 12» daNone, aunque el 12 está en el texto.re.sub,re.splityre.fullmatchcompilan el patrón ellos mismos y guardan los últimos patrones a mano.re.compilees ú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())['450,00 €', '87,35 €', '23,10 €']None12Mí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 = 0for m in re.finditer("ERROR", log): count += 1 print(f"{m.group()} de {m.start()} a {m.end()}")print(f"Total: {count}")ERROR de 0 a 5ERROR de 26 a 31Total: 2m.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()\d+ es «uno o más dígitos», así que «12» es una sola coincidencia. Con \d sin +, cada dígito iría por separado: [1][2][4][2]. El espacio y la coma cortan la secuencia, así que 12 y 4 no se juntan.
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))15.11.202615 / 11 / 20262026-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)ogroup()es toda la coincidencia.searchsirve cuando solo necesitas la primera coincidencia. Si no hay ninguna,mesNoneyif 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
- El patrón de la fecha tiene tres pares de paréntesis: tres grupos.
- search busca el patrón en el texto. Si lo encuentra, m es la coincidencia (Match); si no, None.
- search encuentra la fecha: del índice 7 al 17.
- Cada grupo recuerda su parte: el grupo 1 es 15, el grupo 2 es 11, el grupo 3 es 2026.
- Las partes están en m, cada una bajo su número. group(0) es toda la coincidencia.
- group() es lo mismo que group(0): 15.11.2026.
- Los tres grupos, uno por uno.
- 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.groupsda el número de grupos del patrón.- Si un grupo no participó en la coincidencia (por ejemplo,
(,\d{2})?no estaba), el grupo esNone.
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))35 NoneLa idea
Grupos con nombre y reemplazo
(?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} €")Chocolate caliente: 6.4 €Tostada: 2.9 €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))Plazo: 2026-11-15, recordatorio: 2026-11-08Plazo: 11/15/2026, recordatorio: 11/08/2026re.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.subpuede detenerse con un error. Duplica las barras invertidas conreplace("\\", "\\\\"):
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"))bad escape \T at position 2Copia en C:\Temp- También puedes usar un grupo en el propio patrón:
\1significa «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())Una sola coincidencia, toda la línea. .+ toma todo lo que puede: llega al final del texto y luego devuelve carácter a carácter hasta encontrar un >. El primer > desde el final es el último de la línea. Los cuantificadores así se llaman codiciosos.
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))<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.
search encuentra solo la primera coincidencia. Para todas necesitas for m in order.finditer(...): cada vuelta sigue desde donde se detuvo la anterior.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))Sin conexión con la base de datosDisco 95%re.IGNORECASEes lo mismo que(?i): mayúsculas y minúsculas son iguales.re.MULTILINEhace 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.DOTALLpermite 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)Repetida: caféRepetida: favorFalse 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.
Este es contenido viejo. Más contenido viejo, no los viejos.Este es contenido nuevo. Más contenido nuevo, no los viejos.Reemplazos: 22. Palabras con prefijo. La primera línea es el prefijo, la segunda es el texto. Imprime cada palabra que empiece con el prefijo.
proprograma producto banana aprobar profesor improvisarprogramaproductoprofesor3. 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.
Mira el sitio https://www.example.com y este: http://www.test.com. Los documentos están en https://docs.example.com/guide/start.https://www.example.comhttp://www.test.comhttps://docs.example.com/guide/start4. Fechas a ISO. Convierte cada fecha dd.mm.aaaa del texto a la forma aaaa-mm-dd con un solo re.sub.
Contrato del 03.10.2026, pago hasta el 15.10.2026.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 banderare.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 yre.sub.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Métodos de instancia · Lo que comparten todos los objetos: atributos de clase · Cadenas · Expresiones regulares
- Ejercicios sobre el tema: Texto y archivos