Patrón de expresiones regulares para que coincida, excepto cuando ... / Excepto entre

108

--Editar-- Las respuestas actuales tienen algunas ideas útiles, pero quiero algo más completo que pueda comprender y reutilizar al 100%; por eso puse una recompensa. Además, las ideas que funcionan en todas partes son mejores para mí que la sintaxis estándar como\K

Esta pregunta trata sobre 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 entender al 100% para poder reutilizarla en otras situaciones.

Ejemplo

Quiero hacer coincidir cinco dígitos usando \b\d{5}\bpero no en tres situaciones s1 s2 s3:

s1: No en una línea que termine con un punto como esta oración.

s2: No en ningún lugar dentro de los parens.

s3: No dentro de un bloque que comienza if(y termina con//endif

Sé cómo resolver cualquiera de s1 s2 s3 con un lookahead y lookbehind, especialmente en C # lookbehind o \Ken 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 mezcla de condiciones juntas hace que mi cabeza explote. Aún más malas noticias es que es posible que deba 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 prácticamente un principiante en Python y Java, pero estoy interesado en 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 des el código completo. :)

Gracias.

Hans Schindler
fuente
1
\Kno es una sintaxis php especial. Por favor, explique y aclare lo que quiere decir. Si su objetivo es decirnos que no necesita una solución "complicada", tiene que decir qué es complicado para usted y por qué.
hakre
@hakre ¿Te refieres a que ruby ​​ahora lo usa y comenzó en perl?
Hans Schindler
1
No, porque es PCRE que no es PHP (ni Ruby). Perl es diferente, sin embargo, PCRE pretende ser compatible con Perl Regex.
Hakre
Sus requisitos de s2 y s3 ​​parecen ser contradictorios. s2 implica que los paréntesis siempre coinciden y pueden estar anidados, pero s3 requiere que el "if("par : open sea cerrado, no con a ")", sino con a "//endif":? Y si para s3 realmente quiso decir que la cláusula if debería cerrarse con:, "//endif)"entonces el requisito de s3 es un subconjunto de s2.
ridgerunner
@hakre Sí, conozco PCRE, pero para explicarlo, la pregunta es sobre el lenguaje de programación ... dice especially in C# lookbehind or \K in PHP... Pero C # mira hacia atrás no solo C # es .NET, así que tú también puedes quejarte. Yo digo C # no .NET :) Y en respuesta digo Ruby, no Onigurama, eso también es malo ... ¿Hay otro idioma que use PCRE? No hablamos de Notepad ++ o herramientas de servidor esta es la pregunta acerca del uso de función en el lenguaje espero que la explican y lo siento si se ve mal
Hans Schindler

Respuestas:

205

Hans, morderé el anzuelo y desarrollaré mi respuesta anterior. Dijiste que querías "algo más completo", así que espero que no te importe la respuesta larga, solo intento complacer. 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 entre paréntesis). Estas preguntas a menudo dan lugar a soluciones bastante incómodas. Entonces, su pregunta sobre múltiples contextos es un desafío especial.

Sorpresa

Sorprendentemente, existe al menos una solución eficiente que es general, fácil de implementar y un placer de mantener. Se trabaja con todos los sabores de expresiones regulares que le permiten inspeccionar los grupos de captura en su código. Y resulta que responde a una serie de preguntas comunes que al principio pueden sonar diferentes a las suyas: "hacer coincidir todo excepto Donuts", "reemplazar todas menos ...", "hacer coincidir todas las palabras excepto las de la lista negra de mi madre", "ignorar etiquetas "," coinciden con la temperatura a menos que estén en cursiva "...

Lamentablemente, la técnica no es muy conocida: estimo que en veinte preguntas de SO que podrían usarla, solo una tiene una respuesta que la menciona, lo que significa tal vez una de cada cincuenta o sesenta respuestas. Vea mi intercambio con Kobi en los comentarios. La técnica se describe con cierta profundidad en este artículo que la llama (con optimismo) el "mejor truco de expresiones regulares". Sin entrar en muchos detalles, intentaré darles una idea clara de cómo funciona la técnica. Para obtener más detalles y ejemplos de código en varios idiomas, le animo a que consulte ese recurso.

Una variación más conocida

Existe una variación que usa la sintaxis específica de Perl y PHP que logra lo mismo. Lo verá en SO en manos de maestros de expresiones regulares como CasimiretHippolyte y HamZa . Le contaré más sobre esto a continuación, pero mi enfoque aquí está en la solución general que funciona con todos los tipos de expresiones regulares (siempre que pueda inspeccionar los grupos de captura en su código).

Gracias por todos los antecedentes, 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 el partido en general.

De hecho, el truco consiste en hacer coincidir los diversos contextos que no queremos (encadenar estos contextos usando la |OR / alternancia) para "neutralizarlos". Después de comparar todos los contextos no deseados, la parte final de la alternancia coincide con lo que queremos y lo captura al grupo 1.

La receta general es

Not_this_context|Not_this_either|StayAway|(WhatYouWant)

Esto coincidirá Not_this_context, pero en cierto sentido esa coincidencia va a la basura, porque no miraremos las coincidencias generales: solo miramos 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 debido a que en realidad hacemos coincidir s1, s2 y s3 ​​en lugar de tratar de evitarlos con cambios de opinión, las expresiones individuales para s1, s2 y s3 ​​pueden permanecer claras como el día. (Son las subexpresiones a cada lado de a |)

La expresión completa se puede escribir así:

(?m)^.*\.$|\([^\)]*\)|if\(.*?//endif|(\b\d+\b)

Vea esta demostración (pero concéntrese en los grupos de captura en el panel inferior derecho).

Si mentalmente intenta dividir esta expresión regular en cada |delimitador, en realidad es solo una serie de cuatro expresiones muy simples.

Para los sabores que admiten el espacio 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 alternancias 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 a la izquierda: coincidirán, pero estas coincidencias generales nunca se examinan, por lo que emparejarlas es una forma de ponerlas en un "contenedor de basura".

Sin embargo, el contenido que desea se captura en el Grupo 1. Luego, debe verificar mediante programación que el Grupo 1 esté configurado y no vacío. Esta es una tarea de programación trivial (y luego hablaremos sobre cómo se hace), especialmente considerando que te deja con una expresión regular simple que puedes entender de un vistazo y revisar o ampliar según sea necesario.

No siempre soy un fanático de las visualizaciones, pero esta hace un buen trabajo al mostrar lo simple que es el método. Cada "línea" corresponde a una coincidencia potencial, pero solo la línea de fondo se captura en el Grupo 1.

Visualización de expresiones regulares

Demostración de Debuggex

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 que coincide 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 contienenwhatYouWant

Tenga en cuenta que (*F), (*FAIL)y (?!)son la misma cosa. Si quisieras ser más oscuro, podrías usar(*SKIP)(?!)

demo para esta versión

Aplicaciones

A continuación, se muestran algunos problemas comunes que esta técnica a menudo puede resolver fácilmente. Notará que la elección de palabras puede hacer que algunos de estos problemas suenen diferentes, mientras que en realidad son prácticamente idénticos.

  1. ¿Cómo puedo hacer coincidir foo excepto en cualquier lugar de una etiqueta como <a stuff...>...</a>?
  2. ¿Cómo puedo hacer coincidir foo excepto en una <i>etiqueta o un fragmento de JavaScript (más condiciones)?
  3. ¿Cómo puedo hacer coincidir todas las palabras que no están en esta lista negra?
  4. ¿Cómo puedo ignorar algo dentro de un bloque SUB ... END SUB?
  5. ¿Cómo puedo hacer coincidir todo excepto ... s1 s2 s3?

Cómo programar las capturas del grupo 1

No lo hizo en cuanto al código, pero, para completarlo ... El código para inspeccionar el Grupo 1 obviamente dependerá del idioma que elija. 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.

En caso de duda, le recomiendo que consulte la sección de ejemplos de código del artículo mencionado anteriormente, que presenta 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 pueden aplicarse a la mayoría de situaciones, incluidas múltiples condiciones. En mi opinión, ninguno es tan atractivo como la s1|s2|s3|(whatYouWant)receta, aunque solo sea porque la claridad siempre gana.

1. Reemplace y luego haga coincidir.

Una buena solución que suena hacky pero 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 reemplazar con una cadena vacía y luego ejecutar su coincidencia en el segundo paso. Si desea reemplazar, primero puede reemplazar las cadenas que se ignorarán con algo distintivo, por ejemplo, rodeando sus dígitos con una cadena de ancho fijo de @@@. Después de este reemplazo, es libre de reemplazar lo que realmente deseaba, luego tendrá que revertir sus @@@cadenas distintivas .

2. Lookarounds.

Su publicación original demostró que comprende cómo excluir una sola condición utilizando métodos alternativos. Dijiste que C # es genial para esto, y tienes razón, pero no es la única opción. Los tipos de expresiones regulares de .NET que se encuentran en C #, VB.NET y Visual C ++, por ejemplo, así como el regexmódulo aún experimental para reemplazar reen Python, son los únicos dos motores que conozco que admiten la búsqueda hacia atrás de ancho infinito. Con estas herramientas, una condición en una mirada hacia atrás puede encargarse de mirar no solo hacia atrás, sino también al partido y más allá del partido, evitando la necesidad de coordinar con un mirar hacia adelante. ¿Más condiciones? Más revisiones.

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 a estas alturas ya sabes que no estoy recomendando esto, ¿verdad?

Eliminaciones

@HamZa y @Jerry han sugerido que mencione un truco adicional para los casos en los que solo busca eliminar WhatYouWant. ¿Recuerdas que la receta para combinar WhatYouWant(capturarla 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 s1|s2|s3que coincide, el reemplazo $1reemplaza esa instancia por sí misma (referenciada por $1). Por otro lado, cuando WhatYouWantcoincide, se reemplaza por un grupo vacío y nada más y, por lo tanto, se elimina. Vea esta demostración , gracias @HamZa y @Jerry por sugerir esta maravillosa adición.

Reemplazos

Esto nos lleva a los reemplazos, de los que hablaré brevemente.

  1. Cuando reemplace con nada, vea el truco "Eliminaciones" arriba.
  2. Al reemplazar, si usa Perl o PCRE, use la (*SKIP)(*F)variación mencionada anteriormente para que coincida exactamente con lo que desea y realice un reemplazo directo.
  3. En otros sabores, dentro de la llamada a la función de reemplazo, inspeccione la coincidencia usando una devolución de llamada o lambda, y reemplace si el Grupo 1 está configurado. Si necesita ayuda con esto, el artículo ya mencionado le proporcionará código en varios idiomas.

¡Que te diviertas!

¡No, espera, hay más!

Ah, no, lo guardaré para mis memorias en veinte volúmenes, que se publicarán la próxima primavera.

zx81
fuente
2
@Kobi Respuesta en dos partes. Sí, me dejé llevar escribiendo anoche y escribí en la parte de abajo que dormiría y lo ordenaría más tarde. :) Sí, el truco es simple, pero no comparto su percepción de que es "básico" porque no parece ser parte de las herramientas comunes que la gente usa para resolver problemas de exclusión. Cuando busqué en Google los problemas "excepto" o "a menos que" o "no dentro" en SO, solo una respuesta (sin votos) lo sugirió, ninguna de las otras lo hizo. Por cierto, no había visto tus respuestas, que son estupendas. :)
zx81
2
Lo siento, pero el "Mejor truco" de Rex simplemente no funciona (de manera confiable ). Diga que quiere hacer coincidir Tarzan, pero no cuando esté entre comillas dobles. La /no|no|(yes)/expresión regular : truco sería algo como: /"[^"]*"|Tarzan/(ignorando los caracteres escapados). Esto funcionará para muchos casos, pero falla por completo cuando se aplica a la siguiente texto JavaScript válida: var bug1 = 'One " quote here. Should match this Tarzan'; var bug2 = "Should not match this Tarzan";. El truco de Rex solo funciona cuando TODAS las estructuras posibles coinciden; en otras palabras, es necesario analizar completamente el texto para garantizar el 100% de precisión.
ridgerunner
1
Perdón si sonaba duro, ciertamente esa no era mi intención. Mi punto (como en mi segundo comentario a la pregunta original anterior) es que una solución correcta depende en gran medida del texto de destino que se busca. Mi ejemplo tiene el código fuente de JavaScript como texto de destino que tiene una comilla doble dentro de una sola cadena entre comillas. Fácilmente podría haber sido una expresión regular literal como: var bug1 = /"[^"]*"|(Tarzan)/gi;y tuvo el mismo efecto (y este segundo ejemplo ciertamente no es un caso límite). Hay muchos más ejemplos que podría citar en los que esta técnica no funciona de manera confiable.
ridgerunner
1
@ridgerunner Siempre disfruto escuchar de ti, simplemente me suena injustificadamente duro. Cuando sabemos que nuestras cadenas pueden contener "falsas alertas", todos ajustamos nuestros patrones. Por ejemplo, para hacer coincidir una cadena que puede contener comillas de escape que pueden provocar un emparejamiento de cadenas, puede usar (?<!\\)"(?:\\"|[^"\r\n])*+" No tira de las armas grandes a menos que tenga una razón. El principio de la solución sigue siendo válido. Si no podemos expresar un patrón para poner en el lado izquierdo, esa es una historia diferente, necesitamos una solución diferente. Pero la solución hace lo que anuncia.
zx81
1
El usuario @funkwurm agregó esta respuesta a las Preguntas frecuentes sobre expresiones regulares de desbordamiento de pila .
aliteralmind
11

Haga tres coincidencias diferentes y maneje la combinación de las tres situaciones utilizando lógica condicional en el programa. No necesita manejar todo en una expresión regular gigante.

EDITAR: permítanme expandirme un poco porque la pregunta se volvió más interesante :-)

La idea general que está tratando de capturar aquí es hacer coincidir con un cierto patrón de expresiones regulares, pero no cuando hay ciertos otros patrones (podría haber 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 condicional compuesto. Una mejor práctica sería capturar esta idea en un componente reutilizable, así que creemos una clase y un método que lo implemente:

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));
    }
  }
}

Entonces, arriba, configuramos la cadena de búsqueda (los cinco dígitos), múltiples cadenas de excepción (su s1 , s2 y s3 ), y luego intentamos hacer coincidir varias cadenas de prueba. Los resultados impresos deben ser los que se muestran en los comentarios junto a cada cadena de prueba.

Yawar
fuente
2
¿Te refieres a que tal vez te gusten tres expresiones regulares seguidas? ¿Regex 1 elimina la situación 1 (tal vez solo elimine el dígito incorrecto), r2 elimine s2, r3 elimine s3 y coincida con los dígitos restantes? Esa es una idea interesante.
Hans Schindler
Ja, claro, por eso te voté a favor. :) No me malinterpretes, sigo pensando que en este caso particular mi respuesta es más eficiente y fácil de mantener. ¿Has visto la versión de espacio libre que agregué ayer? Eso es de una pasada y excepcionalmente fácil de leer y mantener. Pero me gusta tu trabajo y tu respuesta ampliada. Lo siento, no puedo volver a votar, de lo contrario lo haría. :)
zx81
2

Su requisito de que no esté dentro de los parens es imposible de satisfacer en todos los casos. Es decir, 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 los parientes todavía 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 + (.....))- Parens interiores aunque hay cierres )ya (izquierda y derecha.

Es imposible saber si está dentro de los parens usando regex, ya que regex no puede contar cuántos parens se han abierto y cuántos cerrados.

Considere esta tarea más fácil: usando expresiones regulares, averigüe si todos los parens (posiblemente anidados) en una cadena están cerrados, es decir, para todos los (que necesita encontrar ). Descubrirá que es imposible de resolver y si no puede resolverlo con expresiones regulares, entonces no puede averiguar si una palabra está dentro de los paréntesis en todos los casos, ya que no puede averiguar en alguna posición en la cadena si todos los anteriores (tienen un correspondiente ).

RokL
fuente
2
Nadie dijo nada sobre los paréntesis anidados, y la respuesta de zx81 maneja bien su caso n. ° 1.
Dan Bechard
Gracias por los buenos pensamientos :) pero los paréntesis anidados no me preocupan por esta pregunta, se trata más de la idea de situaciones malas t1 s2 s3
Hans Schindler
¡Por supuesto que no es imposible! Esta es exactamente la razón por la que necesitaría realizar un seguimiento del nivel de parens en el que está analizando actualmente.
MrWonderful
Bueno, si está analizando algún tipo de CFG como OP parece estar haciendo, es mejor que genere un LALR o un analizador similar que no tenga problemas con esto.
RokL
2

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
debería coincidir con 12345
debería coincidir con 12345
debería coincidir con 12345
Tiago Lopo
fuente
2

No estoy seguro de si esto lo ayudaría o no, pero estoy brindando una solución considerando las siguientes suposiciones:

  1. Necesitas una solución elegante para comprobar todas las condiciones.
  2. Las condiciones pueden cambiar en el futuro y en cualquier momento.
  3. Una condición no debería depender de otras.

Sin embargo, consideré también lo siguiente:

  1. El archivo proporcionado tiene errores mínimos. Si es así, entonces mi código podría necesitar algunas modificaciones para hacer frente a eso.
  2. Usé Stack para realizar un seguimiento de los if(bloques.

Ok, aquí está la solución:

Usé C # y con él MEF (Microsoft Extensibility Framework) para implementar los analizadores configurables. La idea es usar un solo analizador para analizar y una lista de clases de validación configurables para validar la línea y devolver verdadero o falso según 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 mencionaste, verifica las clases en el punto 3. Tienes que agregar clases para s4, s5 si lo necesitas en el futuro.

  1. Primero, cree 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);
        }
    }
    
  2. Luego viene el lector y verificador de archivos:

    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();
            }
        }
    }
    
  3. Luego viene la implementación de las fichas individuales, los nombres de las clases se explican por sí mismos, por lo 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);
            }
        }
    }
    
  4. 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 la prueba, tomé el archivo de muestra de @ Tiago, Test.txtque 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 él .... :)

La mejor parte de esto 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, por lo tanto, se validará.

codificador sin cerebro
fuente
2

Igual que @ zx81 (*SKIP)(*F)pero con el uso de una aserción de anticipación negativa.

(?m)(?:if\(.*?\/\/endif|\([^()]*\))(*SKIP)(*F)|\b\d+\b(?!.*\.$)

MANIFESTACIÓN

En Python, lo haría fácilmente así,

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
Avinash Raj
fuente