13 de junio de 2022

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

Compatibilidad de navegadores en lookbehind

Ten en cuenta: Lookbehind no está soportado en navegadores que no utilizan V8, como Safari, Internet Explorer.

“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 necesitamos 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:
  • Verifica 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