regulares - regex replace online
Coincidencia de texto entre delimitadores: ¿expresión regular codiciosa o floja? (3)
Para el problema común de emparejar texto entre delimitadores (por ejemplo, <
y >
), hay dos patrones comunes:
- usando el codificador
*
o+
cuantificador en la formaSTART [^END]* END
, por ejemplo<[^>]*>
, o - usando el perezoso
*?
o+?
cuantificador en la formaSTART .*? END
START .*? END
, por ej<.*?>
.
¿Hay alguna razón particular para favorecer a uno sobre el otro?
Algunas ventajas:
[^>]*
:
- Más expresiva.
- Captura líneas nuevas sin importar el
/s
indicador. - Considerado más rápido, porque el motor no tiene que retroceder para encontrar una coincidencia exitosa (con
[^>]
el motor no toma decisiones; solo le damos una forma de hacer coincidir el patrón con la cadena).
.*?
- Sin "duplicación de código": el carácter final solo aparece una vez.
- En casos más simples, el delimitador final tiene más de un carácter de longitud. (una clase de personaje no funcionaría en este caso) Una alternativa común es
(?:(?!END).)*
. Esto es aún peor si el delimitador END es otro patrón.
El primero es más explícito, es decir, definitivamente excluye que el delimitador de cierre sea parte del texto coincidente. Esto no está garantizado en el segundo caso (si la expresión regular se extiende para que coincida con más que solo esta etiqueta).
Ejemplo: si intenta hacer coincidir <tag1><tag2>Hello!
con <.*?>Hello!
, la expresión regular coincidirá
<tag1><tag2>Hello!
mientras que <[^>]*>Hello!
coincidirá
<tag2>Hello!
Lo que la mayoría de las personas no tiene en cuenta al abordar preguntas como esta es lo que sucede cuando la expresión regular no puede encontrar una coincidencia. Ahí es cuando es más probable que aparezcan los sumideros de rendimiento asesino. Por ejemplo, tome el ejemplo de Tim, donde está buscando algo como <tag>Hello!
. Considera lo que sucede con:
<.*?>Hello!
El motor de expresiones regulares encuentra un <
y rápidamente encuentra un cierre >
, pero no >Hello!
. Entonces el .*?
continúa buscando >
que es seguido por Hello!
. Si no hay uno, llegará hasta el final del documento antes de que se rinda. Luego, el motor de expresiones regulares reanuda el escaneo hasta que encuentra otro <
y vuelve a intentarlo. Ya sabemos cómo resultará eso, pero el motor de expresiones regulares, por lo general, no; pasa por el mismo rigamarole con cada <
en el documento. Ahora considera la otra expresión regular:
<[^>]*>Hello!
Como antes, coincide rápidamente desde <
hasta >
, pero no coincide con Hello!
. Retrocederá al <
, luego saldrá y comenzará a buscar otro <
. Seguirá controlando cada <
como la primera expresión regular, pero no buscará hasta el final del documento cada vez que encuentre una.
Pero es incluso peor que eso. Si lo piensas,. .*?
es efectivamente equivalente a un lookahead negativo. Dice "Antes de consumir el siguiente personaje, asegúrate de que el resto de la expresión regular no coincida en esta posición". En otras palabras,
/<.*?>Hello!/
...es equivalente a:
/<(?:(?!>Hello!).)*(?:>Hello!|/z(*FAIL))/
Por lo tanto, en cada posición que está realizando, no solo un intento de coincidencia normal, sino un aspecto mucho más costoso. (Es al menos el doble de costoso, porque la búsqueda anticipada tiene que escanear al menos un personaje, luego el .
Continúa y consume un personaje).
( (*FAIL)
es uno de los verbos de control de retroceso de Perl (también soportado en PHP). |/z(*FAIL)
significa "o llega al final del documento y abandona".
Finalmente, hay otra ventaja del enfoque de clase de carácter negado. Si bien no (como señaló @Bart) actúa como si el cuantificador fuera posesivo, no hay nada que te impida hacerlo posesivo, si tu sabor lo admite:
/<[^>]*+>Hello!/
... o envolverlo en un grupo atómico:
/(?><[^>]*>)Hello!/
Estas expresiones no solo no retrocederán innecesariamente, sino que no tienen que guardar la información de estado que hace posible el retroceso.