regex - probar - expresiones regulares java
Patrón Regex para coincidir, excluyendo cuando.../Excepto entre (6)
Haga tres coincidencias diferentes y maneje la combinación de las tres situaciones utilizando la lógica condicional dentro del programa. No necesita manejar todo en una expresión regular gigante.
EDITAR: déjame expandir un poco porque la pregunta se volvió más interesante :-)
La idea general que está tratando de capturar aquí es hacer coincidir un cierto patrón de expresiones regulares, pero no cuando hay ciertos otros patrones (podría ser cualquier número) presentes en la cadena de prueba. Afortunadamente, puede aprovechar su lenguaje de programación: mantenga las expresiones regulares simples y solo use un compuesto condicional. Una mejor práctica sería capturar esta idea en un componente reutilizable, así que creemos una clase y un método que la implementen:
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
public class MatcherWithExceptions {
private string m_searchStr;
private Regex m_searchRegex;
private IEnumerable<Regex> m_exceptionRegexes;
public string SearchString {
get { return m_searchStr; }
set {
m_searchStr = value;
m_searchRegex = new Regex(value);
}
}
public string[] ExceptionStrings {
set { m_exceptionRegexes = from es in value select new Regex(es); }
}
public bool IsMatch(string testStr) {
return (
m_searchRegex.IsMatch(testStr)
&& !m_exceptionRegexes.Any(er => er.IsMatch(testStr))
);
}
}
public class App {
public static void Main() {
var mwe = new MatcherWithExceptions();
// Set up the matcher object.
mwe.SearchString = @"/b/d{5}/b";
mwe.ExceptionStrings = new string[] {
@"/.$"
, @"/(.*" + mwe.SearchString + @".*/)"
, @"if/(.*" + mwe.SearchString + @".*//endif"
};
var testStrs = new string[] {
"1." // False
, "11111." // False
, "(11111)" // False
, "if(11111//endif" // False
, "if(11111" // True
, "11111" // True
};
// Perform the tests.
foreach (var ts in testStrs) {
System.Console.WriteLine(mwe.IsMatch(ts));
}
}
}
Así que arriba, configuramos la cadena de búsqueda (los cinco dígitos), varias cadenas de excepción (su s1 , s2 y s3 ), y luego tratamos de hacer coincidir varias cadenas de prueba. Los resultados impresos deben ser como se muestran en los comentarios al lado de cada cadena de prueba.
- Edite-- Las respuestas actuales tienen algunas ideas útiles, pero quiero algo más completo que pueda comprender y reutilizar al 100%; es por eso que puse una recompensa. También las ideas que funcionan en todas partes son mejores para mí que la sintaxis estándar como /K
Esta pregunta es acerca de cómo puedo hacer coincidir un patrón, excepto en algunas situaciones s1 s2 s3. Doy un ejemplo específico para mostrar mi significado, pero prefiero una respuesta general que pueda comprender al 100% para poder reutilizarla en otras situaciones.
Ejemplo
Quiero unir cinco dígitos usando /b/d{5}/b
pero no en tres situaciones s1 s2 s3:
s1: No en una línea que termina con un período como esta oración.
s2: No en cualquier lugar dentro de los parientes.
s3: No dentro de un bloque que comienza con if(
y termina con //endif
Sé cómo resolver cualquiera de s1 s2 s3 con un lookahead y un lookbehind, especialmente en C # lookbehind o /K
en PHP.
Por ejemplo
s1 (?m)(?!/d+.*?/.$)/d+
s3 con C # lookbehind (?<!if/(/D*(?=/d+.*?//endif))/b/d+/b
s3 con PHP / K (?:(?:if/(.*?//endif)/D*)*/K/d+
Pero la combinación de condiciones juntas hace que mi cabeza explote. Aún más malas noticias es que podría necesitar agregar otras condiciones s4 s5 en otro momento.
La buena noticia es que no me importa si proceso los archivos usando los lenguajes más comunes como PHP, C #, Python o la lavadora de mi vecino. :) Soy bastante principiante en Python y Java pero me interesa saber si tiene una solución.
Así que vine aquí para ver si alguien piensa en una receta flexible.
Las sugerencias están bien: no es necesario que me proporciones el código completo. :)
Gracias.
Hans, si no te importa, usé la lavadora de tu vecino llamada Perl :)
Editado: debajo de un pseudo código:
loop through input
if line contains ''if('' set skip=true
if skip= true do nothing
else
if line match ''/b/d{5}/b'' set s0=true
if line does not match s1 condition set s1=true
if line does not match s2 condition set s2=true
if s0,s1,s2 are true print line
if line contains ''//endif'' set skip=false
Dado el archivo input.txt:
tiago@dell:~$ cat input.txt
this is a text
it should match 12345
if(
it should not match 12345
//endif
it should match 12345
it should not match 12345.
it should not match ( blabla 12345 blablabla )
it should not match ( 12345 )
it should match 12345
Y el script validator.pl:
tiago@dell:~$ cat validator.pl
#! /usr/bin/perl
use warnings;
use strict;
use Data::Dumper;
sub validate_s0 {
my $line = $_[0];
if ( $line =~ /d{5/ ){
return "true";
}
return "false";
}
sub validate_s1 {
my $line = $_[0];
if ( $line =~ //.$/ ){
return "false";
}
return "true";
}
sub validate_s2 {
my $line = $_[0];
if ( $line =~ /.*?/(.*/d{5.*?/).*/ ){
return "false";
}
return "true";
}
my $skip = "false";
while (<>){
my $line = $_;
if( $line =~ /if/(/ ){
$skip = "true";
}
if ( $skip eq "false" ) {
my $s0_status = validate_s0 "$line";
my $s1_status = validate_s1 "$line";
my $s2_status = validate_s2 "$line";
if ( $s0_status eq "true"){
if ( $s1_status eq "true"){
if ( $s2_status eq "true"){
print "$line";
}
}
}
}
if ( $line =~ /////endif/) {
$skip="false";
}
}
Ejecución:
tiago@dell:~$ cat input.txt | perl validator.pl it should match 12345 it should match 12345 it should match 12345
Hans, tomaré el cebo y daré cuerpo a mi respuesta anterior. Dijiste que querías "algo más completo", así que espero que no te importe la respuesta larga, solo intentar por favor. Comencemos con algunos antecedentes.
En primer lugar, esta es una excelente pregunta. A menudo hay preguntas sobre la coincidencia de ciertos patrones, excepto en ciertos contextos (por ejemplo, dentro de un bloque de código o dentro de paréntesis). Estas preguntas a menudo dan lugar a soluciones bastante incómodas. Entonces su pregunta sobre contextos múltiples es un desafío especial.
Sorpresa
Sorprendentemente, hay al menos una solución eficiente que es general, fácil de implementar y un placer de mantener. Funciona con todos los sabores regex que le permiten inspeccionar grupos de captura en su código. Y resulta que responde una serie de preguntas comunes que al principio pueden sonar diferentes a las tuyas: "unir todo excepto Donuts", "reemplazar todo pero ...", "unir todas las palabras excepto las de la lista negra de mi madre", "ignorar" etiquetas "," temperatura de coincidencia a menos que esté en cursiva "...
Tristemente, la técnica no es bien conocida: estimo que en veinte preguntas de SO que podrían usarla, solo una tiene una respuesta que lo mencione, lo que significa quizás una en cincuenta o sesenta respuestas. Ver mi intercambio con Kobi en los comentarios. La técnica se describe con cierta profundidad en este artículo que lo llama (de manera optimista) el "mejor truco de regex". Sin entrar en tantos detalles, trataré de darte una idea clara de cómo funciona la técnica. Para obtener más detalles y ejemplos de código en varios idiomas, lo invito a consultar ese recurso.
Una variación mejor conocida
Existe una variación que utiliza la sintaxis específica de Perl y PHP que logra lo mismo. Lo verá en SO en manos de maestros regex como CasimiretHippolyte y HamZa . Te contaré más sobre esto a continuación, pero mi enfoque aquí está en la solución general que funciona con todos los sabores regex (siempre y cuando puedas inspeccionar los grupos de captura en tu código).
Gracias por todo el fondo, zx81 ... ¿Pero cuál es la receta?
Hecho clave
El método devuelve la coincidencia en la captura del Grupo 1. No le importa en absoluto la combinación general.
De hecho, el truco es hacer coincidir los diversos contextos que no queremos (encadenar estos contextos usando la alternancia |
O /) para "neutralizarlos". Después de hacer coincidir todos los contextos no deseados, la parte final de la alternancia coincide con lo que queremos y lo captura en el Grupo 1.
La receta general es
Not_this_context|Not_this_either|StayAway|(WhatYouWant)
Esto coincidirá con Not_this_context
, pero en cierto sentido esa coincidencia entra en un contenedor de basura, porque no miraremos las coincidencias generales: solo observamos las capturas del Grupo 1.
En su caso, con sus dígitos y sus tres contextos para ignorar, podemos hacer:
s1|s2|s3|(/b/d+/b)
Tenga en cuenta que, dado que en realidad hacemos coincidir s1, s2 y s3 en lugar de tratar de evitarlos con vistas alternativas, las expresiones individuales para s1, s2 y s3 pueden permanecer claras como el día. (Son las subexpresiones en cada lado de un |
)
La expresión completa se puede escribir así:
(?m)^.*/.$|/([^/)]*/)|if/(.*?//endif|(/b/d+/b)
Vea esta demo (pero concéntrese en los grupos de captura en el panel inferior derecho).
Si intentas mentalmente dividir esta expresión regular en cada uno |
delimitador, en realidad es solo una serie de cuatro expresiones muy simples.
Para los sabores que admiten espaciado libre, esto se lee particularmente bien.
(?mx)
### s1: Match line that ends with a period ###
^.*/.$
| ### OR s2: Match anything between parentheses ###
/([^/)]*/)
| ### OR s3: Match any if(...//endif block ###
if/(.*?//endif
| ### OR capture digits to Group 1 ###
(/b/d+/b)
Esto es excepcionalmente fácil de leer y mantener.
Extendiendo la expresión regular
Cuando desee ignorar más situaciones s4 y s5, agréguelas en más alternativas a la izquierda:
s4|s5|s1|s2|s3|(/b/d+/b)
¿Como funciona esto?
Los contextos que no desea se agregan a una lista de alternancias de la izquierda: coincidirán, pero estas coincidencias generales nunca se examinan, por lo que compararlas es una forma de ponerlas en un "cubo de basura".
El contenido que desea, sin embargo, se captura en el Grupo 1. Luego debe verificar programáticamente que el Grupo 1 esté configurado y no esté vacío. Esta es una tarea de programación trivial (y más adelante hablaremos de cómo se hace), especialmente considerando que le deja una expresión regular simple que puede comprender de un vistazo y revisar o ampliar según sea necesario.
No siempre soy fanático de las visualizaciones, pero este hace un buen trabajo al mostrar lo simple que es el método. Cada "línea" corresponde a una posible coincidencia, pero solo la línea inferior se captura en el Grupo 1.
Variación Perl / PCRE
En contraste con la solución general anterior, existe una variación para Perl y PCRE que a menudo se ve en SO, al menos en manos de dioses regex como @CasimiretHippolyte y @HamZa. Es:
(?:s1|s2|s3)(*SKIP)(*F)|whatYouWant
En tu caso:
(?m)(?:^.*/.$|/([^()]*/)|if/(.*?//endif)(*SKIP)(*F)|/b/d+/b
Esta variación es un poco más fácil de usar porque el contenido coincidente en los contextos s1, s2 y s3 simplemente se omite, por lo que no es necesario inspeccionar las capturas del Grupo 1 (observe que los paréntesis han desaparecido). Los partidos solo contienen lo que whatYouWant
Tenga en cuenta que (*F)
, (*FAIL)
y (?!)
Son todos lo mismo. Si quisieras ser más oscuro, podrías usar (*SKIP)(?!)
demo para esta versión
Aplicaciones
Aquí hay algunos problemas comunes que esta técnica a menudo puede resolver fácilmente. Notarás que la selección de palabras puede hacer que algunos de estos problemas suenen diferentes, mientras que de hecho son prácticamente idénticos.
- ¿Cómo puedo hacer coincidir Foo, excepto en cualquier parte de una etiqueta como
<a stuff...>...</a>
? - ¿Cómo puedo hacer coincidir Foo, excepto en una etiqueta
<i>
o un fragmento de JavaScript (más condiciones)? - ¿Cómo puedo unir todas las palabras que no están en esta lista negra?
- ¿Cómo puedo ignorar algo dentro de un bloque SUB ... END SUB?
- ¿Cómo puedo unir todo excepto ... s1 s2 s3?
Cómo programar las capturas del grupo 1
No lo hizo como para el código, pero, para completar ... El código para inspeccionar el Grupo 1 obviamente dependerá de su idioma de elección. En cualquier caso, no debería agregar más de un par de líneas al código que usaría para inspeccionar las coincidencias.
Si tiene dudas, le recomiendo que mire la sección de ejemplos de código del artículo mencionado anteriormente, que presenta el código para bastantes idiomas.
Alternativas
Dependiendo de la complejidad de la pregunta y del motor de expresiones regulares utilizado, existen varias alternativas. Estos son los dos que se pueden aplicar a la mayoría de las situaciones, que incluyen múltiples condiciones. En mi opinión, ninguno es tan atractivo como la receta s1|s2|s3|(whatYouWant)
, aunque solo sea porque la claridad siempre gana.
1. Reemplazar y luego Match.
Una buena solución que suena malvada pero que funciona bien en muchos entornos es trabajar en dos pasos. Una primera expresión regular neutraliza el contexto que desea ignorar reemplazando cadenas potencialmente conflictivas. Si solo desea hacer coincidir, puede reemplazarlo con una cadena vacía, luego ejecute la coincidencia en el segundo paso. Si desea reemplazar, primero puede reemplazar las cadenas para ignorar con algo distintivo, por ejemplo, rodeando sus dígitos con una cadena de ancho fijo de @@@
. Después de este reemplazo, puede reemplazar lo que realmente quería, luego deberá revertir sus cadenas distintivas @@@
.
2. Miradas alternativas.
Su publicación original mostró que usted entiende cómo excluir una sola condición usando lookarounds. Dijiste que C # es excelente para esto, y tienes razón, pero no es la única opción. Los sabores regex de .NET encontrados en C #, VB.NET y Visual C ++ por ejemplo, así como el módulo regex
aún experimental para reemplazar a re
en Python, son los únicos dos motores que conozco que admiten lookbehind de ancho infinito. Con estas herramientas, una condición en un vistazo puede ayudar a mirar no solo hacia atrás sino también hacia el encuentro y más allá del juego, evitando la necesidad de coordinar con anticipación. ¿Más condiciones? Más orientaciones
Reciclando la expresión regular que tenía para s3 en C #, todo el patrón se vería así.
(?!.*/.)(?<!/([^()]*(?=/d+[^)]*/)))(?<!if/(/D*(?=/d+.*?//endif))/b/d+/b
Pero ahora sabes que no estoy recomendando esto, ¿verdad?
Deleciones
@HamZa y @Jerry me han sugerido que mencione un truco adicional para casos en los que simplemente WhatYouWant
. Recuerda que la receta para WhatYouWant
(capturándola en el Grupo 1) era s1|s2|s3|(WhatYouWant)
, ¿verdad? Para eliminar todas las instancias de WhatYouWant
, cambie la expresión regular a
(s1|s2|s3)|WhatYouWant
Para la cadena de reemplazo, usa $1
. Lo que sucede aquí es que para cada instancia de s1|s2|s3
que coincida, el reemplazo de $1
reemplaza a esa instancia consigo mismo (referenciado por $1
). Por otro lado, cuando WhatYouWant
coincide, se reemplaza por un grupo vacío y nada más, y por lo tanto se elimina. Vea esta demo , gracias @HamZa y @Jerry por sugerir esta maravillosa adición.
Reemplazos
Esto nos lleva a reemplazos, sobre los cuales tocaré brevemente.
- Cuando reemplace con nada, vea el truco de "Eliminaciones" arriba.
- Al reemplazar, si usa Perl o PCRE, use la variación
(*SKIP)(*F)
mencionada anteriormente para que coincida exactamente con lo que desea, y realice un reemplazo directo. - En otros sabores, dentro de la llamada a la función de reemplazo, inspeccione la coincidencia mediante una devolución de llamada o lambda, y reemplace si está configurado el Grupo 1. Si necesita ayuda con esto, el artículo ya mencionado le dará el código en varios idiomas.
¡Que te diviertas!
No, espera, ¡hay más!
Ah, nah, lo guardaré para mis memorias en veinte volúmenes, que se lanzarán la próxima primavera.
Igual que @ zx81 (*SKIP)(*F)
pero con el uso de una aserción de búsqueda anticipada negativa.
(?m)(?:if/(.*?////endif|/([^()]*/))(*SKIP)(*F)|/b/d+/b(?!.*/.$)
En Python, me gustaría hacer esto fácilmente,
import re
string = """cat 123 sat.
I like 000 not (456) though 111 is fine
222 if( //endif if(cat==789 stuff //endif 333"""
for line in string.split(''/n''): # Split the input according to the `/n` character and then iterate over the parts.
if not line.endswith(''.''): # Don''t consider the part which ends with a dot.
for i in re.split(r''/([^()]*/)|if/(.*?//endif'', line): # Again split the part by brackets or if condition which endswith `//endif` and then iterate over the inner parts.
for j in re.findall(r''/b/d+/b'', i): # Then find all the numbers which are present inside the inner parts and then loop through the fetched numbers.
print(j) # Prints the number one ny one.
Salida:
000
111
222
333
No estoy seguro si esto lo ayudaría o no, pero estoy brindando una solución considerando las siguientes suposiciones:
- Necesita una solución elegante para verificar todas las condiciones
- Las condiciones pueden cambiar en el futuro y en cualquier momento.
- Una condición no debe depender de los demás.
Sin embargo, también consideré lo siguiente:
- El archivo dado tiene errores mínimos. Si lo hace, entonces mi código podría necesitar algunas modificaciones para lidiar con eso.
- Usé Stack para realizar un seguimiento de
if(
blocks.
Ok aquí está la solución -
Usé C # y con él MEF (Microsoft Extensibility Framework) para implementar los analizadores configurables. La idea es utilizar un analizador único para analizar y una lista de clases de validador configurables para validar la línea y devolver verdadero o falso en función de la validación. Luego puede agregar o eliminar cualquier validador en cualquier momento o agregar nuevos si lo desea. Hasta ahora ya he implementado para S1, S2 y S3 que mencionas, revisa las clases en el punto 3. Tienes que agregar clases para s4, s5 si lo necesitas en el futuro.
Primero, crea las interfaces -
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace FileParserDemo.Contracts { public interface IParser { String[] GetMatchedLines(String filename); } public interface IPatternMatcher { Boolean IsMatched(String line, Stack<string> stack); } }
Luego viene el lector de archivos y el comprobador -
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using FileParserDemo.Contracts; using System.ComponentModel.Composition.Hosting; using System.ComponentModel.Composition; using System.IO; using System.Collections; namespace FileParserDemo.Parsers { public class Parser : IParser { [ImportMany] IEnumerable<Lazy<IPatternMatcher>> parsers; private CompositionContainer _container; public void ComposeParts() { var catalog = new AggregateCatalog(); catalog.Catalogs.Add(new AssemblyCatalog(typeof(IParser).Assembly)); _container = new CompositionContainer(catalog); try { this._container.ComposeParts(this); } catch { } } public String[] GetMatchedLines(String filename) { var matched = new List<String>(); var stack = new Stack<string>(); using (StreamReader sr = File.OpenText(filename)) { String line = ""; while (!sr.EndOfStream) { line = sr.ReadLine(); var m = true; foreach(var matcher in this.parsers){ m = m && matcher.Value.IsMatched(line, stack); } if (m) { matched.Add(line); } } } return matched.ToArray(); } } }
Luego viene la implementación de las fichas individuales, los nombres de las clases se explican por sí mismos, así que no creo que necesiten más descripciones.
using FileParserDemo.Contracts; using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace FileParserDemo.PatternMatchers { [Export(typeof(IPatternMatcher))] public class MatchAllNumbers : IPatternMatcher { public Boolean IsMatched(String line, Stack<string> stack) { var regex = new Regex("//d+"); return regex.IsMatch(line); } } [Export(typeof(IPatternMatcher))] public class RemoveIfBlock : IPatternMatcher { public Boolean IsMatched(String line, Stack<string> stack) { var regex = new Regex("if//("); if (regex.IsMatch(line)) { foreach (var m in regex.Matches(line)) { //push the if stack.Push(m.ToString()); } //ignore current line, and will validate on next line with stack return true; } regex = new Regex("//endif"); if (regex.IsMatch(line)) { foreach (var m in regex.Matches(line)) { stack.Pop(); } } return stack.Count == 0; //if stack has an item then ignoring this block } } [Export(typeof(IPatternMatcher))] public class RemoveWithEndPeriod : IPatternMatcher { public Boolean IsMatched(String line, Stack<string> stack) { var regex = new Regex("(?m)(?!//d+.*?//.$)//d+"); return regex.IsMatch(line); } } [Export(typeof(IPatternMatcher))] public class RemoveWithInParenthesis : IPatternMatcher { public Boolean IsMatched(String line, Stack<string> stack) { var regex = new Regex("//(.*//d+.*//)"); return !regex.IsMatch(line); } } }
El programa -
using FileParserDemo.Contracts; using FileParserDemo.Parsers; using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace FileParserDemo { class Program { static void Main(string[] args) { var parser = new Parser(); parser.ComposeParts(); var matches = parser.GetMatchedLines(Path.GetFullPath("test.txt")); foreach (var s in matches) { Console.WriteLine(s); } Console.ReadLine(); } } }
Para las pruebas tomé el archivo de muestra de Test.txt
como Test.txt
que tenía las siguientes líneas:
this is a text
it should match 12345
if(
it should not match 12345
//endif
it should match 12345
it should not match 12345.
it should not match ( blabla 12345 blablabla )
it should not match ( 12345 )
it should match 12345
Da la salida -
it should match 12345
it should match 12345
it should match 12345
No sé si esto te ayudaría o no, me divertí mucho jugando con eso ... :)
La mejor parte es que, para agregar una nueva condición, todo lo que tiene que hacer es proporcionar una implementación de IPatternMatcher
, se llamará automáticamente y se validará.
Su requisito de que no esté dentro de los parientes es imposible de satisfacer en todos los casos. A saber, si de alguna manera puede encontrar un (
a la izquierda y )
a la derecha, no siempre significa que está dentro de los parientes. P.ej.
(....) + 55555 + (.....)
- no dentro de parens aún hay (
y )
a izquierda y derecha
Ahora puede pensar que es inteligente y buscar (
a la izquierda solo si no se encuentra )
antes y viceversa a la derecha. Esto no funcionará para este caso:
((.....) + 55555 + (.....))
- dentro de parens aunque haya cierre )
y (
hacia la izquierda y hacia la derecha.
Es imposible averiguar si está dentro de parens utilizando expresiones regulares, ya que la expresión regular no puede contar cuántos pares se han abierto y cuántos han cerrado.
Considere esta tarea más fácil: utilizando expresiones regulares, descubra si todos los paréntesis (posiblemente anidados) en una cadena están cerrados, es decir, para cada (
necesita encontrar )
. Descubrirás que es imposible de resolver y si no puedes resolverlo con expresiones regulares, entonces no puedes averiguar si hay una palabra dentro de los paréntesis para todos los casos, ya que no puedes encontrar una posición en la cadena si todos los precedentes (
tienen un correspondiente )
.