Contraer y capturar un patrón repetido en una sola expresión regular
Sigo encontrándome con situaciones en las que necesito capturar una cantidad de tokens de una cadena y después de innumerables intentos no pude encontrar una manera de simplificar el proceso.
Entonces digamos que el texto es:
inicio:prueba-prueba-lorem-ipsum-sir-doloret-etc-etc-algo:fin
Este ejemplo tiene 8 elementos en su interior, pero digamos que podría tener entre 3 y 10 elementos.
Lo ideal sería algo como esto:
start:(?:(\w+)-?){3,10}:end
bonito y limpio PERO sólo captura el último partido. mira aquí
Normalmente uso algo como esto en situaciones simples:
start:(\w+)-(\w+)-(\w+)-?(\w+)?-?(\w+)?-?(\w+)?-?(\w+)?-?(\w+)?-?(\w+)?-?(\w+)?:end
3 grupos son obligatorios y otros 7 opcionales debido al límite máximo de 10, pero esto no se ve "agradable" y sería complicado escribir y realizar un seguimiento si el límite máximo fuera 100 y las coincidencias fueran más complejas. manifestación
Y lo mejor que pude hacer hasta ahora:
start:(\w+)-((?1))-((?1))-?((?1))?-?((?1))?-?((?1))?-?((?1))?-?((?1))?:end
más cortos, especialmente si los partidos son complejos pero aún largos. manifestación
¿Alguien logró que funcionara como una solución de 1 expresión regular sin programación ?
Lo que más me interesa es cómo se puede hacer esto en PCRE, pero otras versiones también estarían bien.
Actualizar:
El propósito es validar una coincidencia y capturar tokens individuales dentro match 0
solo mediante RegEx, sin ninguna limitación de sistema operativo/software/lenguaje de programación.
Actualización 2 (recompensa):
Con la ayuda de @nhahtdh llegué a la RegExp siguiente usando \G
:
(?:start:(?=(?:[\w]+(?:-|(?=:end))){3,10}:end)|(?!^)\G-)([\w]+)
Demostración aún más corta, pero se puede describir sin repetir el código.
También estoy interesado en la versión ECMA y, como no es compatible, \G
me pregunto si hay otra forma, especialmente sin usar /g
modificador.
¡Leé esto primero!
Esta publicación es para mostrar la posibilidad en lugar de respaldar el enfoque del problema de "todo con expresiones regulares". El autor ha escrito entre 3 y 4 variaciones, cada una con errores sutiles que son difíciles de detectar antes de llegar a la solución actual.
Para su ejemplo específico, existen otras soluciones mejores que son más fáciles de mantener, como hacer coincidir y dividir la coincidencia a lo largo de los delimitadores.
Esta publicación trata sobre su ejemplo específico. Realmente dudo que sea posible una generalización completa, pero la idea detrás es reutilizable para casos similares.
Resumen
- .NET admite la captura de patrones repetidos con
CaptureCollection
clase. - Para los idiomas que admiten
\G
y miran hacia atrás, es posible que podamos construir una expresión regular que funcione con la función de coincidencia global. No es fácil escribirlo completamente correcto y es fácil escribir una expresión regular sutilmente defectuosa. - Para idiomas sin
\G
soporte de retrospectiva: es posible emular\G
con^
, cortando la cadena de entrada después de una sola coincidencia. (No cubierto en esta respuesta).
Solución
Esta solución supone que el motor de expresiones regulares admite \G
límites de coincidencia, mirar hacia adelante (?=pattern)
y mirar hacia atrás (?<=pattern)
. Los tipos de expresiones regulares Java, Perl, PCRE, .NET y Ruby admiten todas las funciones avanzadas mencionadas anteriormente.
Sin embargo, puede utilizar su expresión regular en .NET. Dado que .NET admite la captura de todas las instancias, coincide con un grupo de captura que se repite a través de CaptureCollection
la clase.
Para su caso, se puede hacer en una expresión regular, con el uso de \G
límite de coincidencia y anticipación para limitar el número de repeticiones:
(?:start:(?=\w+(?:-\w+){2,9}:end)|(?<=-)\G)(\w+)(?:-|:end)
DEMOSTRACIÓN . \w+-
Se repite entoncesla construcción\w+:end
.
(?:start:(?=\w+(?:-\w+){2,9}:end)|(?!^)\G-)(\w+)
DEMOSTRACIÓN . La construcción es\w+
para el primer elemento y luego-\w+
se repite. (Gracias a kaᵠ por la sugerencia). Esta construcción es más sencilla de razonar sobre su corrección, ya que hay menos alternancias.
\G
El límite de coincidencia es especialmente útil cuando necesitas hacer tokenización, donde necesitas asegurarte de que el motor no se salte hacia adelante y coincida con cosas que deberían haber sido inválidas.
Explicación
Analicemos la expresión regular:
(?:
start:(?=\w+(?:-\w+){2,9}:end)
|
(?<=-)\G
)
(\w+)
(?:-|:end)
La parte más fácil de reconocer está (\w+)
en la línea antepenúltima, que es la palabra que desea capturar.
La última línea también es bastante fácil de reconocer: la palabra que se va a buscar puede ir seguida de -
o :end
.
Permito que la expresión regular comience a coincidir libremente en cualquier parte de la cadena . En otras palabras, start:...:end
puede aparecer en cualquier parte de la cadena y cualquier cantidad de veces; la expresión regular simplemente coincidirá con todas las palabras. Solo necesita procesar la matriz devuelta para separar de dónde provienen realmente los tokens coincidentes.
En cuanto a la explicación, el comienzo de la expresión regular verifica la presencia de la cadena start:
, y la siguiente búsqueda anticipada verifica que el número de palabras esté dentro del límite especificado y termina con :end
. O eso, o comprobamos que el personaje anterior a la coincidencia anterior es un -
y continuamos desde la coincidencia anterior.
Para la otra construcción:
(?:
start:(?=\w+(?:-\w+){2,9}:end)
|
(?!^)\G-
)
(\w+)
Todo es casi igual, excepto que hacemos coincidir start:\w+
primero antes de hacer coincidir la repetición del formulario -\w+
. A diferencia de la primera construcción, donde hacemos coincidir start:\w+-
primero, y las instancias repetidas de \w+-
(o \w+:end
para la última repetición).
Es bastante complicado hacer que esta expresión regular funcione para hacer coincidir en medio de la cadena:
Necesitamos verificar la cantidad de palabras entre
start:
y:end
(como parte del requisito de la expresión regular original).\G
¡También coincide con el comienzo de la cadena!(?!^)
es necesario para prevenir este comportamiento. Sin ocuparse de esto, la expresión regular puede producir una coincidencia cuando no la haystart:
.Para la primera construcción, la mirada hacia atrás
(?<=-)
ya previene este caso ((?!^)
está implícito en(?<=-)
).Para la primera construcción
(?:start:(?=\w+(?:-\w+){2,9}:end)|(?<=-)\G)(\w+)(?:-|:end)
, debemos asegurarnos de no coincidir con nada gracioso después:end
. La búsqueda hacia atrás tiene ese propósito: evita que cualquier basura posterior:end
coincida.La segunda construcción no tiene este problema, ya que nos quedaremos atascados en
:
(de:end
) después de haber emparejado todas las fichas intermedias.
Versión de validación
Si desea validar que la cadena de entrada siga el formato (sin elementos adicionales delante y detrás) y extraer los datos, puede agregar anclajes como tales:
(?:^start:(?=\w+(?:-\w+){2,9}:end$)|(?!^)\G-)(\w+)
(?:^start:(?=\w+(?:-\w+){2,9}:end$)|(?!^)\G)(\w+)(?:-|:end)
(La búsqueda hacia atrás tampoco es necesaria, pero aún así debemos (?!^)
evitar \G
que coincida con el inicio de la cadena).
Construcción
Para todos los problemas en los que desea capturar todas las instancias de una repetición, no creo que exista una forma general de modificar la expresión regular. Un ejemplo de un caso "difícil" (¿o imposible?) de convertir es cuando una repetición tiene que retroceder en uno o más bucles para cumplir cierta condición.
Cuando la expresión regular original describe toda la cadena de entrada (tipo de validación), generalmente es más fácil de convertir en comparación con una expresión regular que intenta hacer coincidir desde el medio de la cadena (tipo de coincidencia). Sin embargo, siempre puede hacer una coincidencia con la expresión regular original y convertiremos el problema de tipo coincidente nuevamente en un problema de tipo de validación.
Construimos dicha expresión regular siguiendo estos pasos:
- Escriba una expresión regular que cubra la parte antes de la repetición (por ejemplo
start:
). Llamemos a este prefijo expresión regular . - Empareja y captura la primera instancia. (p. ej.
(\w+)
)
(En este punto, la primera instancia y el delimitador deberían haber coincidido) - Agregue el
\G
como una alternancia. Por lo general, también es necesario evitar que coincida con el inicio de la cadena. - Agregue el delimitador (si lo hay). (por ejemplo
-
)
(Después de este paso, el resto de los tokens también deberían haber coincidido, excepto el último tal vez) - Agregue la parte que cubre la parte después de la repetición (si es necesario) (por ejemplo
:end
). Llamemos a la parte después del sufijo de repetición expresión regular (no importa si la agregamos a la construcción). - Ahora la parte difícil. Necesitas comprobar que:
- No hay otra forma de iniciar una partida, aparte del prefijo regex . Toma nota de la
\G
sucursal. - No hay forma de iniciar ninguna coincidencia después de que se haya coincidendo con la expresión regular del sufijo . Tome nota de cómo
\G
la rama inicia una partida. - Para la primera construcción, si mezcla la expresión regular del sufijo (por ejemplo
:end
, ) con el delimitador (por ejemplo-
, ) de forma alterna, asegúrese de no terminar permitiendo la expresión regular del sufijo como delimitador.
- No hay otra forma de iniciar una partida, aparte del prefijo regex . Toma nota de la
Aunque en teoría podría ser posible escribir una sola expresión, es mucho más práctico hacer coincidir los límites exteriores primero y luego realizar una división en la parte interior.
En ECMAScript lo escribiría así:
'start:test-test-lorem-ipsum-sir-doloret-etc-etc-something:end'
.match(/^start:([\w-]+):end$/)[1] // match the inner part
.split('-') // split inner part (this could be a split regex as well)
En PHP:
$txt = 'start:test-test-lorem-ipsum-sir-doloret-etc-etc-something:end';
if (preg_match('/^start:([\w-]+):end$/', $txt, $matches)) {
print_r(explode('-', $matches[1]));
}