Creando patrones de expresiones regulares

En los temas anteriores vimos la sintaxis y los métodos de JavaScript para trabajar con expresiones regulares. No es un técnica fácil y requiere de un uso prolongado hasta alcanzar cierta agilidad. Pero cuando llegamos a dominarla viene a resultar algo casi imprescindible en la programación. Aunque no todos los problemas pueden ser resueltos con expresiones regulares, si es cierto que pueden aplicarse a un montón de ellos. La pregunta ahora es cómo atacar un problema con expresiones regulares.

Creando un patrón de expresión regular para validar email

Una frase de búsqueda como validar dirección email con expresiones regulares en Google nos dará resultados para entretenernos un buen rato. Y más si la ponemos en inglés. Podemos copiar estos patrones y usarlos sin más. Pero si se quiere aprender un poco más se debería construir uno sus propios patrones. No hay otra forma de aprender esta materia sino con práctica constante. Intentemos construir un patrón para validar direcciones de email.

Antes de seguir conviene exponer que no existe un patrón para validar direcciones de email que nos asegure un 100% de certeza. El formato de direcciones de email es demasiado complejo para cubrir todas las posibilidades mediante expresiones regulares. Y más con las de JavaScript que adolecen de características importantes como, por ejemplo, las de PHP. Puedes ver comentarios sobre esto en StackOverflow.

En el año 2010 publiqué ejemplos de patrones para validar campos de un formulario. Uno de ellos es para validar la dirección de email que introduce el usuario en el formulario de contacto. Es un patrón muy simple que aún sigo usando, pues en el fondo esa dirección no se usa de forma directa, sino que la recibo como texto en el cuerpo del mensaje del email. No he querido modificarla porque hasta ahora he podido responder a todas las direcciones que me han enviado. Y además porque la solución a este problema pasa por verificar si esa dirección de email existe más que sólo validar si sintácticamente está bien escrita.

El primer paso es definir que es lo que va a hacer nuestro patrón. En la página de Wikipedia encontramos el formato de una dirección email. De ahí tomaremos los requisitos de los que partiremos, pero sin olvidar que no los he contrastado con las especificaciones pues lo único que me interesa es exponer el proceso de creación de un patrón.

  • Una dirección de correo tiene la estructura info@example.com, donde la parte izquierda de la arroba es la parte local y la derecha es el dominio.
  • Longitud máxima de 254 carácteres.
  • Parte local:
    1. Máximo de 64 caracteres.
    2. Letras ASCII mayúsculas y minúsculas a–z, A–Z.
    3. Dígitos 0-9.
    4. Caracteres especiales _#~!$&'*+=- son permitidos. También se permite el carácter de porcentaje cuando codifica un carácter, como %20 que es la codificación del espacio simple (ASCII 20 en hexadecimal).
    5. Carácter punto . siempre que no sea el primero o último carácter y que no aparezcan consecutivamente. Por ejemplo, John..Doe@example.com no está permitido. Cada punto separa la parte local en etiquetas o campos de usuario.
    6. Las comillas dobles " están permitidas pero deben encerrar un campo de usuario. Por ejemplo "abc"@example.com y abc."def".xyz@example.com están permitidos, pero abc"def"ghi@example.com no lo está.
    7. El espacio (ASCII 32 decimal) y ,:;<>@[]()\ son permitidos dentro de un campo de usuario entrecomillado.
    8. Comentarios serán permitido con paréntesis al inicio o final de la parte local. Por ejemplo john.smith(comment)@example.com and (comment)john.smith@example.com son equivalentes a john.smith@example.com.
    9. Carácteres internacionales por encima de U+007F (127 ASCII) en UTF-8 son permitidos en la especificación RFC 6531, aunque los sistema de email podrían restringirlos en la parte local de la dirección. NOTA: Esto no lo aplicaremos pue complicaríamos en exceso el ejercicio.
  • Parte dominio (hostname):
    1. Máximo de 253 caracteres.
    2. Letras, dígitos, guiones intermedios y puntos: a-z 0-9 - ..
    3. Los . separan campos de dominio. Cada campo debe contener entre 1 y 63 caracteres.
    4. Cada dominio finaliza con un dominio de primer nivel (TLD top level domain). Por ejemplo example.com (ver Nombres de dominio).
    5. Se permiten comentarios al principio o final del dominio. Por ejemplo, john.smith@(comment)example.com y john.smith@example.com(comment) equivalen a john.smith@example.com.

El proceso para construir un patrón es dividir el problema en partes. Esta es la forma más práctica de acometer un problema complejo. Iremos desde lo más simple a lo más complejo, paso a paso. De entrada aquí tenemos dos partes, la parte local y la del dominio separada por arroba. Hemos de tener una herramienta para ir probando los patrones parciales, como la que he creado para probar expresiones regulares.

Creando un patrón para la parte de dominio de una dirección email

Empecemos primero por la parte del dominio que parece más fácil. El patrón debe evaluarse completo, por lo que incluiremos las anclas ^ y $ de inicio y fin de texto. En este caso el patrón debe encontrar el carácter @ al principio y uno o más caracteres con .+:

^@.+$

A partir de aquí iremos modificando este patrón básico para cumplir los requisitos. Los cambios en cada patrón con respecto al anterior se presentan en color azul para visualizarlos mejor.

Por (1) el dominio total no puede contener más de 253 caracteres. El dominio contendrá como mínimo tres caracteres puesto que hay un mínimo de dos campos y cada campo tiene un mínimo de un carácter. Necesitamos un cuantificador como {3,253} para cualquier caracter que es el .. Haremos un búsqueda hacia adelante positiva contando todos los caracteres desde 3 hasta 253 de la expresión siguiente, un grupo de no captura que encerrará todo el patrón de evaluación. Si la búsqueda hacia adelante no falla seguirá con ese grupo del patrón.

^@(?=.{3,253}$)(?:.+)$

Los caracteres permitidos en (2) son una clase de letras, dígitos, puntos y guiones. Cambiamos . de cualquier carácter por la clase [a-z0-9.-]:

^@(?=.{3,253}$)(?:[a-z0-9.-]+)$

Pero (3) dice que los puntos separan campos de dominio y (4) obliga a que hayan al menos dos campos de dominio. Por lo tanto quitamos el punto en la clase y forzamos a que empiece por una clase [a-z0-9-]+ seguido de una o más coincidencias de esa clase iniciada por un punto:

^@(?=.{3,253}$)(?:[a-z0-9-]+(?:\.[a-z0-9-]+)+)$

Por (3) tenemos que limitar la longitud de cada campo. Cambiamos el cuantificador + por {n,m}, siendo "n" el límite inferior y "m" el superior:

^@(?=.{3,253}$)(?:[a-z0-9-]{1,63}(?:\.[a-z0-9-]{1,63})+)$

Se permiten comentarios al principio o final. Agregamos (?:\([^\(]*\))? a ambos lados del dominio. Esto buscará cualquier carácter dentro de paréntesis, puesto que aparentemente no se limita a ciertos caracteres. En principio dice que pueden aparecer al principio o final, pero el siguiente patrón permitiría que aparecieran al principio y también al final:

^@(?=.{3,253}$)(?:(?:\([^\(]*\))?[a-z0-9-]{1,63}(?:\.[a-z0-9-]{1,63})+(?:\([^\(]*\))?)$

Para que aparezcan sólo al principio o final pero no en ambos hemos de contemplar ambas alternativas:

^@(?=.{3,253}$)(?:(?:\([^\(]*\))?[a-z0-9-]{1,63}(?:\.[a-z0-9-]{1,63})+|[a-z0-9-]{1,63}(?:\.[a-z0-9-]{1,63})+(?:\([^\(]*\))?)$

Y ya tenemos una ristra díficil de leer.

Creando un patrón para la parte local de una dirección email

Busquemos ahora un patrón para la parte local de una dirección email. Como inicio anclaremos al total del texto y buscaremos uno o más caracteres finalizando con una arroba:

^.+@$

Por (1) el máximo es de 64 caracteres:

^(?=.{1,64}$)(?:.+)@$

Por (2), (3) y (4) podemos limitar los caracteres permitidos. Por lo tanto cambiaremos el punto . por [a-zA-Z0-9_#~!$&'*+=-]:

^(?=.{1,64}$)(?:[a-zA-Z0-9_#~!$&'*+=-]+)@$

La clase [a-zA-Z0-9_] equivale a \w:

^(?=.{1,64}$)(?:[\w#~!$&'*+=-]+)@$

Aunque (4) también permite % para caracteres codificados para URI. Para nuestro propósito y por simplicidad vamos a permitir el patrón %[2-9a-fA-f][0-9a-fA-F] para contemplar cualquier carácter UNICODE superior al espacio simple (20 en hexadecimal).

^(?=.{1,64}$)(?:(?:[\w#~!$&'*+=-]|%[2-9a-fA-f][0-9a-fA-F])+)@$

El siguiente paso (5) sería buscar posibles campos de parte local separados por punto, los que nos permitiría encontrar a.b@ pero no a..b@, .ab@ o ab.@ por ejemplo.

^(?=.{1,64}$)(?:(?:[\w#~!$&'*+=-]|%[2-9a-fA-f][0-9a-fA-F])+(?:\.(?:[\w#~!$&'*+=-]|%[2-9a-fA-f][0-9a-fA-F])+)*)@$

En (6) las comillas pueden encerrar un campo separado por puntos. Así nos permitiría "John"."Doe"@ o "John_Doe"@ por ejemplo, pero no "John.Doe"@ pues las comillas encierran dos campos. Al mismo tiempo (7) dice que dentro de un campo entrecomillado podemos poner el espacio (ASCII 32 decimal) y los reservados siguientes ,:;<>@[]()\. Esto nos permitirá validar algo como abc."de e<f".ghi@ pero no abc.de e<f.ghi@. La clase de carácter quedaría como [,:;<>@[\]()\\ ], escapando el corchete de cierre y la barra invertida, pues el resto de caracteres no es necesario escaparlos. Agregaremos esta clase a la existente [\w#~!$&'*+=-] en una alternativa con comillas:

^(?=.{1,64}$)(?:(?:(?:[\w#~!$&'*+=-]|%[2-9a-fA-f][0-9a-fA-F])+|"(?:[\w#~!$&'*+=\-,:;<>@[\]()\\ ]|%[2-9a-fA-f][0-9a-fA-F])+")(?:\.(?:(?:[\w#~!$&'*+=-]|%[2-9a-fA-f][0-9a-fA-F])+|"(?:[\w#~!$&'*+=\-,:;<>@[\]()\\ ]|%[2-9a-fA-f][0-9a-fA-F])+"))*)@$

Y con (8) vamos con los comentarios que pueden aparecer al principio o final de la parte local con cualquier carácter entre paréntesis. Agregamos (?:\([^\(]*\))? en color azul negrita pero hemos de crear ambas alternativas para ponerlo al principio y al final:

^(?=.{1,64}$)(?:(?:\([^\(]*\))?(?:(?:[\w#~!$&'*+=-]|%[2-9a-fA-f][0-9a-fA-F])+|"(?:[\w#~!$&'*+=\-,:;<>@[\]()\\ ]|%[2-9a-fA-f][0-9a-fA-F])+")(?:\.(?:(?:[\w#~!$&'*+=-]|%[2-9a-fA-f][0-9a-fA-F])+|"(?:[\w#~!$&'*+=\-,:;<>@[\]()\\ ]|%[2-9a-fA-f][0-9a-fA-F])+"))*|(?:(?:[\w#~!$&'*+=-]|%[2-9a-fA-f][0-9a-fA-F])+|"(?:[\w#~!$&'*+=\-,:;<>@[\]()\\ ]|%[2-9a-fA-f][0-9a-fA-F])+")(?:\.(?:(?:[\w#~!$&'*+=-]|%[2-9a-fA-f][0-9a-fA-F])+|"(?:[\w#~!$&'*+=\-,:;<>@[\]()\\ ]|%[2-9a-fA-f][0-9a-fA-F])+"))*(?:\([^\(]*\))?)@$

Esta es una ristra aún más larga.

Unificando la parte local y la de dominio de los patrones de email

Unificaremos ahora la parte local (X) y la de dominio (Y) con el patrón

^(?=.{5,254})(?:X@Y)$

Hemos puesto un limitador de longitud total entre 5 (algo como a@b.c como mínimo) y 254 caracteres. Quedaría así separando por colores las dos partes:

^(?=.{5,254}$)(?:(?=.{1,64}$)(?:(?:\([^\(]*\))?(?:(?:[\w#~!$&'*+=-]|%[2-9a-fA-f][0-9a-fA-F])+|"(?:[\w#~!$&'*+=\-,:;<>@[\]()\\ ]|%[2-9a-fA-f][0-9a-fA-F])+")(?:\.(?:(?:[\w#~!$&'*+=-]|%[2-9a-fA-f][0-9a-fA-F])+|"(?:[\w#~!$&'*+=\-,:;<>@[\]()\\ ]|%[2-9a-fA-f][0-9a-fA-F])+"))*|(?:(?:[\w#~!$&'*+=-]|%[2-9a-fA-f][0-9a-fA-F])+|"(?:[\w#~!$&'*+=\-,:;<>@[\]()\\ ]|%[2-9a-fA-f][0-9a-fA-F])+")(?:\.(?:(?:[\w#~!$&'*+=-]|%[2-9a-fA-f][0-9a-fA-F])+|"(?:[\w#~!$&'*+=\-,:;<>@[\]()\\ ]|%[2-9a-fA-f][0-9a-fA-F])+"))*(?:\([^\(]*\))?)@(?=.{3,253}$)(?:(?:\([^\(]*\))?[a-z0-9-]{1,63}(?:\.[a-z0-9-]{1,63})+|[a-z0-9-]{1,63}(?:\.[a-z0-9-]{1,63})+(?:\([^\(]*\))?))$

Y esta es la ristra definitiva. El proceso inverso sería leer un patrón de expresión regular. Esto ya lo comenté en un tema anterior y se trata de ir extrayendo los subpatrones de cada grupo entre paréntesis, separando las alternativas, hasta llegar a los subpatrones básicos.

La herramienta que comenté antes nos permite separar el patrón por paréntesis, agregando colores para un mejor diferenciación. Lo siguiente es una captura en HTML del patrón anterior en una vista con saltos de línea:

^
(?=.{5,254}$)
 
(?:
(?=.{1,64}$)
 
(?:
(?:\([^\(]*\))?
 
(?:
(?:[\w#~!$&'*+=-]|%[2-9a-fA-f][0-9a-fA-F])+
|"
(?:[\w#~!$&'*+=\-,:;<>@[\]()\\ ]|%[2-9a-fA-f][0-9a-fA-F])+
")
 
(?:\.
(?:
(?:[\w#~!$&'*+=-]|%[2-9a-fA-f][0-9a-fA-F])+
|"
(?:[\w#~!$&'*+=\-,:;<>@[\]()\\ ]|%[2-9a-fA-f][0-9a-fA-F])+
")
)*
|
(?:
(?:[\w#~!$&'*+=-]|%[2-9a-fA-f][0-9a-fA-F])+
|"
(?:[\w#~!$&'*+=\-,:;<>@[\]()\\ ]|%[2-9a-fA-f][0-9a-fA-F])+
")
 
(?:\.
(?:
(?:[\w#~!$&'*+=-]|%[2-9a-fA-f][0-9a-fA-F])+
|"
(?:[\w#~!$&'*+=\-,:;<>@[\]()\\ ]|%[2-9a-fA-f][0-9a-fA-F])+
")
)*
 
(?:\([^\(]*\))?
)
@
(?=.{3,253}$)
 
(?:
(?:\([^\(]*\))?
[a-z0-9-]{1,63}
(?:\.[a-z0-9-]{1,63})+
|[a-z0-9-]{1,63}
(?:\.[a-z0-9-]{1,63})+
 
(?:\([^\(]*\))?
)
)
$

En la misma herramienta podemos probar este patrón. Construiremos un conjunto de prueba para pasarle un test. El texto se separa en líneas, ignorando las líneas en blanco y las que empiecen por "#" a modo de comentarios. Para el resto de líneas se eliminan los espacios antes y después y se ejecuta el patrón.

#EJEMPLO LOCALES------------------
    
#CORRECTAS -----------------------
#Al menos 1 carácter en parte local y dos campos de 1 
#carácter en parte dominio, total 5
    a@b.c
    usuario@example.com
#Guiones son permitidos
    usuario-general_uno@info-test.example.com
#Los caracteres #_~!$&'*+=- pueden aparecer en la parte
#local sin etrecomillar 
    abcABC012#_~!$&'*+=-@example.com
#El % se permite para codificar caracteres URI, valores 
#desde %20 a %FF
    abc%20%A2%bcdef@example.com
#Los puntos separan campos en la parte local
    abc.def@example.com
#Las comillas cierran campos
    abc."def".ghi@example.com
#Dentro de comillas podemos meter los 
#caracteres #_~!$&'*+=-,:;<>@[]()\ 
    abc."#_~!$&'*+=-,:;;<>@[]()\ ".def@example.com
#Por lo tanto una parte local con espacios y otros 
#caracteres es válida si está entrecomillada 
    "John Doe (Manager)"@example.com
#Un comentario entre paréntesis en parte local o 
#dominio es posible al principio o final de cada parte
    (coment1)abc.def@example.com(coment2)
    abc.def(coment1)@(coment2)example.com
    
#INCORRECTAS----------------------
#Una estructura básica que no se cumple
    a@b
    @example.com
    usuario@
    usuario@primero@example.com
#No dos puntos seguidos
    abc..def@example.com
#No se permiten caracteres #_~!$&'*+=- en dominio 
#(aunque sí en parte local)
    abc@#_~!$&'*+=-.example.com
#El % sólo se permite si le siguen 2 dígitos hexadecimales 
#desde %20 a %FF
    abc%1A.def@example.com
#Las comillas sólo pueden cerrar campos en la parte local
    abc.def"ghi"ijk@example.com
#Una parte local con espacios y otros caracteres NO es 
#válida si no está entrecomillada 
    John Doe (Manager)@example.com
#Comentarios no permitidos en medio de las partes o a 
#ambos lados
    abc.def(coment1).ghi@example.com
    (comment1)abc.def(coment2)@example.com
#No se contemplan Unicodes
    üñîçøðé@üñîçøðé.com
    

Tras probar este texto parece que funciona según se espera.


Y hasta aquí esta serie de temas que trata de introducirnos en las expresiones regulares. Quedan por tratar algunas cosas como la eficiencia en la ejecución y el problema del backtracking, temas que quizás presente en otro momento.