R

ReflexKR

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

Tiempo 2:30–3:20

Los registros falsos: anclas y validación

CASE-004

Grep & Co. Agencia de Detectives de Datos

Expediente

Los registros falsos

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 None

Y 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=minsu99

4-1. Reconstruir el accidente: search busca en cualquier parte (5 min)

Primero 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"))     # False

re.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».

4-2. ^ y $: clavar el principio y el final (8 min)

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
  • Las anclas no ocupan ningún carácter. Coinciden con una posición entre caracteres. Por eso la coincidencia de ^id sigue siendo solo los dos caracteres id.
  • No las confundas con el [^...] 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

Las condiciones del caso se comprueban con search. Pon ^ delante del patrón y $ detrás para cerrarles el paso a los nombres falsos.

4-3. match y fullmatch (8 min)

Aunque no escribas las anclas tú, puedes elegir con la función dónde se comprueba el patrón.

FunciónQué preguntaCon 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'>
  • A pesar de su nombre, re.match no comprueba que encaje todo. Le basta con que encaje el principio, así que deja pasar ok_id!!!.
  • La respuesta correcta para validar entradas es 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

Pulsa search, match y fullmatch, uno tras otro. Cambia el texto a !!!ok_id y a ok_id para ver la diferencia entre las tres funciones.

4-4. La trampa de $ y las anclas \A, \Z y \z (6 min)

$ 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.
  • Desde Python 3.14 también existe \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")    # None

El 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.

4-5. Límites de palabra: \b y \B (8 min)

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

Es el patrón de teléfono de la lección 3. Ponle \b delante y detrás para dejar fuera el número de tarjeta y los números demasiado largos.

4-6. Línea a línea en un texto de varias líneas: un adelanto de re.MULTILINE (5 min)

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

Activa y desactiva el botón re.M. Al activarlo, ^ coincide en cada línea. ¿Qué pasa si cambias ^ por \A?

Caso resuelto (10 min)

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=a1

Los 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.

Ejercicios

  1. Intenta adivinar qué devuelven re.match(r"\d{4}", "2026-10-19") y re.match(r"\d{4}", "#2026"), y luego ejecútalos.
  2. Si a la regla de nombres de usuario le añadieras «no puede terminar en guion bajo», ¿cómo cambiarías el patrón? (Pista: escribe el último carácter aparte).
  3. ¿Qué cat encuentra cada uno, 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.

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