La mayor parte de mi vida he evitado las cosas complicadas sin siquiera intentarlas de verdad. Simplemente supongo que son complicadas porque así se ven... complicadas.
En una ocasión tuve que usar expresiones regulares para resolver algo muy sencillo. Tardé horas averiguando la mejor forma de hacerlo. Al final lo logré, estaba feliz porque había aprendido algo nuevo y todo funcionaba correctamente, así que hice deploy de la aplicación.
Una semana después un amigo me avisó que la aplicación no le funcionaba en Safari. Y pues claro yo no sabía cuál podría ser la raíz del problema porque en una semana ya había hecho muchos cambios. Para ponerla en corto era que yo había caído en el error de la compatibilidad, porque Safari no soportaba el look behind1 <=
y look ahead =>
de las expresiones regulares, algo que sí soportaban Google Chrome y Firefox.
Después de esta experiencia ya no quería usar más expresiones regulares, porque es difícil de entender el significado de algo asi (/^(.){5}\w?[a-Z|A-Z|0-9]$/ig
), sin experiencia puede parecer abrumador y luego le agregamos una carga más al tener que estar atento qué navegador soporta que cosa.
Lo tenía claro las expresiones regulares no son para mí, ni para muchos, ya que pueden llegar a ser un dolor de cabeza...
Pero de vez en cuando te encuentras con un problema que crees que se puede resolver con expresiones regulares y no que se puede hacer e incluso te preguntas si se puede resolver sin expresiones regulares o no. Al final, las expresiones pueden parecer caracteres absurdos, pero lo tienen, pueden ser muy complejas, pero no son difíciles de entender.
¿Qué son las expresiones regulares?
Las expresiones regulares2 son patrones que definimos para filtrar en una cadena de caracteres. Son útiles para seleccionar parte de la información que necesitamos, descartando lo que sobra. ¿A que se parece a un buscador normal tipo CTRL+F, cierto?
Se diferencia del CTRL+F en que busca textos precisos y te arroja el match. Con expresiones regulares es más complejo porque busca por patrones, como por ejemplo, buscar todas las palabras que estén entre dos espacios, palabras que comienzan con mayúscula, encontrar la primera palabra de cada línea, etc.
Sintaxis de expresiones regulares
Lucen algo así /#\d+\s+.*/g
, /contenido/flags
, esto JavaScript lo entenderá como una expresión regular, pero por detrás, lo que en realidad hace es que lo envuelve en un objeto RegExp, como se explicó en el blog Tipos y objetos en JavaScript; gracias a esto podemos acceder a las propiedades y métodos como flags, ignoreCase, exec()
, test()
, etc.
javascriptconst reg1 = /#\d+\s+.*/g;const reg2 = new RegExp (/#\d+\s+.*/, "g");
Lo mismo, pero con el constructor
javascriptconst reg1 = /#\d+\s+.*/g;const reg2 = new RegExp (/#\d+\s+.*/, "g");
Lo mismo, pero con el constructor
Forma de la expresión regular
Flags
Los flags, se sitúan al final de una expresión regular /contenido/flags
. Los flags permiten darle características a nuestras búsquedas y pueden ser usadas en cualquier orden.
g
: Coincidencia global: comprueba en toda la cadena, en lugar de detenerse cuando encuentra la primera coincidencia.i
: Ignora si es mayúscula o minúscula.m
: Búsqueda en multilínea
Grupos y Rangos
Dentro del contenido, podemos encontrar los siguientes grupos y rangos de caracteres de expresión.
[]
: Toma en cuenta los caracteres dentro de los brackets/[a]/g
aplicado: "aea"[^]
: Toma en cuenta los caracteres que no están dentro de los brackets después del símbolo^
,/[^a]/g
aplicado: "aea"( )
: Recuerda lo que está dentro del paréntesis, captura al grupo para después usarlos con$n
en algún método de string donden
es el índice del grupo empezando por 1. También se puede declarar un nombre a cada grupo(?<nombre>)
para identificarlos en la propiedad groups de RegExp|
: Uno u otro/true|false/g
aplicado: "true es verdadero y false es falso"
Clases de caracteres predefinidas
Para facilitarnos las cosas tenemos clases ya predefinidas y construidas que distinguen tipos.
.
: Toma cualquier carácter, no toma nuevas líneas a excepción de que se use el flags
. Si por ejemplo queremos usar un punto, pero no representar una clase, lo tendremos que escapar con el backslash\
seguido de la clase\.
.\d
: Encuentra todos los dígitos de 0 a 9, es equivalente a [0-9].\D
: Encuentra todo lo que no es un dígito.\w
: Carácter alfanumérico es equivalente a [a-zA-Z0-9_].\W
: No es un carácter alfanumérico.\s
: Espacios de cualquier tipo. (espacio, tab, nueva línea).\S
: No es un espacio, tab o nueva línea.
La siguientes expresiones son equivalentes: /[0-9]+[a-t]*/g
y /\d+[a-t]*/g
aplicado: "
967toma769cuanto 234 texto"
Cuantificadores
Seguimos con los cuantificadores indican el número de caracteres o expresiones que deben coincidir.
{ }
: Hace match al número exacto entre corchetes/a{3}/
aplica a "aaa" pero no a "aaaa"{ , }
: Hace match el número de veces en el rango separado por comas (mínimo, máximo), el máximo tiene que ser siempre mayor y si no está presente significa a todos.*
: 0 o más, es equivalente a{0,}
+
: 1 o más, es equivalente a{1,}
?
: 0 o uno, es equivalente a{0,1}
Limitadores
Terminamos con los limitadores que indican el comienzo y el final de líneas y palabras, y otros patrones que indican de alguna manera que el reconocimiento es posible.
\b
: Indica un límite de palabra, por ejemplo si tenemos oraciones que terminan en espacio para no tomarlo podemos agregar este límite,/[\s\w]*\b/g
, aplicado: "no termines con espacios ", de esta forma no toma el espacio final.\B
: Indica lo que no es un límite de palabra.^
: Indica el inicio de una cadena de texto, puedes limitar que una cadena empieza con una palabra/^caso:.*/ig
aplicado selecciona "caso: 43", pero no "casa: 43"$
: Final de una cadena de texto, puedes limitar a cadenas que terminen con/.*\.$/ig
, aplicado selecciona "Fin.", pero no "Fin"
Tip: Utiliza una herramienta para hacer test de tus expresiones regulares como regextester
Métodos para expresiones regulares
Las expresiones regulares se utilizan con los métodos test()
, exec()
, match()
, replace()
, search()
y split()
.
Test y search
Test:
es una propiedad de las expresiones regulares que busca una ocurrencia, si la hay devuelvetrue
de lo contrariofalse
.Search:
es una propiedad de los strings que busca la primera ocurrencia y devuelve el índice, si no la hay devuelve -1.
javascript"aaaaa".search (/aa/) // => 0"sddaaaaa".search (/aa/) // => 3"sddaaaaa".search (/aad/) // => -1 /a / .test ("asxsxa") // => trueExpression expected.1109Expression expected./e / .test ("asxsxa") // => falseExpression expected.1109Expression expected.
javascript"aaaaa".search (/aa/) // => 0"sddaaaaa".search (/aa/) // => 3"sddaaaaa".search (/aad/) // => -1 /a / .test ("asxsxa") // => trueExpression expected.1109Expression expected./e / .test ("asxsxa") // => falseExpression expected.1109Expression expected.
Replace, ReplaceAll y Split
Replace:
es un método de los strings que toma una expresión regular para una ocurrencia que será remplazada por el segundo parámetro de este método que puede ser una función o un string que puede incluir patrones de remplazo$
según los elementos capturados (entre paréntesis).ReplaceAll:
Es igual areplace
solo que está es de todas las ocurrencias, es una propiedad de los strings que permite ejecutarlas. Este método devuelve un array con las coincidencias en el índice 0 y los demás índices son las partes que se encuentran entre paréntesis.Split:
Utiliza una expresión regular para dividir el string en cada ocurrencia.
tslet tweetText = "@NovallSwift “If you have a problem and decide to fix it using regex, now you have two problems” — @dlpasco, circa 2013"; tweetText .replaceAll (/@(\w+)/g, "<a href='twitter.com/$1'>@$1</a>"); //<a href='twitter.com/NovallSwift'>@NovallSwift</a> “If you have a problem and decide to fix it using regex, now you have two problems” — <a href='twitter.com/dlpasco'>@dlpasco</a>, circa 2013
tslet tweetText = "@NovallSwift “If you have a problem and decide to fix it using regex, now you have two problems” — @dlpasco, circa 2013"; tweetText .replaceAll (/@(\w+)/g, "<a href='twitter.com/$1'>@$1</a>"); //<a href='twitter.com/NovallSwift'>@NovallSwift</a> “If you have a problem and decide to fix it using regex, now you have two problems” — <a href='twitter.com/dlpasco'>@dlpasco</a>, circa 2013
Match, Exec y MatchAll
Match:
es una propiedad de los strings que permite hacer uso de expresiones regulares. Este método devuelve un array con todas las coincidencias en el string.Exec:
es una propiedad de las expresiones regulares que permite ejecutarlas. Este método devuelve un array con las coincidencias en el índice 0 y los demás indices son las partes que se encuentran entre paréntesis.MatchAll:
Este método devuelve un iterador con las coincidencias de una expresión regular incluidos los grupos de captura.
Me encontré el siguiente tweet con información valiosa. Quiero usarla con otra presentación, por lo que tengo que extraer sus partes. Suponiendo que el contenido del string a manipular es desde la palabra Hot hasta @taylorswift13.
Hot 100’s top 10 this week in 2008: #1 Disturbia @rihanna #2 Crush @DavidArchie #3 Forever @chrisbrown #4 I Kissed A Girl @katyperry #5 Viva La Vida @coldplay #6 Paper Planes @MIAuniverse #7 Dangerous @KardinalO #8 Take A Bow #9 Closer @NeYoCompound #10 Change @taylorswift13
tsconst hits = string .match (/#\d+\s+.*/g); const hitsData = hits ?.map ((hit ) => { const regularExp = /#(?<number>\d+)\s+(?<songTitle>[\s\w]*\b)\s?(?<username>@\w+)?/gi; // Diferentes formas de obtener los datos // const [match, number, songTitle, username] = regularExp.exec(hit); // const [match, number, songTitle, username] = [...hit.matchAll(regularExp)][0]; // const {number, songTitle, username} = [...hit.matchAll(regularExp)][0].groups; return regularExp ?.exec (hit )?.groups ;});
tsconst hits = string .match (/#\d+\s+.*/g); const hitsData = hits ?.map ((hit ) => { const regularExp = /#(?<number>\d+)\s+(?<songTitle>[\s\w]*\b)\s?(?<username>@\w+)?/gi; // Diferentes formas de obtener los datos // const [match, number, songTitle, username] = regularExp.exec(hit); // const [match, number, songTitle, username] = [...hit.matchAll(regularExp)][0]; // const {number, songTitle, username} = [...hit.matchAll(regularExp)][0].groups; return regularExp ?.exec (hit )?.groups ;});
Precaución: El uso del método exec()
con el flag global recordará el lastIndex
en la expresión regular, por lo que al usarla de nuevo se pueden obtener resultados inesperados. La razón de por qué funciona con este ejemplo es porque la constante regularExp
se crea cada vez que un hit es manejado por map
, lo que crea un problema de performance. Si se define regularExp
fuera para que no se cree de nuevo es conveniente usar matchAll
dentro de la función al mapear hits.
Ejercicio: Modifica el código para que se pueda usar exec()
y matchAll()
para obtener los hits del string.
Explicación de la expresión regular:
- Lo que significa
/#\d+\s+.*/g
es todo lo que contenga # seguido de uno o más dígitos, seguido por uno o más espacios y después por cualquier carácter excepto nueva línea. Esto retornará un array con todos los elementos. - Ya teniendo el array de hits se mapea para obtener la información de cada uno, para ello creamos nuestra expresión dónde queremos capturar los grupos que deseamos extraer
/#(?<number>\d+)\s+(?<songTitle>[\s\w]*\b)\s?(?<username>@\w+)?/gi
, esta significa:- En la expresión que empieza con # captura uno o más dígitos
\d+
que le siguen, en el grupo llamado number. - Seguido de uno o más espacios
\s+
, captura en el grupo songTitle ya sean espacios o alfanuméricos tantas veces como se presenten,[\s\w]*
, pero que terminen en el límite de palabra\b
. - Luego puede haber o no haber un espacio
\s?
captura en el grupo username el símbolo @ seguido de uno o más alfanuméricos\w+
, pero hacemos este grupo username opcional agregando?
al final del grupo.
- En la expresión que empieza con # captura uno o más dígitos
- Después podemos extraer los datos capturados con
exec()
o conmatchAll()
y retornar nuestro objeto.
json// Resultado[ { "number": "1", "songTitle": "Disturbia", "username": "@rihanna" }, { "number": "2", "songTitle": "Crush", "username": "@DavidArchie" }, { "number": "3", "songTitle": "Forever", "username": "@chrisbrown" }, { "number": "4", "songTitle": "I Kissed A Girl", "username": "@katyperry" }, { "number": "5", "songTitle": "Viva La Vida", "username": "@coldplay" }, { "number": "6", "songTitle": "Paper Planes", "username": "@MIAuniverse" }, { "number": "7", "songTitle": "Dangerous", "username": "@KardinalO" }, { "number": "8", "songTitle": "Take A Bow" }, { "number": "9", "songTitle": "Closer", "username": "@NeYoCompound" }, { "number": "10", "songTitle": "Change", "username": "@taylorswift13" }]
json// Resultado[ { "number": "1", "songTitle": "Disturbia", "username": "@rihanna" }, { "number": "2", "songTitle": "Crush", "username": "@DavidArchie" }, { "number": "3", "songTitle": "Forever", "username": "@chrisbrown" }, { "number": "4", "songTitle": "I Kissed A Girl", "username": "@katyperry" }, { "number": "5", "songTitle": "Viva La Vida", "username": "@coldplay" }, { "number": "6", "songTitle": "Paper Planes", "username": "@MIAuniverse" }, { "number": "7", "songTitle": "Dangerous", "username": "@KardinalO" }, { "number": "8", "songTitle": "Take A Bow" }, { "number": "9", "songTitle": "Closer", "username": "@NeYoCompound" }, { "number": "10", "songTitle": "Change", "username": "@taylorswift13" }]
Ahora ya tenemos nuestra información bien separada y lista para usar de otra forma. Como era esperado el hit número 8 no tiene nombre de usuario.
Conclusión
Las expresiones regulares son una herramienta muy útil para validar datos de formularios, pero también para extraer información de un string.
Aunque exista un amor/odio con las expresiones regulares, en algún momento nos viene bien su uso. Tienen muchos buenos usos porque puede hacer búsquedas complejas como validar entradas de usuario o hacer highlight a un bloque de código aplicando clases. Hay que tener cuidado dónde y cómo las usamos porque pueden ser objetivo de ataques.
Referencias
-
A la fecha de publicar este post, Safari no soporta la expresión regular
<=
y=>
(look behind
ylook ahead
). Can I use Lookbehind in JS regular expressions ↩ -
MDN Web Docs & MDN contributors Regular expressions ↩