Buscar y capturar textos

Funciones para ejecutar búsquedas mediante RegExp


Hasta ahora hemos visto como crear expresiones regulares en Javascript, creando objetos mediante los cuales podemos detectar textos con múltiples y variadas restricciones de una forma compacta.

Para ello, hemos utilizado métodos como .test() o .exec(), pero sin saber demasiado como funcionan. En esta sección vamos a analizarlos y ver sus características y detalles.

Métodos de un objeto RegExp

Cualquier objeto tiene los siguientes métodos que podemos ejecutar para realizar una búsqueda:

MétodoDescripción
test(text)Comprueba si la expresión regular «casa» con el texto text pasado por parámetro.
exec(text)Ejecuta una búsqueda en el texto text. Devuelve con capturas de lo que coincide.

Mientras que el primero, el método .test() se suele utilizar simplemente para comprobar si la expresión regular detecta algún texto que encaje con el proporcionado, el método .exec() es un poco más avanzado, y podemos utilizarlo para capturar coincidencias.

Detectando coincidencias

Veamos, por ejemplo, como utilizar la expresión regular siguiente con el método .test() para comprobar si encaja con alguna ocurrencia en un texto determinado:

const regexp = /.a.o/i;

regexp.test("gato");    // true
regexp.test("pato");    // true
regexp.test("perro");   // false
regexp.test("DATO");    // true   (el flag i ignora mayúsculas/minúsculas)

El método .test() siempre te devolverá un para indicar si la expresión regular ha encontrado un texto que encaja con el patrón definido.

Recuerda que aunque test() espera un por parámetro, en caso de enviarle otro objeto, lo pasará a mediante el método .toString() que existe en todos los objetos de Javascript:

const objeto = { name: "Manz" };
const regexp = /object/g;

objeto.toString();      // "[object Object]"
regexp.test(objeto);    // true

Mucho cuidado con esto, ya que de pasar un tipo de dato que no sea podrías tener resultados no esperados o previstos.

Captura de patrones (exec)

Pero con las expresiones regulares, además de poder realizar búsquedas de patrones, también se puede capturar coincidencias. De hecho, es una de sus características más potentes y versátiles.

Toda expresión regular que utilice la parentización (englobar con paréntesis fragmentos de texto) está realizando implícitamente una captura de texto, con la que es muy útil obtener rápidamente información. Para ello, en lugar de utilizar el método test(), vamos a utilizar el método exec(). Funciona exactamente igual, sólo que devuelve un con las capturas realizadas.

Antes de empezar a utilizarlo, necesitamos saber detalles sobre la parentización:

FormatoDescripción
(x)El patrón x incluído dentro de paréntesis se captura y se guarda.
(?:x)Si incluímos ?: al inicio del patrón en los paréntesis, no se captura ese patrón.
x(?=y)Busca sólo si x está seguido de y.
x(?!y)Busca sólo si x no está seguido de y.

Así pues, veamos algunos ejemplos para entenderlo bien. Observa que hemos creado una expresión regular que parentiza el texto .a.o, sin embargo, tiene un . (cualquier carácter, recuerda) fuera del paréntesis, por lo que ese carácter no se capturará (solo se captura el interior del paréntesis):

const text = `Hola Manz,

Soy el otro Manz (el gato) y necesito Whiskas.
El pato del patio sigue haciendo ruido. Te lo digo como dato.

Gracias.`;

const regexp = /(.a.o)./g;

regexp.exec(text);     // ["gato)", "gato"]
regexp.exec(text);     // ["pato ", "pato"]
regexp.exec(text);     // ["dato.", "dato"]
regexp.exec(text);     // null

Observa que en cada ejecución del método .exec() se nos devuelve un resultado diferente. Esto ocurre por usar el flag g (búsqueda global) y nos devuelve un . El primer elemento del array es la coincidencia con toda la expresión regular, mientras que el segundo elemento del array es la coincidencia con lo incluido en paréntesis.

Ten en cuenta que también podemos hacer múltiples capturas. Al definir la expresión regular, he incluído varios paréntesis para realizar varias capturas:

const regexp = /(..) (.a.o)/g;

regexp.exec(text);     // ["el gato", "el", "gato"]
regexp.exec(text);     // ["El pato", "El", "pato"]
regexp.exec(text);     // ["mo dato", "mo", "dato"]
regexp.exec(text);     // null

En esta ocasión, el devuelto tiene 3 elementos, el primero de ellos la coincidencia completa, el segundo la primera captura con parentización, y el tercer elemento la segunda captura con parentización.

El array devuelto por .exec()

Ten en cuenta que el devuelto por .exec() es un array especial que, a parte de funcionar como un array normal, tiene algunas propiedades extra que nos pueden ser de ayuda:

PropiedadDescripción
.lengthComo array, se puede consultar la longitud (coincidencia completa + capturas)
.groupsCrea un objeto con los resultados de parentizaciones nombradas (ver más adelante)
.indexPosición del donde se encontró la ocurrencia.
.inputTexto original pasado por parámetro a .exec().
.indicesSi se usa el flag d, se incluye un con las posiciones inicial y final de las coincidencias del .

Veamos el resultante de la ejecución del método .exec() en esta ocasión:

const text = "Hola Manz. El formato adecuado es 2022-08-15. Ignoraremos fechas en el formato 15-08-2022.";
const regexp = /([0-9]{4})-([0-9]{2})-([0-9]{2})/gd;

regexp.global;        // true (el flag global está activado)

const result = regexp.exec(text);    // ["2022-08-15", "2022", "08", "15"]   index: 34
regexp.exec(text);                   // null

Observa que la segunda fecha del texto no tiene el mismo formato en el mismo orden, por lo que no es capturada. Si analizamos el obtenido en la primera ejecución de .exec() y guardado en result, tendremos algo así:

result                  // ["2022-08-15", "2022", "08", "15"]
result.length           // 4
result.index            // 34
result.input === text   // true

regexp.hasIndices       // true
result.indices          // [[34, 44], [34, 38], [39, 41], [42, 44]]

El contenido en result.indices contiene varios , uno por cada elemento del array result. Así pues, 34 es la posición inicial de result[0], mientras que 44 es la posición final. En el siguiente array, 34 es la posición inicial de result[1], mientras que 38 es la posición final. Y así sucesivamente.

Recuerda que para tener esta propiedad .indices necesitas tener activado el flag d.

El array .result.indices también tiene una propiedad .groups similar a la que posee .result y que explicaremos en el siguiente apartado de Parentización nombrada.

Parentizaciones nombradas

Es posible asignarle un nombre a cada parentización realizada, de modo que sea más «humana» la forma de capturar elementos y gestionarlos después. Para ello, solo tenemos que añadir ?<nombre> al inicio de la parentización, como se puede ver en el siguiente ejemplo:

const text = `Hola Manz. Son las 13:33:02, a las 18:45:00 te avisaré para decirle a afor que deje de usar Tailwind.`;
const regexp = /(?<hours>[0-9]{2}):(?<mins>[0-9]{2}):(?<secs>[0-9]{2})/gd;

regexp.exec(text);     // ["13:33:02", "13", "33", "02"]
regexp.exec(text);     // ["18:45:00", "18", "45", "00"]
regexp.exec(text);     // null

En este caso, podremos ver que la propiedad .groups no es undefined, sino que tiene los textos de las parentizaciones capturadas:

result.groups           // { hours: "13", mins: "33", secs: "02" }
result.index            // 19
result.indices          // [[19, 27], [19, 21], [22, 24], [25, 27]]
result.indices.groups   // { hours: [19, 21], mins: [22, 24], secs: [25, 27] }

¿Quién soy yo?

Soy Manz, vivo en Tenerife (España) y soy streamer partner en Twitch y profesor. Me apasiona el universo de la programación web, el diseño y desarrollo web y la tecnología en general. Aunque soy full-stack, mi pasión es el front-end, la terminal y crear cosas divertidas y locas.

Puedes encontrar más sobre mi en Manz.dev