Guías

¿Tu regex no coincide? Lista de verificación para depurarla

Cuando una regex que parece correcta no coincide, la causa suele ser una de una docena de cosas: caracteres ocultos, barras invertidas perdidas, un punto sin escapar, un ancla o un modificador que falta. Aquí están en el orden en que conviene revisarlas, con ejemplos que puedes probar.

Logo de Regex Hunter

· 7 min de lectura

/1\.5/

atrapa

  • 1.5
  • v1.5.2

deja libre

  • 125
  • 1x5

Cuando una regex parece correcta pero no coincide, rara vez el patrón está mal de una forma interesante. Casi siempre es una de una docena de causas corrientes: la entrada tiene un carácter que no ves, se perdió una barra invertida en una cadena, un carácter especial no está escapado, falta un ancla o un modificador, o el código ejecuta la regex de forma distinta a tu tester. Abajo tienes una lista de verificación en el orden en que vale la pena revisar, con ejemplos interactivos.

Los ejemplos usan el motor de JavaScript; cuando Python u otro lenguaje se comporta de otra forma, el punto lo indica.

01Primero, un método de 60 segundos

  1. Imprime la entrada tal cual. En Python, print(repr(text)) muestra los caracteres ocultos como escapes del tipo \xa0 (un espacio de no separación), \u200b (un espacio de ancho cero) o \r. En JavaScript, [...text].map(c => c.codePointAt(0).toString(16)) lista el código de cada carácter, así que a0 o 200b saltan a la vista.
  2. Reduce el patrón. Quita trozos desde el final hasta que coincida y luego vuelve a añadirlos uno a uno. El trozo que lo rompe es el bug.
  3. Escribe dos listas de líneas de prueba: algunas que deben coincidir y otras que no. Pega ambas en la Zona de pruebas, cuyo panel Anatomía explica cada parte del patrón en español sencillo.

02La entrada no es lo que crees

El texto copiado de páginas web, PDF y apps de chat suele tener caracteres que parecen normales pero no lo son: un espacio de no separación en lugar de un espacio, un espacio de ancho cero dentro de una palabra, comillas tipográficas en lugar de rectas. Las dos líneas de abajo parecen idénticas. La segunda tiene un espacio de no separación después de los dos puntos, así que un patrón con un espacio normal no la encuentra:

Coincidencias
/price: \d+/
  • price: 40
  • price: 40
Pruébalo en la Zona de pruebas

En JavaScript, \s coincide con los dos tipos de espacio, así que es la solución cuando no puedes fiarte de la fuente:

Coincidencias2/2 como se esperaba
/price:\s\d+/
  • price: 40
  • price: 40
Pruébalo en la Zona de pruebas

Un espacio de ancho cero es peor: hel​lo se imprime como “hello” pero tiene seis caracteres, y ^hello$ no coincide con él. Las comillas tipográficas “hi” no coinciden con "hi". Imprimir la entrada (paso 1 de arriba) revela todo esto.

03Las barras invertidas no sobrevivieron

Un patrón escrito dentro de una cadena normal pierde sus barras invertidas antes de que lo vea el motor de regex:

  • JavaScript: new RegExp("\d+") se convierte en el patrón d+, que coincide con ddd y no con 42. Escribe new RegExp("\\d+") o usa un literal: /\d+/.
  • Python: usa siempre cadenas raw, r"\bcat\b". Sin la r, "\b" es un carácter de retroceso, así que "\bcat\b" nunca coincide con a cat y no da ningún error.
  • Java: cada barra invertida se duplica en el código fuente: "\\d+", "\\.".

Si un patrón funciona en un tester pero no en el código, este es el primer sospechoso.

04Un carácter especial no está escapado

. * + ? ^ $ | ( ) [ ] { } \ significan algo. El punto sin escapar es el caso clásico, porque el patrón sigue “funcionando”, solo que demasiado bien: 1.5 coincide con 125.

Coincidencias2/2 como se esperaba
/1\.5/

Debe coincidir1/1

  • version 1.5

No debe1/1

  • version 125
Pruébalo en la Zona de pruebas

Lo mismo pasa con $ en los precios, ? en las URL y + en los números de teléfono: pon una barra invertida delante o mete el carácter en una clase como [.] o [+].

05Coincide con parte de la cadena, o con nada

Una búsqueda con regex encuentra una coincidencia en cualquier parte del texto. \d{3} “valida” 12345abc porque en algún sitio aparecen tres dígitos. Para validar, ancla los dos extremos: ^\d{3}$.

Python añade un matiz: re.match solo ancla el inicio, así que re.match(r"\d{3}", "12345") tiene éxito. Usa re.fullmatch para “la cadena completa”.

El problema contrario: ^ y $ significan el inicio y el final de todo el texto a menos que actives el modificador m, así que ^ERROR no encuentra nada en la línea 5 de un log hasta que añades m. Y los archivos con finales de línea de Windows (\r\n) le juegan una mala pasada a Python: con re.M, su $ no coincide antes de \r, así que ^abc$ falla con abc\r\n. El $ de JavaScript sí coincide ahí. Quita primero el \r o permítelo: ^abc\r?$.

Más en Anclas en regex.

06Falta un modificador

  • i: sin él, hello no coincide con Hello.
  • s: sin él, . no coincide con un salto de línea, así que a.b no encuentra a y b en dos líneas.
  • g: sin él, el replace de JavaScript solo cambia la primera coincidencia: "a-b-c".replace(/-/, "+") da a+b-c.
  • u: sin él, un emoji cuenta como dos caracteres, así que ^.$ no coincide con 😀.

07El modificador g recuerda dónde se quedó (JavaScript)

Una regex con el modificador g guarda lastIndex entre llamadas. Reutilízala con test() y los resultados se alternan:

const re = /cat/g;
re.test("cat");  // true
re.test("cat");  // false: the search started at index 3
re.test("cat");  // true again

Quita la g para test() o crea la regex dentro de la función. Lo mismo vale para exec() en un bucle que nunca llega al final.

08Coincidió con demasiado

.* es codicioso: en <b>x</b><b>y</b>, el patrón <b>.*</b> se lleva todo desde el primer <b> hasta el último </b>. Usa .*? o, mejor, una clase que no pueda pasarse del final, como [^<]*. Cuantificadores codiciosos y perezosos lo explica paso a paso.

09La clase de caracteres no significa lo que parece

  • [a-Z] es un error (“range out of order”): las mayúsculas van antes que las minúsculas. Escribe [a-zA-Z].
  • Un guion en medio crea un rango. [\w-.] funciona de casualidad sin el modificador u y da error con él. Pon el guion al principio o al final: [\w.-].
  • ^ solo niega justo al principio. [^abc] es “ni a, ni b, ni c”; [a^bc] es “a, ^, b o c”.

10Tu tester usa otro motor

Un patrón creado en un tester de PCRE puede fallar en JavaScript, Python o Go: los grupos con nombre se escriben distinto, los límites del lookbehind cambian y el motor de Go no tiene lookarounds. Motores de regex comparados tiene la tabla. Prueba en el motor que usa tu código; la Zona de pruebas usa el de JavaScript.

11No falla, se cuelga

Si la regex tarda segundos con algunas entradas, no es un problema de coincidencia: los cuantificadores anidados como (a+)+ hacen que un motor con backtracking pruebe una cantidad enorme de caminos. Backtracking catastrófico muestra cómo detectarlo y corregirlo.

12Que no vuelva a pasar

Guarda las líneas de prueba junto al patrón: un comentario con dos o tres líneas que deben coincidir y dos o tres que no, o un test unitario. La Zona de pruebas puede compartir el patrón y tus líneas de prueba en un solo enlace, algo útil en una revisión de código. Y si quieres que estos reflejos de depuración se vuelvan automáticos, la caza del día son cinco minutos justo de esto: líneas que atrapar, líneas que dejar libres y un patrón que corregir hasta que ambas estén bien.