R

ReflexKR

Documentación de Reflex
Expresiones regulares a la vista
Lección 10

Tiempo 7:30–8:20

El mismo modus operandi: retrorreferencias y condicionales

CASE-010

Grep & Co. Agencia de Detectives de Datos

Expediente

El mismo modus operandi

Cliente

El inspector Kang

Pruebas

Tres cartas de amenaza dirigidas al inspector Kang y una más que llegó ayer. La científica ha transcrito las cuatro, una por línea.

Para cerrar el caso

Averiguar qué cartas envió la misma persona y demostrarlo con patrones.

Una tarde de lluvia, el inspector Kang extiende cuatro bolsas de plástico sobre el escritorio. «Cartas de amenaza. Tres llegaron desde la semana pasada; la otra, ayer. Después de que saliera el artículo en el periódico». La directora se ajusta las gafas. «Entonces hay un imitador colado. Las pruebas dejan patrones. Lo que escribe una misma mano tiene las mismas manías».

Esta es la transcripción de la científica, una carta por línea. El cuerpo (msg=) es una frase corta en inglés.

#1 2026.10.18 key='K-17' tel=(010)-0000-1018 msg=the server server falls on friday
#2 2026-10-19 key="K-17" tel=010-0000-1019 msg=no one can can read me
#3 2026/10/20 key='K-17' tel=(010)-0000-1020 msg=see you At at the end
#4 2026-10/21 key="K-17' tel=(010-0000-1021 msg=pay now or else

A simple vista, las cuatro se parecen. Hoy vamos a buscar manías con la retrorreferencia, una sintaxis que pregunta: «¿vuelve a aparecer exactamente lo mismo que capturé antes?».

10-1. Volver a buscar lo que ya se capturó: \1 (10 min)

La directora señala la primera carta. «server server. Ha escrito la misma palabra dos veces. En la segunda también: can can». Parece un despiste, pero si está en las tres cartas, es una manía.

En la lección 5 aprendiste los números de grupo. Esos números también se pueden usar dentro del patrón. \1 significa «el mismo texto que acaba de capturar el grupo 1». A esto se le llama retrorreferencia (backreference).

import re

line = "msg=the server server falls on friday"
m = re.search(r"\b(\w+)\s+\1\b", line)
print(m.group())    # server server
print(m.group(1))   # server
  • En cuanto (\w+) captura server, \1 pasa a ser el texto server.
  • \1 no vuelve a aplicar el patrón \w+. Por eso no coincide con dos palabras distintas.
re.search(r"(\w+)\s+(\w+)", "the server").groups()   # ('the', 'server')
re.search(r"(\w+)\s+\1", "the server")               # None

Los \b de los extremos también importan. Sin ellos, el can can de can cannot también contaría como «palabra repetida».

re.search(r"(\w+)\s+\1", "can cannot")       # <re.Match object; span=(0, 7), match='can can'>
re.search(r"\b(\w+)\s+\1\b", "can cannot")   # None

Laboratorio de pruebas

re.search(r"", text)

Valor devuelto

El patrón actual captura dos palabras cualesquiera. Cambia el segundo \w+ por \1 para encontrar solo los sitios donde la misma palabra aparece dos veces.

En el resultado del laboratorio hay algo raro: falta el At at de la tercera carta. Es porque A y a son caracteres distintos. Con re.I (ignorar mayúsculas; lo veremos a fondo en la lección 11), la retrorreferencia también compara sin distinguir mayúsculas y minúsculas.

re.findall(r"\b(\w+)\s+\1\b", letters)          # ['server', 'can']
re.findall(r"\b(\w+)\s+\1\b", letters, re.I)    # ['server', 'can', 'At']
print(re.search(r"(a)\1", "aA", re.I))          # <re.Match object; span=(0, 2), match='aA'>

Aquí letters es la cadena con las cuatro líneas de arriba. Según el resultado, la manía de repetir palabra solo aparece en las cartas 1, 2 y 3.

10-2. Los deberes de la lección 5: el mismo separador (8 min)

En la lección 5, la directora dejó un problema pendiente. Si eliges el separador con [-./], también pasan fechas mezcladas como 2026-10/20, porque la clase elige en cada posición por separado. La solución es capturar el primer separador con una retrorreferencia y exigir el mismo carácter en la segunda posición.

re.fullmatch(r"\d{4}[-./]\d\d[-./]\d\d", "2026-10/21")   # <re.Match object; span=(0, 10), match='2026-10/21'>
re.fullmatch(r"\d{4}([-./])\d\d\1\d\d", "2026-10/21")    # None
re.fullmatch(r"\d{4}([-./])\d\d\1\d\d", "2026/10/20")    # <re.Match object; span=(0, 10), match='2026/10/20'>

Los grupos con nombre (lección 6) también se pueden llamar dentro del patrón, con la sintaxis (?P=nombre). Cuando hay muchos grupos, los nombres se leen mejor que los números.

DATE = r"(?P<y>\d{4})(?P<sep>[-./])(?P<m>\d\d)(?P=sep)(?P<d>\d\d)"
m = re.search(DATE, "#3 2026/10/20 key='K-17'")
print(m.groupdict())   # {'y': '2026', 'sep': '/', 'm': '10', 'd': '20'}
  • Los grupos con nombre también tienen número, así que en lugar de (?P=sep) podrías escribir \2.
  • La forma de llamar a un mismo grupo cambia según el sitio: dentro del patrón, (?P=sep); en el reemplazo, \g<sep> (lección 7); en el objeto Match, m["sep"].

Laboratorio de pruebas

re.search(r"", text)

Valor devuelto

Es el patrón de fechas de la lección 5. Mete el primer [-./] entre paréntesis y cambia el segundo [-./] por \1 para descartar las fechas con separadores mezclados.

10-3. Comillas de apertura y de cierre (7 min)

Mira el valor que sigue a key=. Las cartas 1 y 3 lo envuelven en comillas simples, 'K-17', y la 2 en comillas dobles, "K-17". Solo la 4 abre con comillas dobles y cierra con simples: "K-17'. Si capturas la comilla de apertura en un grupo y exiges \1 al cerrar, sabrás si el par coincide.

QUOTED = r'(["\']).*?\1'
for line in letters.splitlines():
    m = re.search(QUOTED, line)
    print(line[:2], m.group() if m else None)
# #1 'K-17'
# #2 "K-17"
# #3 'K-17'
# #4 None
  • La cadena de Python tiene que contener los dos tipos de comillas, por eso se escribe \'. En una regex, \' es simplemente '.
  • El centro va con .*? (cuantificador perezoso, lección 8) por un motivo: con .* se comería todo, desde la primera comilla hasta la última comilla igual.
re.search(r'(["\']).*\1', '"a" and "b"').group()    # '"a" and "b"'
re.search(r'(["\']).*?\1', '"a" and "b"').group()   # '"a"'

Laboratorio de pruebas

re.search(r"", text)

Valor devuelto

El patrón actual elige la comilla de apertura y la de cierre por separado, así que pasa aunque no formen pareja. Captura la de apertura en un grupo y cambia la de cierre por \1. Cuando lo consigas, mira también el resultado de findall.

Pero si metes el mismo patrón en findall, el resultado es raro.

re.findall(QUOTED, letters)                        # ["'", '"', "'"]
[m.group() for m in re.finditer(QUOTED, letters)]  # ["'K-17'", '"K-17"', "'K-17'"]

Es la regla de la lección 5: si hay un grupo, findall devuelve solo ese grupo. Y esta vez no puedes cambiarlo por (?:...), porque desaparecería el grupo al que llama \1.

El consejo de Dot

La primera vez yo también junté parejas de comillas con findall y volví con la boca llena de comillas sueltas. Para usar una retrorreferencia hace falta un grupo sí o sí, así que cuando quiero la coincidencia entera, uso finditer y saco m.group(). También puedes envolver todo el patrón en otro paréntesis, pero entonces se corren los números y \1 pasa a ser \2.

10-4. Si el grupo participó, esto; si no, aquello: patrones condicionales (10 min)

Fíjate en el teléfono, tel=. Las cartas 1 y 3 ponen el prefijo entre paréntesis, como en (010)-0000-1018, y la 2 lo escribe sin paréntesis. La 4 solo tiene el de apertura: (010-0000-1021. La regla «si hubo paréntesis de apertura, tiene que haber de cierre» se escribe con un patrón condicional.

(?(1)sí|no) significa «si el grupo 1 participó en la coincidencia, aplica el patrón sí; si no, el patrón no». La rama no se puede omitir: (?(1)sí) significa «si el grupo participó, exige sí; si no, no exijas nada».

PHONE = r"(\()?\d{2,3}(?(1)\))-\d{3,4}-\d{4}"
for tel in ["(010)-0000-1018", "010-0000-1019", "(010-0000-1021", "010)-0000-1021"]:
    print(tel, bool(re.fullmatch(PHONE, tel)))
# (010)-0000-1018 True
# 010-0000-1019 True
# (010-0000-1021 False
# 010)-0000-1021 False
  • (\()?: captura el paréntesis de apertura como grupo 1, pero puede no estar.
  • (?(1)\)): si el grupo 1 capturó algo, exige el paréntesis de cierre; si no, sigue adelante.

Con grupos con nombre, la condición usa el nombre en vez del número: (?(nombre)sí|no). Aquí la regla es que si el número de lista va como (1), tiene que cerrarse con paréntesis, y si va sin paréntesis, como 2., tiene que acabar en punto.

ITEM = r"(?P<p>\()?\d+(?(p)\)|\.)"
for s in ["(1)", "2.", "(3.", "4)"]:
    print(s, bool(re.fullmatch(ITEM, s)))
# (1) True
# 2. True
# (3. False
# 4) False

Laboratorio de pruebas

re.search(r"", text)

Valor devuelto

En el patrón actual, \(? y \)? no saben nada el uno del otro, así que pasa aunque solo haya un paréntesis. Captura el de apertura con (\()? y cambia el de cierre por (?(1)\)).

Aunque cumplas todas las condiciones del caso, en la línea de la carta 4 del laboratorio 010-0000-1021 sigue resaltado. search y finditer pueden empezar a coincidir en cualquier posición, así que se saltan el paréntesis de apertura y empiezan justo detrás. Pon tel= delante para fijar el punto de partida.

re.search(PHONE, "tel=(010-0000-1021").group()           # '010-0000-1021'
re.search(r"tel=" + PHONE + r"\b", "tel=(010-0000-1021")  # None

10-5. ¿Qué grupo es \10? (5 min)

Cuando el número de la retrorreferencia tiene dos cifras, es fácil confundirse. Dentro del patrón, \10 no es «el grupo 1 seguido del carácter 0», sino el grupo 10. Si no existe un grupo 10, es un error. Tampoco se interpreta como un carácter en octal.

re.compile(r"(a)\10")
# re.PatternError: invalid group reference 10 at position 4

p = re.compile(r"(a)(b)(c)(d)(e)(f)(g)(h)(i)(j)\10")
print(p.fullmatch("abcdefghijj"))   # <re.Match object; span=(0, 11), match='abcdefghijj'>
  • Si quieres «el grupo 1 seguido del carácter 0», separa el 0 metiéndolo en una clase: \1[0].
  • El \g<1>0 del reemplazo (lección 7) no sirve dentro de un patrón. En un patrón, \g es un escape desconocido y da error.
  • Un carácter en octal solo se lee así si empieza por \0 o tiene tres cifras (\101 es A). Lo repasaremos en la lección 12.
print(re.fullmatch(r"(a)\1[0]", "aa0"))   # <re.Match object; span=(0, 3), match='aa0'>

Caso resuelto (10 min)

Escribe las cuatro manías como reglas y cuenta cuántas cumple cada carta. Los nombres de las reglas están en coreano: 반복 = repetición, 날짜 = fecha, 따옴표 = comillas, 괄호 = paréntesis.

RULES = {
    "반복": re.compile(r"\b(\w+)\s+\1\b", re.I),
    "날짜": re.compile(r"\d{4}([-./])\d\d\1\d\d"),
    "따옴표": re.compile(r'key=(["\']).*?\1'),
    "괄호": re.compile(r"tel=(\()?\d{2,3}(?(1)\))-\d{3,4}-\d{4}\b"),
}
for line in letters.splitlines():
    hits = [name for name, rx in RULES.items() if rx.search(line)]
    print(line[:2], hits)
# #1 ['반복', '날짜', '따옴표', '괄호']
# #2 ['반복', '날짜', '따옴표', '괄호']
# #3 ['반복', '날짜', '따옴표', '괄호']
# #4 []

Las cartas 1, 2 y 3 comparten las cuatro manías: repiten una palabra, usan un único separador en las fechas y nunca fallan una pareja de comillas o de paréntesis. La 4 solo imita la apariencia y no tiene ni una.

El inspector Kang chasquea la lengua. «O sea, que la 4 es de algún gracioso que leyó el artículo, y las de verdad son tres». La directora asiente. «El de verdad es meticuloso. No falla ni una pareja».

Informe de caso cerrado — Las cartas de amenaza 1, 2 y 3 las envió la misma persona (el Backtracker). Coinciden en la palabra repetida, el separador de fecha único y las parejas de comillas y paréntesis. La 4 se considera una imitación y se entregó aparte al inspector Kang.

Cuando vas a cerrar el informe, la directora vuelve a tomar los originales. Hay líneas que la científica no transcribió. Al pie de cada una de las tres cartas auténticas, una línea extraña hecha solo de paréntesis y barras invertidas.

Ejercicios

  1. Escribe un patrón que encuentre el mismo dígito repetido cuatro veces seguidas, como 0000. Pista: a \1 también se le puede poner un cuantificador.
  2. En <b>bold</b> <i>x</b>, busca solo los casos en que la etiqueta de apertura y la de cierre tienen el mismo nombre. El resultado debe ser solo <b>bold</b>.
  3. Con un patrón condicional, escribe uno en el que, si un email empieza por <, tenga que terminar en >. <kim@example.com> y kim@example.com deben pasar, y <kim@example.com debe fallar.

El rastro del Backtracker

(a+)+$

La última línea de cada una de las tres. Únelas en orden. Saldrá una sola línea. Si son capaces de leerla.

La libreta de la directora

  • Dentro del patrón, \1 es exactamente el mismo texto que capturó el grupo 1. No vuelve a aplicar el patrón.

  • Palabra repetida: \b(\w+)\s+\1\b. Mismo separador de fecha: \d{4}([-./])\d\d\1\d\d.

  • Un grupo con nombre se vuelve a llamar con (?P=name). En el reemplazo, \g<name>; en el Match, m['name'].

  • Pareja de comillas: (["']).*?\1. findall devuelve solo las comillas, así que para la coincidencia entera usa finditer.

  • Condicionales: (?(1)sí|no), (?(name)sí|no). La rama «no» se puede omitir: (?(1)sí).

  • Con re.I, la retrorreferencia también ignora mayúsculas. \10 es el grupo 10; para un 0 tras el grupo 1, \1[0].

← Anterior

Lección 9 · Las reglas de la caja fuerte: lookaround

Siguiente →

Lección 11 · La regex ilegible: flags y compilación

Recibe avisos de nuevas lecciones

Te avisaremos por correo cuando este curso tenga lecciones nuevas o se abra un curso de continuación.

Datos: tu correo electrónico · Finalidad: avisos de nuevas lecciones o de la publicación de este curso · Conservación: hasta que pidas que lo borremos. No lo usamos para nada más ni lo compartimos. Responde a cualquier aviso y lo borraremos enseguida.

Preguntas y respuestas

Pregunta lo que quieras sobre esta lección. Aquí se reúnen las preguntas de todas las versiones de idioma de esta página.

Se muestra como texto plano, sin enlaces ni formato.

R

ReflexKR

Aprende Python y Reflex haciendo. Todos los ejemplos se ejecutaron y verificaron en Reflex 0.10.0.

ReflexKR es gratis y sin banners publicitarios. Si te ayudó, puedes apoyarlo con un café. ☕ Invítanos un café

Aviso de afiliados
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex