Grupos en Strings de Python — Grupos de Captura en Regex
Aprende cómo funcionan los grupos de captura en Python: group(), groups(), grupos con nombre, no capturantes y referencias inversas con ejemplos.
Cuando el módulo re de Python encuentra una coincidencia dentro de un string, te devuelve un objeto match. El objeto match es más que un simple resultado de sí/no — recuerda cada subpatrón que encerraste entre paréntesis, llamado grupo de captura. Los métodos .group() y .groups() son la forma de recuperar esas partes capturadas.
Este capítulo cubre todo lo que necesitas para usar grupos con confianza: grupos numerados, grupos con nombre, grupos no capturantes, referencias inversas en sustituciones y errores comunes.
¿Qué es un Grupo de Captura?
Un grupo de captura es una parte de un patrón regex encerrada entre paréntesis simples (...). Cuando el patrón coincide, Python guarda el texto que coincidió con cada par de paréntesis por separado, además de guardar la coincidencia completa.
import re
m = re.search(r'(\d{4})-(\d{2})-(\d{2})', '2024-03-15')
print(m.group(0)) # 2024-03-15 — the whole match
print(m.group(1)) # 2024 — first group
print(m.group(2)) # 03 — second group
print(m.group(3)) # 15 — third groupLos grupos se numeran de izquierda a derecha, comenzando en 1, en el orden en que aparece su paréntesis de apertura. group(0) (o simplemente group() sin argumento) siempre devuelve la coincidencia completa.
El Método .group()
.group(n) devuelve el texto que coincidió con el enésimo grupo de captura. Puedes pasar múltiples índices a la vez para obtener una tupla de resultados.
import re
m = re.search(r'(\w+)@(\w+)\.(\w+)', '[email protected]')
# Individual groups
print(m.group(1)) # alice
print(m.group(2)) # example
print(m.group(3)) # com
# Multiple at once
print(m.group(1, 3)) # ('alice', 'com')Si un grupo existe en el patrón pero no participó en la coincidencia (por ejemplo, estaba dentro de una sección opcional), group(n) devuelve None.
import re
# group(1) is optional — it may not match
m = re.search(r'(\d+)?-(\d+)', 'abc-456')
print(m.group(1)) # None (the optional \d+ did not match)
print(m.group(2)) # 456El Método .groups()
.groups() devuelve una tupla que contiene el texto coincidente de cada grupo de captura, en orden. Es un atajo conveniente cuando quieres todos los grupos a la vez.
import re
m = re.search(r'(\d{4})-(\d{2})-(\d{2})', '2024-03-15')
print(m.groups()) # ('2024', '03', '15')Proporcionar un Valor Predeterminado para Grupos sin Coincidencia
Si un grupo no participó en la coincidencia, aparece como None en la tupla. Puedes suministrar un argumento default para reemplazar esos valores None con algo más útil.
import re
m = re.search(r'(\d+)?-(\d+)', 'abc-456')
print(m.groups()) # (None, '456')
print(m.groups(default='0')) # ('0', '456')Grupos con Nombre usando (?P<name>...)
Los grupos numerados se vuelven difíciles de seguir en patrones complejos. Los grupos con nombre te permiten asignar una etiqueta significativa a cada grupo usando la sintaxis (?P<name>...), y luego recuperar el valor por nombre en lugar de por posición.
import re
pattern = r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})'
m = re.search(pattern, '2024-03-15')
print(m.group('year')) # 2024
print(m.group('month')) # 03
print(m.group('day')) # 15Los grupos con nombre aún tienen un número, por lo que puedes acceder a ellos de cualquier manera.
El Método .groupdict()
Cuando tienes grupos con nombre, .groupdict() devuelve un diccionario que mapea cada nombre con el texto que coincidió. Esto es útil cuando quieres desempaquetar una coincidencia en un objeto estructurado.
import re
m = re.search(
r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})',
'2024-03-15'
)
print(m.groupdict())
# {'year': '2024', 'month': '03', 'day': '15'}Luego puedes usar el resultado directamente:
date_parts = m.groupdict()
print(f"{date_parts['day']}/{date_parts['month']}/{date_parts['year']}")
# 15/03/2024Grupos No Capturantes con (?:...)
A veces necesitas agrupar parte de un patrón para aplicar un cuantificador o una alternancia, pero no quieres que ese grupo aparezca en los resultados de la coincidencia. Usa (?:...) — un grupo no capturante.
import re
# Without non-capturing group: both parts appear in groups()
m1 = re.search(r'(\d{4})-(\d{2})-(\d{2})', '2024-03-15')
print(m1.groups()) # ('2024', '03', '15')
# Year grouped but not captured — month and day are groups 1 and 2
m2 = re.search(r'(?:\d{4})-(\d{2})-(\d{2})', '2024-03-15')
print(m2.groups()) # ('03', '15')Los grupos no capturantes son una buena práctica cuando no necesitas una subcoincidencia en particular — mantienen limpios los índices de tus grupos y evitan desperdiciar memoria.
Grupos Anidados
Los grupos pueden anidarse. La numeración siempre se basa en la posición del paréntesis de apertura, contando de izquierda a derecha.
import re
# ((\d{4})-(\d{2}))-(\d{2})
# group 1 = the outer (year-month pair)
# group 2 = year
# group 3 = month
# group 4 = day
m = re.search(r'((\d{4})-(\d{2}))-(\d{2})', '2024-03-15')
print(m.group(1)) # 2024-03
print(m.group(2)) # 2024
print(m.group(3)) # 03
print(m.group(4)) # 15
print(m.groups()) # ('2024-03', '2024', '03', '15')Grupos con re.findall()
Cuando usas re.findall() y el patrón contiene exactamente un grupo de captura, devuelve una lista de strings — una por coincidencia.
Cuando el patrón contiene dos o más grupos de captura, devuelve una lista de tuplas.
import re
# One group → list of strings
emails = '[email protected], [email protected]'
usernames = re.findall(r'(\w+)@\w+\.\w+', emails)
print(usernames) # ['alice', 'bob']
# Two groups → list of tuples
pairs = re.findall(r'(\w+)@(\w+)\.com', emails)
print(pairs) # [('alice', 'gmail'), ('bob', 'yahoo')]Grupos con re.finditer()
re.finditer() devuelve un iterador de objetos match, por lo que puedes acceder a .group() y .groups() en cada uno individualmente. Este es el enfoque más flexible cuando necesitas todos los detalles de la coincidencia para cada ocurrencia.
import re
text = '[email protected], [email protected]'
for m in re.finditer(r'(?P<user>\w+)@(?P<domain>\w+)\.com', text):
print(m.group('user'), '->', m.group('domain'))
# alice -> gmail
# bob -> yahooReferencias Inversas en re.sub()
Los grupos de captura también potencian las referencias inversas en re.sub(). En el string de reemplazo, \1, \2, … hacen referencia al texto capturado por el grupo 1, el grupo 2, etc. Los grupos con nombre usan \g<name>.
import re
# Swap first and last name
result = re.sub(r'(\w+)\s(\w+)', r'\2 \1', 'John Smith')
print(result) # Smith John
# Same using named groups
result2 = re.sub(
r'(?P<first>\w+)\s(?P<last>\w+)',
r'\g<last> \g<first>',
'Jane Doe'
)
print(result2) # Doe JanePosición de un Grupo: .start(), .end(), .span()
Los objetos match exponen la posición de cada grupo, no solo su texto. Pasa un número de grupo (o nombre) a .start(), .end() o .span().
import re
m = re.search(r'(\d{4})-(\d{2})', '2024-03')
print(m.span(1)) # (0, 4) — year occupies indices 0..3
print(m.start(2)) # 5 — month starts at index 5
print(m.end(2)) # 7 — month ends before index 7Referencia Rápida
| Método | Devuelve | Notas |
|---|---|---|
m.group() o m.group(0) | String de la coincidencia completa | Siempre disponible |
m.group(n) | Texto coincidente del grupo n | None si el grupo no coincidió |
m.group(n, m, ...) | Tupla de grupos | Múltiples índices en una sola llamada |
m.groups() | Tupla de todos los grupos | default= opcional para los no coincidentes |
m.groupdict() | Dict de grupos con nombre | Solo aparecen los grupos con nombre |
m.start(n) / m.end(n) | Índice de inicio / fin del grupo n | Pasa 0 para la coincidencia completa |
m.span(n) | Tupla (start, end) | Atajo para ambos |
Errores Comunes
Usar .group() sin verificar si hay None. re.search() devuelve None cuando no hay coincidencia, por lo que llamar a .group() directamente sobre el resultado lanza un AttributeError. Siempre protege la llamada:
import re
m = re.search(r'(\d+)', 'no digits here')
if m:
print(m.group(1))
else:
print('no match')
# no digits hereConfundir .group() con .groups(). .group(1) devuelve un string; .groups() siempre devuelve una tupla. Mezclarlos provoca errores de tipo sutiles.
Olvidar que re.findall() cambia de forma según los grupos. Sin grupos devuelve una lista plana de strings; con grupos devuelve una lista de tuplas. Agregar o eliminar un grupo de captura puede romper silenciosamente el código que procesa el resultado.
Cuándo Usar Cada Enfoque
- Usa grupos numerados para patrones simples y cortos con pocos grupos.
- Usa grupos con nombre cuando los patrones son complejos, o cuando usarás el resultado a lo largo de varias líneas de código — los nombres actúan como autodocumentación.
- Usa grupos no capturantes
(?:...)cuando necesites agrupar solo para aplicar un cuantificador o una alternancia, no para extraer un valor. - Usa
.groups()para desempaquetar todas las capturas a la vez en una tupla. - Usa
.groupdict()cuando todos los grupos tienen nombre y quieres un diccionario.
Para una visión más amplia del módulo re y su sintaxis de patrones, consulta el capítulo Python RegEx. Para operaciones con strings que no requieren regex, consulta Modify Strings y Python String Methods.