Expresiones regulares a la vista
·
Resumen del curso
1
El pago desaparecido: buscar el texto tal cual
2
El coche que se dio a la fuga: clases de caracteres
3
La guía telefónica rota: cuantificadores
4
Los registros falsos: anclas y validación
5
Fechas por todas partes: grupos y alternativas
6
El acceso de las 3 de la madrugada: grupos con nombre y objetos Match
7
La víspera de la noticia: sustitución y división
8
El desafío del Backtracker: codicioso, perezoso y posesivo
9
Las reglas de la caja fuerte: lookaround
10
El mismo modus operandi: retrorreferencias y condicionales
11
La regex ilegible: flags y compilación
12
El Backtracker, desenmascarado: repaso y un tokenizador
CASE-004
Grep & Co. Agencia de Detectives de Datos
Expediente
Cliente
La señora Jeong, de la tienda en línea «Mercado Jeong»
Pruebas
El historial de solicitudes de registro y una función de Python que comprueba las reglas del nombre de usuario.
Para cerrar el caso
Filtrar todos los nombres de usuario que no cumplen las reglas y averiguar por qué el validador dejó pasar a los falsos.
A la hora de comer entra la señora Jeong abrazada a su portátil. «Dejé muy claras las reglas para los nombres de usuario. Y aun así me están entrando nombres rarísimos». La directora mira la pantalla del portátil un buen rato y dice, sin más: «Las reglas están bien. Lo que está mal es cómo preguntas».
Estas son la regla de nombres de usuario de Mercado Jeong y su función de validación. La regla, tal como la escribió ella:
아이디 규칙: 영문 소문자로 시작하고, 소문자·숫자·밑줄(_)만 써서 4~16자Es decir: «empieza por una letra minúscula y usa solo minúsculas, dígitos y guiones bajos (_), entre 4 y 16 caracteres».
import re
ID = r"[a-z][a-z0-9_]{3,15}"
def is_valid_id(uid):
return re.search(ID, uid) is not NoneY este es el historial de solicitudes de registro.
2026-10-18 14:02:51 signup id=jeong_fan
2026-10-19 03:11:02 signup id=ok_id!!!
2026-10-19 03:11:04 signup id=_admin
2026-10-19 03:11:05 signup id=ABC123
2026-10-19 03:11:07 signup id=a1
2026-10-19 09:40:13 signup id=minsu99Primero veamos qué deja pasar el validador de la señora Jeong.
print(is_valid_id("ok_id!!!")) # True ← tiene signos de exclamación y pasa
print(is_valid_id("_admin")) # True ← empieza con guion bajo y pasa
print(is_valid_id("ABC123")) # Falsere.search pregunta: «¿hay en alguna parte de esta cadena un trozo que cumpla la regla?». Dentro de ok_id!!! está ok_id, y dentro de _admin está admin, así que la respuesta es sí.
re.search(ID, "ok_id!!!") # <re.Match object; span=(0, 5), match='ok_id'>
re.search(ID, "!!!ok_id") # <re.Match object; span=(3, 8), match='ok_id'>Para validar una entrada hay que hacer otra pregunta: «¿cumple la regla esta cadena entera?». Y para eso el patrón tiene que decir «aquí empieza y aquí acaba».
Fuera de los corchetes, ^ significa el principio de la cadena y $, el final de la cadena. Estos símbolos se llaman anclas (anchors).
re.search(r"^id", "id=kim") # <re.Match object; span=(0, 2), match='id'>
re.search(r"^id", "my id=kim") # None
re.search(r"kim$", "id=kim") # <re.Match object; span=(3, 6), match='kim'>
re.search(r"kim$", "id=kim!") # None^id sigue siendo solo los dos caracteres id.[^...] de la lección 2. ^ niega si va el primero dentro de los corchetes; fuera de ellos, marca el principio.Si pones ^ delante y $ detrás del patrón del validador, el trozo que cumple la regla tiene que ir de principio a fin de la cadena.
Laboratorio de pruebas
re.search(r"", text)
Valor devuelto
Aunque no escribas las anclas tú, puedes elegir con la función dónde se comprueba el patrón.
| Función | Qué pregunta | Con anclas sería |
|---|---|---|
re.search | ¿hay en alguna parte un trozo que encaje? | (sin anclas) |
re.match | ¿hay un trozo que encaje desde el principio? | \Apatrón |
re.fullmatch | ¿encaja todo? | \Apatrón\Z |
\A y \Z son anclas parecidas a ^ y $; la diferencia la verás en el apartado 4-4.
re.match(ID, "ok_id!!!") # <re.Match object; span=(0, 5), match='ok_id'>
re.match(ID, "!!!ok_id") # None
re.fullmatch(ID, "ok_id!!!") # None
re.fullmatch(ID, "ok_id") # <re.Match object; span=(0, 5), match='ok_id'>re.match no comprueba que encaje todo. Le basta con que encaje el principio, así que deja pasar ok_id!!!.re.fullmatch. Es seguro aunque se te olvide poner las anclas, y además esquiva la trampa del siguiente apartado.Laboratorio de pruebas
re.search(r"", text)
Valor devuelto
$ tiene una regla escondida. No solo coincide en el final de verdad de la cadena, sino también justo antes del último salto de línea. Por eso ^abc$ deja pasar "abc\n", que lleva un salto de línea al final.
re.search(r"^abc$", "abc\n") # <re.Match object; span=(0, 3), match='abc'>
re.fullmatch(r"abc", "abc\n") # None
re.search(r"\Aabc\Z", "abc\n") # None\A solo coincide al principio del todo de la cadena, y \Z, solo al final del todo. Sin excepción para el salto de línea.\z. En Python es idéntico a \Z; se añadió para que la notación coincida con la de las regex de otros lenguajes.re.search(r"\Aabc\z", "abc") # <re.Match object; span=(0, 3), match='abc'>
re.search(r"\Aabc\z", "abc\n") # NoneEl consejo de Dot
Cuando lees un archivo línea a línea, muchas veces cada línea trae un salto de línea al final. Si validas con ^...$, hace como que no ve ese salto y te lo deja pasar. Y el nombre de usuario se guarda con el salto de línea pegado. Para validar, usa fullmatch o \A...\Z.
Nos quedaba un deber pendiente de la lección 3: el patrón de teléfono empezaba a coincidir en mitad de 1234-5678-9012, en el 234. Esta vez no necesitamos un ancla para la cadena entera, sino una que marque dónde empieza y dónde acaba un número dentro de un texto largo.
\b es un límite de palabra. Coincide entre un \w (letra, dígito o guion bajo) y un \W (cualquier otro carácter), o al principio o al final de la cadena si al lado hay un \w. \B es lo contrario: una posición que no es un límite. Ninguno de los dos ocupa caracteres.
re.findall(r"\bid\b", "id=kim userid=lee id_card") # ['id']
re.findall(r"\Bid\b", "id=kim userid=lee id_card") # ['id']
re.findall(r"\bid\B", "id=kim userid=lee id_card") # ['id']Los tres resultados son ['id'], pero cada uno lo encontró en un sitio distinto.
\bid\b : el id de id=kim, con límite delante y detrás\Bid\b : el id de userid, sin límite delante\bid\B : el id de id_card, sin límite detrás porque le sigue un guion bajo (\w)Si pones \b delante y detrás del patrón de teléfono, ya no puede empezar en mitad de un número: entre 1 y 2 no hay límite, porque los dos son \w.
re.findall(r"\b\d{2,3}[-. ]?\d{3,4}[-. ]?\d{4}\b", phonebook)
# ['010-0000-1234', '010 0000 9876', '02-0000-5678', '010.0000.4321']Ojo con una cosa: en Python, el hangul también es \w. Así que delante de un número pegado justo detrás de un texto en hangul no hay límite. En el ejemplo, 전화 significa «teléfono».
re.findall(r"\b\d{2,3}-\d{3,4}-\d{4}\b", "전화010-0000-1234") # []
re.findall(r"\b\d{2,3}-\d{3,4}-\d{4}\b", "전화 010-0000-1234") # ['010-0000-1234']Laboratorio de pruebas
re.search(r"", text)
Valor devuelto
Cuando una cadena tiene varias líneas, como el historial de registros, ^ solo coincide al principio de la primera línea. Si quieres sacar la fecha del principio de cada línea, usa el flag re.MULTILINE (abreviado, re.M). Así ^ y $ coinciden al principio y al final de cada línea.
re.findall(r"^\d{4}-\d\d-\d\d", signups)
# ['2026-10-18']
re.findall(r"^\d{4}-\d\d-\d\d", signups, re.MULTILINE)
# ['2026-10-18', '2026-10-19', '2026-10-19', '2026-10-19', '2026-10-19', '2026-10-19']Aunque actives re.M, \A y \Z siguen coincidiendo solo al principio y al final de la cadena entera.
re.findall(r"^\d", "1\n2", re.M) # ['1', '2']
re.findall(r"\A\d", "1\n2", re.M) # ['1']Todos los flags los veremos a fondo en la lección 11. Por hoy, quédate con que «existe un interruptor que hace que ^ y $ funcionen línea a línea».
Laboratorio de pruebas
re.search(r"", text)
Flags
Valor devuelto
Cambiamos el re.search del validador por re.fullmatch. El patrón se queda igual.
def is_valid_id(uid):
return re.fullmatch(r"[a-z][a-z0-9_]{3,15}", uid) is not None
for line in signups.splitlines():
uid = line.split("id=")[1]
if not is_valid_id(uid):
print(line)
# 2026-10-19 03:11:02 signup id=ok_id!!!
# 2026-10-19 03:11:04 signup id=_admin
# 2026-10-19 03:11:05 signup id=ABC123
# 2026-10-19 03:11:07 signup id=a1Los cuatro filtrados incumplen la regla: ok_id!!! tiene exclamaciones, _admin empieza con guion bajo, ABC123 está en mayúsculas y a1 es demasiado corto.
Informe de caso cerrado — El validador de nombres de usuario usaba re.search y daba por bueno cualquier nombre que cumpliera la regla «solo en parte». Se corrige con re.fullmatch y se bloquean 4 registros que incumplían las reglas.
Pero la directora va bajando el dedo por las horas de las cuatro líneas: 03:11:02, 03:11:04, 03:11:05, 03:11:07. «Cuatro en cinco segundos. A las tres de la madrugada. Esto no lo ha hecho una mano humana». A la señora Jeong se le congela la cara. Las cuentas falsas llegaron todas de golpe, la misma madrugada. Quién las envió, de momento, no lo sabe nadie.
Aquí termina la parte 1. Ya sabes escribir «qué buscar»: un carácter, una posición, cuántas veces y dónde están los límites. En la parte 2 aprenderás a sacar los trozos que necesitas de lo que encuentras, mientras sigues la pista a este visitante de madrugada.
re.match(r"\d{4}", "2026-10-19") y re.match(r"\d{4}", "#2026"), y luego ejecútalos.re.findall(r"\bcat\b", "cat concat cats") y re.findall(r"\Bcat", "cat concat cats")?La libreta de la directora
^ es el principio y $ el final. Las anclas no coinciden con caracteres, sino con posiciones.
re.search busca en cualquier parte, re.match solo al principio, re.fullmatch exige todo. Para validar, fullmatch.
$ también coincide antes del último salto de línea. Para el final sin excepciones, \Z (desde 3.14, \z es lo mismo).
\b es el límite entre \w y \W; \B, una posición que no es límite. El hangul también es \w.
Con re.M, ^ y $ coinciden en cada línea. \A y \Z siguen refiriéndose a la cadena entera.
← Anterior
Lección 3 · La guía telefónica rota: cuantificadores
Siguiente →
Lección 5 · Fechas por todas partes: grupos y alternativas
Recibe avisos de nuevas lecciones
Te avisaremos por correo cuando este curso tenga lecciones nuevas o se abra un curso de continuación.
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© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
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.