Expresiones regulares en Javascript

20 de diciembre 2018ComentariosjavascriptDavid Poza SuárezComentarios

Una expresión regular o regex es un patrón que se usa para validar, extraer o reemplazar una determinada combinación de caracteres dentro de una cadena de texto.

Existen muchos motores de expresiones regulares, los más conocidas son: Perl, Python, PCRE(motor basado en las de Perl), Posix o Vim. Y aunque no son tan potentes como las anteriores, Javascript posee las suyas.

Las regex son muy útiles en los editores de texto: Sublime Text y Notepad++ usan la variante PCRE, mientras que Visual Studio Code usa la variante Javascript. Incluso los hay que usan su propio motor, como Vim.

regex de javascript en visual studio code

usamos una regex javascript en VSCode para quitar la "s" al final de los años

Vamos a centrarnos ahora en el motor de regex que usa Javascript, aunque los fundamentos se mantienen en otros.

Definir patrones

Un patrón es un objeto que se define por un conjunto de caracteres y símbolos que determinan el formato contra el cual vamos a poder validar cadenas de texto o usar para extraer o reemplazar fragmentos de las mismas.

Podemos crear el objeto con la sintaxis clásica para instanciar objetos:

var re = new RegExp("abc"); var re = new RegExp("(\d{4})-(\d{2})-(\d{2})");

Aunque lo normal es hacerlo mediante un atajo, que consiste en rodear el patrón con barras diagonales:

re = /abc/; que valida la cadena "esto es el abc de regex" ó re = /(\d{4})-(\d{2})-(\d{2})/; que valida la cadena "1987-09-11"

Greediness

Traduciendo directamente al español sería como la avaricia de la regex. Podríamos tener una expresión greedy o lazy.

  • Greedy: Es el comportamiento por defecto de un patrón. Significa que siempre va a buscar la coincidencia más larga.
  • Lazy: Por el contrario, sería una regex que devuelva siempre la coincidencia más pequeña posible. Se indica incorporando un símbolo de interrogación ? después de cualquier cuantificador.

//"avariciosa" /\d+/.exec("12345"); //devuelve 12345

//"vaga" /\d+?/.exec("12345"); //devuelve 1

Patrones alternativos

Si queremos que una expresión regular que busque varias opciones usaremos el carácter "|" para separarlas. Se corresponde con la operación lógica OR entre las dos opciones.

/0|1/ hace match contra "0" o contra "1"

Comodín

Podemos usar el punto "." como comodín que representa cualquier carácter, sin incluir los saltos de línea.

/a.o/ hace match contra "amo", "aso", "ato" entre otros.

Anclas inicio y fin de línea

Para indicar el principio de línea usamos el carácter "^". Mientras que para indicar el fin de línea usamos el carácter "$".

/^palabra/ contra "palabra palabra palabra" Hace match con la primera palabra.

/palabra$/ contra "palabra palabra palabra" Hace match con la última palabra.

Conjuntos (corchetes)

A veces queremos buscar un caracter dentro de los definidos en un conjunto concreto, podemos definirlo usando corchetes y escribiendo dentro todas los caracteres que comprende.

/[aeiou]/ solo haría matching con una vocal.

Rangos

Si queremos definir un conjunto de caracteres consecutivos muy largo, podemos abreviar usando un rango del tipo a-z para indicar los caracteres de la a hasta la z, incluidos ambos. Se pueden especificar varios rangos dentro de un mismo conjunto, simplemente hay que escribirlos uno a continuación del otro.

/[0-9]/ hace match con cualquier número del 0 al 9. /[a-zA-Z0-9]/ hace match con cualquier letra mayúscula, minúscula o un número del 0 al 9.

Negación

Si se trata de validar un carácter que no pertenece a un conjunto definido, podemos añadir el símbolo "^" después de abrir el corchete.

/[^aeiou]/ hace matching con cualquier consonante

Conjuntos de caracteres predefinidos

Existen una serie de conjuntos habituales que ya vienen definidos con un atajo:

  • \s: Espacio, Tabulador o Salto de línea.
  • \S: Cualquier carácter diferente del conjunto \s.
  • \w: Caracteres alfanuméricos. Equivale al conjunto [A-Za-z0-9_]
  • \W: Cualquier carácter diferente del conjunto \w.
  • \d: Cualquier dígito. Equivale al conjunto [0-9]
  • \D:  Cualquier carácter diferente del conjunto \d.

Cuantificadores

Nos sirven para indicar el número de veces que puede presentarse un carácter, conjunto de caracteres o grupo anónimo.

  • Una o más veces: +
  • Cero o más veces: *
  • Una vez o ninguna: ?
  • n veces: {n}
  • n veces o más: {n,}
  • Entre n y m veces (incluídos): {n,m}

/a+b/ => valida "ab", "aab", "aaab", etc /a*b/ => valida "b", "ab", "aab", "aaab", etc /a?b/ => valida "b", "ab". /a{3}/ => valida "aaa". /a{3,}/ => valida "aaa", "aaaa", "aaaaa", etc /a{3,5}/ => valida "aaa", "aaaa", "aaaaa". /[0-9]{8}[a-z]/i =>Serviría para validar un dni, que tiene 8 dígitos y una letra.

Flags

Un flag sirve para modificar el comportamiento de una expresión regular. Se puede indicar uno o varios, poniendo sus caracteres correspondientes después de la barra diagonal final, ej: /patrón/gi:

g: Búsqueda global. Hace todos los matching posibles, no solo el primero.

/palabra/ contra "primera palabra y segunda palabra" Solo hace un matching aunque "palabra" aparece dos veces.

/palabra/g contra "primera palabra y segunda palabra" Hace matching dos veces.

i: Case insensitive: Ignora mayúsculas y minúsculas.

/PALABRA/ contra "palabra" No hace matching

/PALABRA/i contra "palabra" Hace matching

m: Modo multilínea. Si existen múltiples líneas, considera un inicio y fin de cadena en cada una de las líneas y no solo en la cadena completa.

/línea$/ contra "\nUna línea\nOtra línea" Solo devuelve el último match.

/línea$/m contra "\nUna línea\nOtra línea" Devuelve el primer match. Si añadimos el flag 'g' devuelve los dos.

y: Modo Sicky. La expresión regular se comprueba a partir de la posición donde acabó el anterior match, es decir, desde la posición que se especifique en la propiedad lastIndex de la misma.

/frase/ contra "Esta frase está formada por varias palabras." Hace match

/frase/y contra "Esta frase está formada por varias palabras." si la propiedad lastIndex=11 No Hace match

s: Single Line. Este es nuevo en ES2018. Hace que el comodín "." incluya los saltos de línea. 

/./ contra "\n" No hace match

/./s contra "\n" Hace match

Grupos de captura

Un grupo de captura o grupo anónimo es simplemente un bloque del patrón rodeado por paréntesis. Se numeran según aparecen comenzando a leer la regex por la izquierda.

Sirven para poder hacer referencia a fragmentos de la expresión regular. En concreto se puede usar para buscar fragmentos repetidos en una cadena, es decir, podemos hacer referencia a un grupo dentro de la misma. La referencia se hace con la barra diagonal invertida y la numeración que tiene el grupo en cuestión.

La segunda ventaja de usar grupos de captura es que al dividirla en fragmentos nos permite tener la expresión más organizada.

En el siguiente ejemplo podemos ver que buscamos 3 números con 2 dígitos separados por guiones. Y además queremos que los tres números sean iguales. Para ello definimos un grupo con el primer número y lo referenciamos en los dos números restantes.

regex = /(\d{2})-\1-\1/ regex hace matching con "12-12-12" pero no con "12-12-13"

Como indicaremos en el apartado de "Extraer", podremos recuperar un grupo concreto accediendo al resultado con el índice que corresponde a su posición.

Grupos de captura con nombre

De nuevo, esto es una novedad es ES2018. Ahora podemos definir grupos con un identificador, por ejemplo: /(?<dia>\d{4})-(?<mes>\d{2})-(?<anio>\d{4})/.

Tanto exec() como match() (en el caso de funcionar sin el flag global) nos devuelven la primera coincidencia como un array muy completo, donde podemos encontrar que la propiedad "groups" contiene los grupos con el identificador que hayamos definido en la expresión.

match return

array que devuelven exec o match (cuando no lleva el flag global)

regex = /(?\d{2})-(?\d{2})-(?<año>\d{4})/; resultado = "11-12-2018".match(regex); resultado.groups.dia => 11 resultado.groups.mes => 12 resultado.groups.año=> 2018 cadena = regex.exec("11-12-2018"); cadena.groups.dia => 11 cadena.groups.mes => 12 cadena.groups.año=> 2018

Non-capturing groups

A veces queremos eliminar un grupo del array de resultados, para lo cual existe la sintáxis (?:). Es decir, se añade una interrogación seguida de dos puntos justo después de abrir los paréntesis.

Gracias a los grupos de "no captura" podemos seguir beneficiandonos de una mejor organización por usar grupos de captura aunque no necesitemos parsear el fragmento concreto como resultado individual.

Escapado de caracteres

A veces queremos que nuestra regex busque en un texto caracteres que pertenecen a la sintaxis del propio motor de expresiones regulares. Por lo tanto para poder diferenciarlos tenemos que "escaparlos" colocando una barra diagonal invertida delante del carácter.

Estos son solo algunos ejemplos:

  • El punto: \.
  • La barra invertida: \\
  • Paréntesis: \( ó \)
  • LLaves: \{ ó \}
  • Corchetes: \[ ó \]

La lista completa es: [ ] { } \ ^ $ . | ? * + ( ).

Lookahead

Un lookahead sirve para buscar una coincidencia dependiendo de si le sigue o no un fragmento posterior concreto, sin incluir en el matching ese fragmento.

Puede ser positivo o negativo:

  • Positiva: (?=....) Obliga a que exista una expresión a continuación del texto capturado.
  • Negativa: (?!....) Obliga a que no exista una expresión a continuación del texto capturado.

Por ejemplo queremos obtener todas los nombres de personas que se apellidan "Poza".

regex = /[a-z]+(?=\sPoza)/gi texto.match(regex) nos devuelve únicamente los nombres cuando el apellido sea Poza.

El mismo ejemplo pero en negativo también puede hacerse, para encontrar todos los nombres de aquellos que se apellidan "Poza".

Lookahead para combinar varios patrones en uno solo

Un uso que se le puede dar a un lookahead es el de chequear varios patrones usando un solo patrón compuesto.

En el ejemplo siguiente se comprueba que la contraseña tenga como mínimo una longitud de 6 caracteres

regex = /(?=.{6,})(?=.*[@!?.$/].*[@!?.$/].*)/;

Lookbehind

Esta característica se ha incluído en ES2018.

Un lookbehind sirve para buscar una coincidencia dependiendo de si le precede o no un fragmento previo concreto, sin incluir en el matching ese fragmento.

Pueden ser positivo o negativo:

  • Positiva: (?<=....) Obliga a que exista una expresión previa al texto capturado.
  • Negativa: (?<!....) Obliga a que no exista una expresión a previa al texto capturado.

Por ejemplo queremos obtener todas los apellidos de personas que se llamen "David".

regex = /(?<=David\s)[a-z]+/gi texto.match(regex) nos devuelve únicamente los apellidos de aquellos que se llamen David.

Uso de patrones

Validar

El uso más evidente que podemos darle a una expresión regular es el de validar una cadena de texto en función de si cumple con un formato determinado o no.

Para ello los objetos RegExp tienen un método test() que permite pasar como parámetro la cadena de texto que deseamos comprobar, y devuelve verdadero o falso según si sigue el patrón o no.

regex = /(\d{2}-\d{2}-\d{4}/; //formato de fecha dd-mm-yyyy regex.test("1-1-90"); //devuelve false regex.test("11-09-1987"); //devuelve true

Extraer

Match

Todos los objetos de tipo string poseen un método match() que acepta como argumento un objeto de tipo RegExp. Gracias a éste podemos obtener un array de todas las coincidencias(si se usa el flag global) que ha encontrado la expresión regular dentro de la cadena.

regex = /\d{4}/g; //busca todos los años en el texto cadena = "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s. It was popularised in the 1960s"; cadena.match(regex); //devuelve un array con dos elementos: ["1500","1960"]

Exec

Por otro lado existe el método exec() en los objetos RegExp, que también nos permite extraer las coincidencias de una cadena con la regex. Sin embargo dicha función nos devuelve una coincidencia por cada vez que la llamamos. Está pensada para llamarla en un bucle while hasta que nos devuelve null.

Diferencia entre exec y match

Tanto exec como match devuelven un array con la primera coincidencia, además de los grupos que tuviese la regex, tanto si son anónimos como si son grupos con nombre.

La diferencia reside en que cuando match se usa con el flag global, se devuelve un array de string con todas las coincidencias, mientras que exec ignora dicho flag y devuelve siempre un array con una sola.

Exec() está pensado para ejecutarse en bucle hasta que vale null, tras devolver todas las coincidencias. Mientras que match() siempre devolverá la primera coincidencia independientemente de cuántas veces se le llame.

Reemplazar

El tercer uso que se le puede dar a una regex es el de reemplazar todas las coincidencias que ha encontrado en un texto por otra cadena.

regex = /rojo/g; cadena = "Es recomendable subrayar en color rojo porque el rojo es mejor para nuestra memoria visual."; cadena.replace(regex, "verde"); //cambiamos rojo por verde

Incluso podemos hacer que el texto sustituido contenga fragmentos que ya existían dentro del patrón encontrado.

Para ello se usan los grupos de captura que hemos explicado más arriba.

Se pueden usar las siguientes variables en la cadena reemplazo:

  • $&: contiene todo el texto que ha hecho matching.
  • $n: contiene el texto del grupo capturado número n dentro del match.
  • $`: contiene el fragmento previo a la cadena que ha hecho matching.
  • $': contiene el fragmento posterior a la cadena que ha hecho matching.

En el siguiente ejemplo queremos cambiar el argumento id por class. Para ello vamos a tener 3 grupos de captura en el patrón. Hemos creado un grupo con los argumentos de la etiqueta previos al id, otro con el identificador propiamente dicho y otro más con todos los argumentos que van después del id.

regex

De este modo en el reemplazo podemos reconstruir la etiqueta manteniendo todos sus argumentos pero cambiando el id por class.

regex = /(<h2.*)id(\="[a-z0-9_-]+")(.*)<\/h2>/; cadena ='

Título

texto

...'; cadena.replace(regex, "$1class$2$3");

// resultado =>

Título

texto

...

Herramientas online

Es muy útil cuando estamos estudiando regex poder hacer pruebas fácilmente, una opción muy cómoda es usar los editores de expresiones regulares online. Aquí algunos enlaces útiles para Javascript flavor:

Documentación