.net - sistema - test regex javascript
¿Por qué esta referencia inversa no funciona dentro de una mirada detrás? (1)
Hacer coincidir un carácter repetido en expresiones regulares es simple con una referencia inversa:
(.)/1
Sin embargo, me gustaría hacer coincidir el carácter después del par de caracteres, así que pensé que simplemente podría poner esto en una mirada detrás de:
(?<=(.)/1).
Desafortunadamente, esto no coincide con nada.
¿Porqué es eso? En otros sabores, no me sorprendería porque existen fuertes restricciones en la apariencia, pero .NET generalmente admite patrones arbitrariamente complicados dentro de la apariencia.
La versión corta: Lookbehinds se combinan de derecha a izquierda. Eso significa que cuando el motor de expresiones regulares se encuentra con el /1
aún no ha capturado nada en ese grupo, por lo que la expresión regular siempre falla. La solución es bastante simple:
(?<=/1(.)).
Desafortunadamente, la historia completa una vez que comienzas a usar patrones más complejos es mucho más sutil. Así que aquí está ...
Una guía para leer expresiones regulares en .NET
Primero, algunos reconocimientos importantes. La persona que me enseñó ese aspecto se combina de derecha a izquierda (y descubrió esto por su cuenta a través de mucha experimentación), fue Kobi en esta respuesta . Desafortunadamente, la pregunta que hice en ese entonces era un ejemplo muy complicado que no constituye una gran referencia para un problema tan simple. Así que pensamos que tendría sentido hacer una publicación nueva y más canónica para futuras referencias y como un objetivo adecuado. Pero, por favor, considere darle a Kobi un voto positivo para descubrir un aspecto muy importante del motor de expresiones regulares de .NET que prácticamente no está documentado (por lo que sé, MSDN lo menciona en una sola frase en una página no obvia ).
Tenga en cuenta que rexegg.com explica el funcionamiento interno de la apariencia de .NET de manera diferente (en términos de revertir la cadena, la expresión regular y cualquier captura posible). Aunque eso no haría una diferencia en el resultado del partido, me parece que ese enfoque es mucho más difícil de razonar, y al observar el código es bastante claro que esto no es lo que realmente hace la implementación.
Asi que. La primera pregunta es, ¿por qué es en realidad más sutil que la oración en negrita de arriba? Intentemos hacer coincidir un carácter que esté precedido por a
o A
utilizando un modificador local que no distingue entre mayúsculas y minúsculas. Dado el comportamiento coincidente de derecha a izquierda, uno podría esperar que esto funcione:
(?<=a(?i)).
Sin embargo, como puede ver aquí, esto no parece utilizar el modificador en absoluto. De hecho, si ponemos el modificador al frente:
(?<=(?i)a).
Otro ejemplo, que puede ser sorprendente teniendo en cuenta la coincidencia de derecha a izquierda es el siguiente:
(?<=/2(.)(.)).
¿ /2
refiere el /2
al grupo de captura izquierdo o derecho? Se refiere a la correcta, como lo muestra este ejemplo .
Un último ejemplo: cuando se compara con abc
, ¿esta captura b
o ab
?
(?<=(b|a.))c
Captura b
. (Puedes ver las capturas en la pestaña "Tabla"). Una vez más, "la imagen se aplica de derecha a izquierda" no es la historia completa.
Por lo tanto, esta publicación intenta ser una referencia completa sobre todo lo relacionado con la direccionalidad de la expresión regular en .NET, ya que no conozco ningún recurso de este tipo. El truco para leer un regex complicado en .NET es hacerlo en tres o cuatro pases . Todos, excepto el último paso, son de izquierda a derecha, sin importar el aspecto que RegexOptions.RightToLeft
detrás o RegexOptions.RightToLeft
. Creo que este es el caso, porque .NET los procesa al analizar y compilar la expresión regular.
Primera pasada: modificadores en línea
Esto es básicamente lo que muestra el ejemplo anterior. Si en algún lugar de tu expresión regular, tenías este fragmento:
...a(b(?i)c)d...
Independientemente de dónde se encuentre en el patrón o si está utilizando la opción RTL, c
no será sensible a mayúsculas y minúsculas, mientras que a
, b
y d
no (siempre que no estén afectadas por algún otro modificador global o precedente). Esa es probablemente la regla más simple.
Segundo paso: números de grupo [grupos sin nombre]
Para esta pasada, debe ignorar completamente cualquier grupo nombrado en el patrón, es decir, los de la forma (?<a>...)
. Tenga en cuenta que esto no incluye grupos con números explícitos como (?<2>...)
(que son una cosa en .NET).
Los grupos de captura están numerados de izquierda a derecha. No importa qué tan complicada sea su expresión regular, ya sea que esté usando la opción RTL o si anida docenas de miradas detrás y miradas. Cuando solo utiliza grupos de captura sin nombre, se numeran de izquierda a derecha según la posición de su paréntesis de apertura. Un ejemplo:
(a)(?<=(b)(?=(.)).((c).(d)))(e)
└1┘ └2┘ └3┘ │└5┘ └6┘│ └7┘
└───4───┘
Esto se vuelve un poco más complicado cuando se mezclan grupos sin etiquetar con grupos numerados explícitamente. Aún deberías leer todo esto de izquierda a derecha, pero las reglas son un poco más complicadas. Puede determinar el número de un grupo de la siguiente manera:
- Si el grupo tiene un número explícito, su número es obviamente ese (y solo ese) número. Tenga en cuenta que esto puede agregar una captura adicional a un número de grupo ya existente, o puede crear un nuevo número de grupo. También tenga en cuenta que cuando da números de grupo explícitos, no tienen que ser consecutivos .
(?<1>.)(?<5>.)
Es una expresión regular perfectamente válida con números de grupo2
a4
no utilizados. - Si el grupo no está etiquetado, toma el primer número no utilizado. Debido a las brechas que acabo de mencionar, esto puede ser más pequeño que el número máximo que ya se ha utilizado.
Aquí hay un ejemplo (sin anidar, por simplicidad; recuerde ordenarlos por sus paréntesis de apertura cuando estén anidados):
(a)(?<1>b)(?<2>c)(d)(e)(?<6>f)(g)(h)
└1┘└──1──┘└──2──┘└3┘└4┘└──6──┘└5┘└7┘
Observe cómo el grupo 6
explícito crea una brecha, luego el grupo que captura g
toma esa brecha no utilizada entre los grupos 4
y 6
, mientras que el grupo que captura h
toma 7
porque 6
ya está en uso. Recuerde que puede haber grupos nombrados en algún lugar entre estos, que por el momento estamos ignorando por completo.
Si se está preguntando cuál es el propósito de los grupos repetidos como el grupo 1
en este ejemplo, es posible que desee leer sobre los grupos de balanceo .
Tercera pasada: números de grupo [grupos nombrados]
Por supuesto, puede omitir este pase por completo si no hay grupos nombrados en la expresión regular.
Es una característica poco conocida que los grupos con nombre también tienen números de grupo (implícitos) en .NET, que se pueden usar en referencias inversas y patrones de sustitución para Regex.Replace
. Estos obtienen sus números en un pase separado, una vez que todos los grupos sin nombre han sido procesados. Las reglas para darles números son las siguientes:
- Cuando aparece un nombre por primera vez, el grupo obtiene el primer número no utilizado. Nuevamente, esto podría ser una brecha en los números usados si la expresión regular usa números explícitos, o podría ser uno mayor que el mayor número de grupo hasta el momento. Esto asocia permanentemente este nuevo número con el nombre actual.
- En consecuencia, cuando un nombre aparece nuevamente en la expresión regular, el grupo tendrá el mismo número que se usó para ese nombre la última vez.
Un ejemplo más completo con los tres tipos de grupos, que muestra explícitamente los pasos dos y tres:
(?<a>.)(.)(.)(?<b>.)(?<a>.)(?<5>.)(.)(?<c>.)
Pass 2: │ │└1┘└2┘│ ││ │└──5──┘└3┘│ │
Pass 3: └──4──┘ └──6──┘└──4──┘ └──7──┘
Paso final: siguiendo el motor regex
Ahora que sabemos qué modificadores se aplican a qué tokens y qué grupos tienen qué números, finalmente llegamos a la parte que realmente corresponde a la ejecución del motor de expresiones regulares, y dónde empezamos a ir y venir.
El motor de expresiones regulares de .NET puede procesar expresiones regulares y cadenas en dos direcciones: el modo habitual de izquierda a derecha (LTR) y su modo exclusivo de derecha a izquierda (RTL). Puede activar el modo RTL para toda la expresión regular con RegexOptions.RightToLeft
. En ese caso, el motor comenzará a tratar de encontrar una coincidencia al final de la cadena y pasará a la izquierda a través de la expresión regular y la cadena. Por ejemplo, la expresión regular simple.
a.*b
Coincidiría con una b
, luego intentaría emparejar .*
la izquierda de la misma (retrocediendo según sea necesario) de tal manera que haya un a
algún lugar a la izquierda de la misma. Por supuesto, en este sencillo ejemplo, el resultado entre los modos LTR y RTL es idéntico, pero ayuda a hacer un esfuerzo consciente para seguir al motor en su retroceso. Puede hacer una diferencia para algo tan simple como los modificadores poco comunes. Considere la expresión regular
a.*?b
en lugar. Estamos tratando de emparejar axxbxxb
. En el modo LTR, obtiene la coincidencia axxb
como se esperaba, porque el cuantificador no satisfecho está satisfecho con el xx
. Sin embargo, en el modo RTL, en realidad coincidirías con toda la cadena, ya que la primera b
se encuentra al final de la cadena, pero luego .*?
necesita coincidir con todo xxbxx
para que coincida.
Y claramente también hace una diferencia para las referencias inversas, como lo muestra el ejemplo en la pregunta y en la parte superior de esta respuesta. En el modo LTR usamos (.)/1
para hacer coincidir los caracteres repetidos y en el modo RTL usamos /1(.)
, Ya que necesitamos asegurarnos de que el motor de expresiones regulares encuentre la captura antes de intentar referenciarla.
Teniendo esto en cuenta, podemos ver las miradas desde una nueva perspectiva. Cuando el motor de expresiones regulares se encuentra con algún aspecto, lo procesa de la siguiente manera:
- Recuerda su posición actual
x
en la cadena de destino, así como su dirección de procesamiento actual. - Ahora aplica el modo RTL, independientemente del modo en el que se encuentre actualmente.
- Luego, los contenidos de la mirada detrás de se hacen coincidir de derecha a izquierda, comenzando desde la posición actual
x
. - Una vez que el aspecto se procesa completamente, si se pasa, la posición del motor de expresiones regulares se restablece en la posición
x
y se restaura la dirección de procesamiento original.
Mientras que un lookahead parece mucho más inocuo (ya que casi nunca encontramos problemas como el de la pregunta con ellos), su comportamiento es prácticamente el mismo, excepto que impone el modo LTR. Por supuesto, en la mayoría de los patrones que son solo LTR, esto nunca se nota. Pero si la expresión regular en sí misma coincide en modo RTL, o estamos haciendo algo tan loco como poner un lookahead dentro de un look beind, entonces el lookahead cambiará la dirección de procesamiento como lo hace el lookebind.
Entonces, ¿cómo debería realmente leer una expresión regular que hace cosas divertidas como esta? El primer paso es dividirlo en componentes separados, que suelen ser tokens individuales junto con sus cuantificadores relevantes. Luego, dependiendo de si la expresión regular es LTR o RTL, comience a ir de arriba a abajo o de abajo a arriba, respectivamente. Siempre que encuentre una mirada en el proceso, verifique en qué dirección se encuentra y salte al extremo correcto y lea la mirada desde allí. Cuando haya terminado con el lookaround, continúe con el patrón circundante.
Por supuesto, hay otra trampa ... cuando encuentra una alternancia (..|..|..)
, las alternativas siempre se intentan de izquierda a derecha, incluso durante la comparación de RTL. Por supuesto, dentro de cada alternativa, el motor procede de derecha a izquierda.
Aquí hay un ejemplo un tanto artificial para mostrar esto:
.+(?=.(?<=a.+).).(?<=.(?<=b.|c.)..(?=d.|.+(?<=ab*?))).
Y aquí es cómo podemos dividir esto. Los números a la izquierda muestran el orden de lectura si la expresión regular está en modo LTR. Los números a la derecha muestran el orden de lectura en modo RTL:
LTR RTL
1 .+ 18
(?=
2 . 14
(?<=
4 a 16
3 .+ 17
)
5 . 13
)
6 . 13
(?<=
17 . 12
(?<=
14 b 9
13 . 8
|
16 c 11
15 . 10
)
12 .. 7
(?=
7 d 2
8 . 3
|
9 .+ 4
(?<=
11 a 6
10 b*? 5
)
)
)
18 . 1
Espero sinceramente que nunca utilices algo tan loco como este en el código de producción, pero tal vez algún día un colega amigable deje algunas expresiones regulares de solo escritura en el código base de tu empresa antes de ser despedido, y ese día espero que esto suceda. La guía podría ayudarte a descubrir qué diablos está pasando.
Sección avanzada: grupos de equilibrio
En aras de la integridad, esta sección explica cómo los grupos de equilibrio se ven afectados por la direccionalidad del motor de expresiones regulares. Si no sabe qué son los grupos de balanceo, puede ignorar esto de manera segura. Si desea saber qué son los grupos de balanceo, he escrito sobre esto aquí , y en esta sección se supone que sabe al menos eso acerca de ellos.
Hay tres tipos de sintaxis de grupo que son relevantes para los grupos de balanceo.
- Grupos explícitamente nombrados o numerados como
(?<a>...)
o(?<2>...)
(o incluso grupos numerados implícitamente), que hemos tratado anteriormente. - Grupos que emergen de una de las pilas de captura como
(?<-a>...)
y(?<-2>...)
. Estos se comportan como uno esperaría. Cuando se encuentran (en el orden de procesamiento correcto descrito anteriormente), simplemente salen de la pila de captura correspondiente. Podría valer la pena señalar que estos no obtienen números de grupo implícitos. - Los grupos de balanceo "apropiados"
(?<ba>...)
que normalmente se utilizan para capturar la cadena desde el último deb
. Su comportamiento se vuelve extraño cuando se mezcla con el modo de derecha a izquierda, y de eso trata esta sección.
La conclusión es que la función (?<ba>...)
se puede utilizar con el modo de derecha a izquierda. Sin embargo, después de mucha experimentación, el comportamiento (extraño) en realidad parece seguir algunas reglas, que estoy describiendo aquí.
En primer lugar, veamos un ejemplo que muestra por qué las complicaciones complican la situación. Estamos haciendo coincidir la cadena abcde...wvxyz
. Considere la siguiente expresión regular:
(?<a>fgh).{8}(?<=(?<b-a>.{3}).{2})
Al leer la expresión regular en el orden que presenté anteriormente, podemos ver que:
- El regex captura
fgh
en el grupoa
. - El motor luego mueve 8 caracteres a la derecha.
- El lookbehind cambia al modo RTL.
-
.{2}
mueve dos caracteres a la izquierda. - Finalmente,
(?<ba>.{3})
es el grupo de balanceo que saca la captura del grupoa
y empuja algo al grupob
. En este caso, el grupo coincide conlmn
y empujamos aijk
al grupob
como se esperaba.
Sin embargo, debe quedar claro a partir de este ejemplo, que al cambiar los parámetros numéricos, podemos cambiar la posición relativa de las subcadenas que coinciden con los dos grupos. Incluso podemos hacer que esas subcadenas se crucen, o tener una contenida completamente dentro de la otra haciendo las 3
más pequeñas o más grandes. En este caso, ya no queda claro qué significa empujar todo entre las dos subcadenas coincidentes.
Resulta que hay tres casos para distinguir.
Caso 1: (?<a>...)
coincide con la izquierda de (?<ba>...)
Este es el caso normal. La captura superior se extrae de a
y todo lo que se encuentra entre las subcadenas que coinciden con los dos grupos se empuja hacia b
. Considere las siguientes dos subcadenas para los dos grupos:
abcdefghijklmnopqrstuvwxyz
└──<a>──┘ └──<b-a>──┘
Que puede obtener con el regex
(?<a>d.{8}).+$(?<=(?<b-a>.{11}).)
Entonces mn
sería empujado hacia b
.
Caso 2: (?<a>...)
y (?<ba>...)
intersecan
Esto incluye el caso en que las dos subcadenas se tocan, pero no contienen ningún carácter común (solo un límite común entre los caracteres). Esto puede suceder si uno de los grupos está dentro de un mirador y el otro no está o está dentro de un mirador diferente. En este caso, la intersección de ambas subclases se empujará hacia b
. Esto sigue siendo cierto cuando la subcadena está completamente contenida dentro de la otra.
Aquí hay varios ejemplos para mostrar esto:
Example: Pushes onto <b>: Possible regex:
abcdefghijklmnopqrstuvwxyz "" (?<a>d.{8}).+$(?<=(?<b-a>.{11})...)
└──<a>──┘└──<b-a>──┘
abcdefghijklmnopqrstuvwxyz "jkl" (?<a>d.{8}).+$(?<=(?<b-a>.{11}).{6})
└──<a>┼─┘ │
└──<b-a>──┘
abcdefghijklmnopqrstuvwxyz "klmnopq" (?<a>k.{8})(?<=(?<b-a>.{11})..)
│ └──<a>┼─┘
└──<b-a>──┘
abcdefghijklmnopqrstuvwxyz "" (?<=(?<b-a>.{7})(?<a>.{4}o))
└<b-a>┘└<a>┘
abcdefghijklmnopqrstuvwxyz "fghijklmn" (?<a>d.{12})(?<=(?<b-a>.{9})..)
└─┼──<a>──┼─┘
└─<b-a>─┘
abcdefghijklmnopqrstuvwxyz "cdefg" (?<a>c.{4})..(?<=(?<b-a>.{9}))
│ └<a>┘ │
└─<b-a>─┘
Caso 3: (?<a>...)
coincide con el derecho de (?<ba>...)
Este caso no lo entiendo realmente y lo consideraría un error: cuando la subcadena que coincide con (?<ba>...)
se haya dejado correctamente de la subcadena que coincide con (?<a>...)
(con al menos una carácter entre ellos, de modo que no comparten un límite común), nada se empuja b
. Con eso no quiero decir nada, ni siquiera una cadena vacía: la pila de captura permanece vacía. Sin embargo, la coincidencia con el grupo sigue teniendo éxito, y la captura correspondiente se extrae del grupo.
Lo que es particularmente molesto acerca de esto es que este caso probablemente sea mucho más común que el caso 2, ya que esto es lo que sucede si intenta usar grupos de balanceo de la forma en que estaban destinados a ser utilizados, pero de manera simple de derecha a izquierda. expresiones regulares
Actualización sobre el caso 3: Después de algunas pruebas más realizadas por Kobi , resulta que algo sucede en la pila b
. Parece que no se ha m.Groups["b"].Success
nada, porque m.Groups["b"].Success
será False
y m.Groups["b"].Captures.Count
será 0
. Sin embargo, dentro de la expresión regular, el condicional (?(b)true|false)
ahora usará la rama true
. También en .NET parece que se puede hacer (?<-b>)
m.Groups["b"]
(?<-b>)
después (después de lo cual acceder a m.Groups["b"]
lanzará una excepción), mientras que Mono lanza una excepción inmediatamente al coincidir con la expresión regular. Error de hecho.