18º junio 2021

Lookahead y lookbehind (revisar delante/detrás)

A veces necesitamos buscar únicamente aquellas coincidencias donde un patrón es precedido o seguido por otro patrón.

Existe una sintaxis especial para eso llamadas “lookahead” y “lookbehind” (“ver delante” y “ver detrás”), juntas son conocidas como “lookaround” (“ver alrededor”).

Para empezar, busquemos el precio de la cadena siguiente 1 pavo cuesta 30€. Eso es: un número, seguido por el signo .

Lookahead

La sintaxis es: X(?=Y). Esto significa "buscar X, pero considerarlo una coincidencia solo si es seguido por Y". Puede haber cualquier patrón en X y Y.

Para un número entero seguido de , la expresión regular será \d+(?=€):

let str = "1 pavo cuesta 30€";

alert( str.match(/\d+(?=€)/) ); // 30, el número 1 es ignorado porque no está seguido de €

Tenga en cuenta que “lookahead” es solamente una prueba, lo contenido en los paréntesis (?=...) no es incluido en el resultado 30.

Cuando buscamos X(?=Y), el motor de expresión regular encuentra X y luego verifica si existe Y inmediatamente después de él. Si no existe, entonces la coincidencia potencial es omitida y la búsqueda continúa.

Es posible realizar pruebas más complejas, por ejemplo X(?=Y)(?=Z) significa:

  1. Encuentra X.
  2. Verifica si Y está inmediatamente después de X (omite si no es así).
  3. Verifica si Z está también inmediatamente después de X (omite si no es así).
  4. Si ambas verificaciones se cumplen, el X es una coincidencia. De lo contrario continúa buscando.

En otras palabras, dicho patrón significa que estamos buscando por X seguido de Y y Z al mismo tiempo.

Eso es posible solamente si los patrones Y y Z no se excluyen mutuamente.

Por ejemplo, \d+(?=\s)(?=.*30) busca un \d+ que sea seguido por un espacio (?=\s) y que también tenga un 30 en algún lugar después de él (?=.*30):

let str = "1 pavo cuesta 30€";

alert( str.match(/\d+(?=\s)(?=.*30)/) ); // 1

En nuestra cadena eso coincide exactamente con el número 1.

Lookahead negativo

Digamos que queremos una cantidad, no un precio de la misma cadena. Eso es el número \d+ NO seguido por .

Para eso se puede aplicar un “lookahead negativo”.

La sintaxis es: X(?!Y), que significa "busca X, pero solo si no es seguido por Y".

let str = "2 pavos cuestan 60€";

alert( str.match(/\d+\b(?!€)/g) ); // 2 (el precio es omitido)

Lookbehind

“lookahead” permite agregar una condición para “lo que sigue”.

“Lookbehind” es similar. Permite coincidir un patrón solo si hay algo anterior a él.

La sintaxis es:

  • Lookbehind positivo: (?<=Y)X, coincide X, pero solo si hay Y antes de él.
  • Lookbehind negativo: (?<!Y)X, coincide X, pero solo si no hay Y antes de él.

Por ejemplo, cambiemos el precio a dólares estadounidenses. El signo de dólar usualmente va antes del número, entonces para buscar $30 usaremos (?<=\$)\d+: una cantidad precedida por $:

let str = "1 pavo cuesta $30";

// el signo de dólar se ha escapado \$
alert( str.match(/(?<=\$)\d+/) ); // 30 (omite los números aislados)

Y si necesitamos la cantidad (un número no precedida por $), podemos usar “lookbehind negativo” (?<!\$)\d+:

let str = "2 pavos cuestan $60";

alert( str.match(/(?<!\$)\b\d+/g) ); // 2 (el precio es omitido)

Atrapando grupos

Generalmente, los contenidos dentro de los paréntesis de “lookaround” (ver alrededor) no se convierten en parte del resultado.

Ejemplo en el patrón \d+(?=€), el signo no es capturado como parte de la coincidencia. Eso es esperado: buscamos un número \d+, mientras (?=€) es solo una prueba que indica que debe ser seguida por .

Pero en algunas situaciones nosotros podríamos querer capturar también la expresión en “lookaround”, o parte de ella. Eso es posible: solo hay que rodear esa parte con paréntesis adicionales.

En los ejemplos de abajo el signo de divisa (€|kr) es capturado junto con la cantidad:

let str = "1 pavo cuesta 30€";
let regexp = /\d+(?=(€|kr))/; // paréntesis extra alrededor de €|kr

alert( str.match(regexp) ); // 30, €

Lo mismo para “lookbehind”:

let str = "1 pavo cuesta $30";
let regexp = /(?<=(\$|£))\d+/;

alert( str.match(regexp) ); // 30, $

Resumen

Lookahead y lookbehind (en conjunto conocidos como “lookaround”) son útiles cuando queremos hacer coincidir algo dependiendo del contexto antes/después.

Para expresiones regulares simples podemos hacer lo mismo manualmente. Esto es: coincidir todo, en cualquier contexto, y luego filtrar por contexto en el bucle.

Recuerda, str.match (sin el indicador g) y str.matchAll (siempre) devuelven las coincidencias como un array con la propiedad index, así que sabemos exactamente dónde están dentro del texto y podemos comprobar su contexto.

Pero generalmente “lookaround” es más conveniente.

Tipos de “lookaround”:

Patrón Tipo Coincidencias
X(?=Y) lookahead positivo X si está seguido por Y
X(?!Y) lookahead negativo X si no está seguido por Y
(?<=Y)X lookbehind positivo X si está después de Y
(?<!Y)X lookbehind negativo X si no está después de Y

Tareas

Tenemos un string de números enteros.

Crea una expresión regular que encuentre solamente los no negativos (el cero está permitido).

Un ejemplo de uso:

let regexp = /tu regexp/g;

let str = "0 12 -5 123 -18";

alert( str.match(regexp) ); // 0, 12, 123

La expresión regular para un número entero es \d+.

Podemos excluir los negativos anteponiendo un “lookbehind negativo”: (?<!-)\d+.

Pero al probarlo, notamos un resultado de más:

let regexp = /(?<!-)\d+/g;

let str = "0 12 -5 123 -18";

console.log( str.match(regexp) ); // 0, 12, 123, 8

Como puedes ver, hay coincidencia de 8, con -18. Para excluirla necesitamos asegurarnos de que regexp no comience la búsqueda desde el medio de otro número (no coincidente).

Podemos hacerlo especificando otra precedencia “lookbehind negativo”: (?<!-)(?<!\d)\d+. Ahora (?<!\d) asegura que la coicidencia no comienza después de otro dígito, justo lo que necesitamos.

También podemos unirlos en un único “lookbehind”:

let regexp = /(?<![-\d])\d+/g;

let str = "0 12 -5 123 -18";

alert( str.match(regexp) ); // 0, 12, 123

Tenemos un string con un documento HTML.

Escribe una expresión regular que inserte <h1>Hello</h1> inmediatamente después de la etiqueta <body>. La etiqueta puede tener atributos.

Por ejemplo:

let regexp = /tu expresión regular/;

let str = `
<html>
  <body style="height: 200px">
  ...
  </body>
</html>
`;

str = str.replace(regexp, `<h1>Hello</h1>`);

Después de esto el valor de str debe ser:

<html>
  <body style="height: 200px"><h1>Hello</h1>
  ...
  </body>
</html>

Para insertar algo después de la etiqueta <body>, primero debemos encontrarla. Para ello podemos usar la expresión regular <body.*?>.

En esta tarea no debemos modificar la etiqueta <body>. Solamente agregar texto después de ella.

Veamos cómo podemos hacerlo:

let str = '...<body style="...">...';
str = str.replace(/<body.*?>/, '$&<h1>Hello</h1>');

alert(str); // ...<body style="..."><h1>Hello</h1>...

En el string de reemplazo, $& significa la coincidencia misma, la parte del texto original que corresponde a <body.*?>. Es reemplazada por sí misma más <h1>Hello</h1>.

Una alternativa es el uso de “lookbehind”:

let str = '...<body style="...">...';
str = str.replace(/(?<=<body.*?>)/, `<h1>Hello</h1>`);

alert(str); // ...<body style="..."><h1>Hello</h1>...

Como puedes ver, solo está presente la parte “lookbehind” en esta expresión regular.

Esto funciona así:

  • En cada posición en el texto.
  • Chequea si está precedida por <body.*?>.
  • Si es así, tenemos una coincidencia.

La etiqueta <body.*?> no será devuelta. El resultado de esta expresión regular es un string vacío, pero coincide solo en las posiciones precedidas por <body.*?>.

Entonces reemplaza la “linea vacía”, precedida por <body.*?>, con <h1>Hello</h1>. Esto es, la inserción después de <body>.

P.S. Los indicadores de Regexp tales como s y i también nos pueden ser útiles: /<body.*?>/si. El indicador s hace que que el punto . coincida también con el carácter de salto de línea, y el indicador i hace que <body> también acepte coincidencias <BODY> en mayúsculas y minúsculas.

Mapa del Tutorial

Comentarios

lea esto antes de comentar…
  • Si tiene sugerencias sobre qué mejorar, por favor enviar una propuesta de GitHub o una solicitud de extracción en lugar de comentar.
  • Si no puede entender algo en el artículo, por favor explique.
  • Para insertar algunas palabras de código, use la etiqueta <code>, para varias líneas – envolverlas en la etiqueta <pre>, para más de 10 líneas – utilice una entorno controlado (sandbox) (plnkr, jsbin, codepen…)