Como usar expresiones regulares con JavaScript

Como usar expresiones regulares con JavaScript

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 behind <= y look ahead => de las expresiones regulares, algo que sí soportaban chrome y firefox.

Después de esta experiencia ya no quería usar más expresiones regulares, porque ya son difíciles de captar el significado rápidamente /^(.){5}\w?[a-Z|A-Z|0-9]$/ig, tratar de leer algo así sin experiencia puede parecer abrumador y luego le sumamos 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...

Cada tanto tiempo, toca un problema que dices esto lo podría resolver con expresiones regulares y no es que se pueda sino que hasta te cuestionas si se puede resolver sin expresiones regulares. Al final y al cabo las expresiones pueden parecer caracteres sin sentido, pero lo tienen, pueden ser muy complejas, pero no son difíciles de entender.

¿Qué son las expresiones regulares?

Las expresiones regulares 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 cierto?

What?

Se diferencia del CTRL+F porque este busca textos precisos y te arroja el match. Con expresiones regulares es más complejo, se pueden buscar patrones como buscar todas las palabras que estén entre dos espacios, palabras que empiecen con mayúscula, encontrar la primera palabra de cada línea, etc.

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.

const reg1 = /#\d+\s+.*/g;
const reg2 = new RegExp(/#\d+\s+.*/, "g");

reg1; // => /#\d+\s+.*/g
reg2; // => /#\d+\s+.*/g

Sintaxis de expresiones regulares

Empecemos por los flags, estas 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 multilinea

Ahora pasamos al contenido, los siguientes indican 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 donde n 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"

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 flag s. Si por ejemplo queremos usar un punto, pero no representar una clase tendremos que escaparlo 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"

Ya casi

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}

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:.*/igaplicado 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: es una propiedad de las expresiones regulares que busca una ocurrencia, si la hay devuelve true de lo contrario false.
  • Search: es una propiedad de los strings que busca la primera ocurrencia y devuelve el índice, si no la hay devuelve -1.
"aaaaa".search(/aa/) // => 0
"sddaaaaa".search(/aa/) // => 3
"sddaaaaa".search(/aad/) // => -1

/a/.test("asxsxa") // => true
/e/.test("asxsxa") // => false

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 a replace 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.
let 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.

const 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 es que la constante regularExp se crea cada vez que un hit es manejado por map, 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.

Explicación de la expresión regular

  1. Lo que significa hits es has match en esta string con esta expresión /#\d+\s+.*/g que significa: todo lo que contenga # seguido de uno o más dígitos, seguido por uno más espacios y después por cualquier carácter excepto nueva línea. Esto retornará un array con toos los elementos.
  2. 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 /#(\d+)\s+([\s\w]*\b)\s?(@\w+)?/ig, esta significa:
    1. En la expresión que empieza con # captura los dígitos siguientes
    2. Seguida de uno o más espacios captura ya sean espacios o alfanuméricos tantas veces como se presenten, pero que terminen en el límite de palabra.
    3. Luego seguido o no un espacio captura si es que hay el símbolo @ seguido de uno o más alfanuméricos.
  3. Después podemos extraer los datos capturados con exec() o con matchAll() y retornar nuestro objeto.
// 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"
  }
]

Es bellisimo

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

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