Padrão Regex para corresponder, excluindo quando ... / Exceto entre

108

--Editar-- As respostas atuais têm algumas idéias úteis, mas eu quero algo mais completo que eu possa 100% entender e reutilizar; é por isso que estabeleci uma recompensa. Além disso, ideias que funcionam em qualquer lugar são melhores para mim do que sintaxe padrão como\K

Esta questão é sobre como posso combinar um padrão, exceto em algumas situações s1 s2 s3. Dou um exemplo específico para mostrar o que quero dizer, mas prefiro uma resposta geral que eu possa entender 100% para que possa reutilizá-la em outras situações.

Exemplo

Quero combinar cinco dígitos usando, \b\d{5}\bmas não em três situações s1 s2 s3:

s1: Não em uma linha que termina com um ponto final como esta frase.

s2: em nenhum lugar dentro dos parênteses.

s3: Não está dentro de um bloco que começa if(e termina com//endif

Eu sei como resolver qualquer um dos s1 s2 s3 com um lookahead e lookbehind, especialmente em C # lookbehind ou \Kem PHP.

Por exemplo

s1 (?m)(?!\d+.*?\.$)\d+

s3 com C # lookbehind (?<!if\(\D*(?=\d+.*?//endif))\b\d+\b

s3 com PHP \ K (?:(?:if\(.*?//endif)\D*)*\K\d+

Mas a mistura de condições faz minha cabeça explodir. Ainda mais uma má notícia é que posso precisar adicionar outras condições s4 s5 em outro momento.

A boa notícia é que não me importo se processo os arquivos usando as linguagens mais comuns, como PHP, C #, Python ou a máquina de lavar do meu vizinho. :) Sou basicamente um iniciante em Python e Java, mas estou interessado em saber se existe uma solução.

Por isso vim aqui ver se alguém pensa em uma receita flexível.

Dicas: você não precisa me fornecer o código completo. :)

Obrigado.

Hans Schindler
fonte
1
\Knão há sintaxe php especial. Elabore e esclareça o que você quer dizer. Se você pretende nos dizer que não precisa de uma solução "complicada", precisa dizer o que é complicado para você e por quê.
hakre
@hakre: Você quer dizer porque o ruby ​​agora está usando e começou no perl?
Hans Schindler
1
Não, porque é PCRE que não é PHP (nem Ruby). Perl é diferente, mas o PCRE pretende ser compatível com Perl Regex.
hakre
Seus requisitos s2 e s3 parecem ser contraditórios. s2 implica que os parênteses são sempre combinados e podem ser aninhados, mas s3 requer que o: "if("open parênteses seja fechado, não com a ")", mas sim com a "//endif":? E se para s3 você realmente quis dizer que a cláusula if deveria ser fechada com "//endif)":, então o requisito s3 é um subconjunto de s2.
Ridgerunner
@hakre Sim, eu conheço PCRE, mas para explicar, a questão é sobre linguagem de programação ... diz especially in C# lookbehind or \K in PHP... Mas C # lookbehind não apenas C # é .NET, então você pode reclamar também, eu digo C # não .NET :) E em resposta eu digo Ruby não Onigurama isso também é ruim ... Existe outra linguagem que usa PCRE? Sem falar em Notepad ++ ou ferramentas de servidor, esta é a questão sobre o uso de recurso na linguagem espero explicar e desculpe se parecer errado
Hans Schindler

Respostas:

205

Hans, vou morder a isca e detalhar minha resposta anterior. Você disse que queria "algo mais completo", então espero que não se importe com a resposta longa - apenas tentando agradar. Vamos começar com alguns antecedentes.

Em primeiro lugar, esta é uma excelente pergunta. Freqüentemente, há dúvidas sobre como combinar certos padrões, exceto em certos contextos (por exemplo, dentro de um bloco de código ou entre parênteses). Essas perguntas freqüentemente dão origem a soluções bastante estranhas. Portanto, sua pergunta sobre múltiplos contextos é um desafio especial.

Surpresa

Surpreendentemente, existe pelo menos uma solução eficiente que é geral, fácil de implementar e agradável de manter. Ele funciona com todos os sabores de regex que permitem que você inspecione grupos de captura em seu código. E acontece para responder a uma série de perguntas comuns que podem à primeira vista parecer diferentes das suas: "corresponder a tudo, exceto Donuts", "substituir tudo menos ...", "corresponder a todas as palavras, exceto as da lista negra da minha mãe", "ignorar tags "," correspondem à temperatura, a menos que estejam em itálico "...

Infelizmente, a técnica não é bem conhecida: estimo que em vinte perguntas do SO que poderiam usá-la, apenas uma tem uma resposta que a menciona - o que significa talvez uma em cinquenta ou sessenta respostas. Veja minha troca com Kobi nos comentários. A técnica é descrita com alguma profundidade neste artigo, que a chama (otimisticamente) de "melhor truque de regex de todos os tempos". Sem entrar em tantos detalhes, tentarei dar a você uma noção firme de como a técnica funciona. Para obter mais detalhes e exemplos de código em vários idiomas, encorajo você a consultar esse recurso.

Uma variação mais conhecida

Existe uma variação usando sintaxe específica para Perl e PHP que realiza o mesmo. Você o verá no SO nas mãos de mestres de regex, como CasimiretHippolyte e HamZa . Contarei mais sobre isso abaixo, mas meu foco aqui é a solução geral que funciona com todos os tipos de regex (contanto que você possa inspecionar grupos de captura em seu código).

Obrigado por todo o background, zx81 ... Mas qual é a receita?

Fato Chave

O método retorna a correspondência na captura do Grupo 1. Ele não se preocupa com o jogo geral.

Na verdade, o truque é combinar os vários contextos que não queremos (encadeando esses contextos usando o |OR / alternância) de modo a "neutralizá-los". Depois de combinar todos os contextos indesejados, a parte final da alternância coincide com o que nós não queremos e captura-o ao grupo 1.

A receita geral é

Not_this_context|Not_this_either|StayAway|(WhatYouWant)

Isso vai coincidir Not_this_context, mas de certa forma essa partida vai para uma lata de lixo, porque não vamos olhar para as correspondências gerais: olhamos apenas para as capturas do Grupo 1.

No seu caso, com seus dígitos e seus três contextos para ignorar, podemos fazer:

s1|s2|s3|(\b\d+\b)

Observe que, como realmente combinamos s1, s2 e s3 em vez de tentar evitá-los com lookarounds, as expressões individuais para s1, s2 e s3 podem permanecer claras como o dia. (São as subexpressões de cada lado de a |)

Toda a expressão pode ser escrita assim:

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

Veja esta demonstração (mas concentre-se nos grupos de captura no painel inferior direito).

Se você tentar dividir mentalmente esse regex em cada |delimitador, na verdade é apenas uma série de quatro expressões muito simples.

Para sabores que suportam espaçamento livre, essa leitura é particularmente boa.

(?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)

Isso é excepcionalmente fácil de ler e manter.

Estendendo o regex

Quando você deseja ignorar mais situações S4 e S5, você as adiciona em mais alternâncias à esquerda:

s4|s5|s1|s2|s3|(\b\d+\b)

Como é que isso funciona?

Os contextos que você não quer são adicionados a uma lista de alternâncias à esquerda: eles vão combinar, mas essas combinações gerais nunca são examinadas, então combiná-los é uma maneira de colocá-los em uma "lata de lixo".

O conteúdo que você deseja, no entanto, é capturado para o Grupo 1. Em seguida, você deve verificar programaticamente se o Grupo 1 está definido e não vazio. Esta é uma tarefa de programação trivial (e falaremos mais tarde sobre como ela é feita), especialmente considerando que ela deixa você com uma regex simples que você pode entender rapidamente e revisar ou estender conforme necessário.

Nem sempre sou um fã de visualizações, mas este faz um bom trabalho em mostrar como o método é simples. Cada "linha" corresponde a uma correspondência potencial, mas apenas a linha inferior é capturada no Grupo 1.

Visualização de expressão regular

Demonstração Debuggex

Variação Perl / PCRE

Em contraste com a solução geral acima, existe uma variação para Perl e PCRE que é freqüentemente vista no SO, pelo menos nas mãos de Deuses de regex como @CasimiretHippolyte e @HamZa. Isto é:

(?:s1|s2|s3)(*SKIP)(*F)|whatYouWant

No seu caso:

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

Esta variação é um pouco mais fácil de usar porque o conteúdo correspondente nos contextos s1, s2 e s3 é simplesmente ignorado, então você não precisa inspecionar as capturas do Grupo 1 (observe que os parênteses sumiram). As partidas contêm apenaswhatYouWant

Note-se que (*F), (*FAIL)e (?!)são todos a mesma coisa. Se você quiser ser mais obscuro, pode usar(*SKIP)(?!)

demo para esta versão

Formulários

Aqui estão alguns problemas comuns que essa técnica pode facilmente resolver. Você notará que a escolha de palavras pode fazer alguns desses problemas soarem diferentes, embora na verdade sejam virtualmente idênticos.

  1. Como posso combinar foo, exceto em qualquer lugar em uma tag como <a stuff...>...</a>?
  2. Como posso corresponder foo, exceto em uma <i>tag ou snippet de javascript (mais condições)?
  3. Como posso combinar todas as palavras que não estão nesta lista negra?
  4. Como posso ignorar qualquer coisa dentro de um bloco SUB ... END SUB?
  5. Como posso combinar tudo exceto ... s1 s2 s3?

Como programar as capturas do Grupo 1

Você não gostou do código, mas, para completar ... O código para inspecionar o Grupo 1 obviamente dependerá do idioma de sua escolha. De qualquer forma, ele não deve adicionar mais do que algumas linhas ao código que você usaria para inspecionar correspondências.

Em caso de dúvida, recomendo que você dê uma olhada na seção de exemplos de código do artigo mencionado anteriormente, que apresenta código para algumas linguagens.

Alternativas

Dependendo da complexidade da questão e do mecanismo regex usado, existem várias alternativas. Aqui estão os dois que podem se aplicar à maioria das situações, incluindo várias condições. Em minha opinião, nenhum dos dois é tão atraente quanto a s1|s2|s3|(whatYouWant)receita, até porque a clareza sempre vence.

1. Substitua e depois Combine.

Uma boa solução que parece hacky, mas funciona bem em muitos ambientes, é trabalhar em duas etapas. Uma primeira regex neutraliza o contexto que você deseja ignorar, substituindo strings potencialmente conflitantes. Se você deseja apenas corresponder, pode substituir por uma string vazia e, em seguida, executar a correspondência na segunda etapa. Se você quiser substituir, você pode primeiro substituir as strings a serem ignoradas por algo distinto, por exemplo, cercar seus dígitos com uma cadeia de largura fixa de @@@. Após essa substituição, você está livre para substituir o que realmente deseja e, em seguida, terá que reverter suas @@@cordas distintas .

2. Lookarounds.

Sua postagem original mostrou que você entende como excluir uma única condição usando soluções alternativas. Você disse que o C # é ótimo para isso e está certo, mas não é a única opção. Os tipos de regex do .NET encontrados em C #, VB.NET e Visual C ++, por exemplo, bem como o regexmódulo ainda experimental a ser substituído reem Python, são os únicos dois mecanismos que conheço que suportam lookbehind de largura infinita. Com essas ferramentas, uma condição em um olhar para trás pode cuidar não apenas de olhar para trás, mas também para o fósforo e além dele, evitando a necessidade de coordenar com um olhar para frente. Mais condições? Mais alternativas.

Reciclando a regex que você tinha para s3 em C #, todo o padrão ficaria assim.

(?!.*\.)(?<!\([^()]*(?=\d+[^)]*\)))(?<!if\(\D*(?=\d+.*?//endif))\b\d+\b

Mas agora você sabe que não estou recomendando isso, certo?

Exclusões

@HamZa e @Jerry sugeriram que eu mencionasse um truque adicional para casos em que você deseja apenas excluir WhatYouWant. Você se lembra que a receita para combinar WhatYouWant(capturá-lo no Grupo 1) era s1|s2|s3|(WhatYouWant), certo? Para excluir todas as instâncias de WhatYouWant, você altera o regex para

(s1|s2|s3)|WhatYouWant

Para a string de substituição, você usa $1. O que acontece aqui é que para cada instância s1|s2|s3que é correspondida, a substituição $1substitui essa instância por ela mesma (referenciada por $1). Por outro lado, quando WhatYouWanté correspondido, ele é substituído por um grupo vazio e nada mais - e, portanto, excluído. Veja esta demonstração , obrigado @HamZa e @Jerry por sugerirem esta adição maravilhosa.

Substituições

Isso nos leva a substituições, nas quais tocarei brevemente.

  1. Ao substituir por nada, consulte o truque "Exclusões" acima.
  2. Ao substituir, se estiver usando Perl ou PCRE, use a (*SKIP)(*F)variação mencionada acima para corresponder exatamente ao que você deseja e faça uma substituição direta.
  3. Em outros sabores, na chamada da função de substituição, inspecione a correspondência usando um retorno de chamada ou lambda e substitua se o Grupo 1 estiver definido. Se precisar de ajuda com isso, o artigo já referenciado fornecerá o código em várias linguagens.

Diverta-se!

Não, espere, tem mais!

Ah, nah, vou guardar isso para minhas memórias em vinte volumes, a serem lançados na próxima primavera.

zx81
fonte
2
@Kobi Resposta em duas partes. Sim, empolguei-me escrevendo ontem à noite e escrevi no final que dormiria sobre isso e arrumaria depois. :) Sim, o truque é simples, mas não compartilho da sua percepção de que é "básico" porque não parece fazer parte das ferramentas comuns que as pessoas usam para resolver problemas de exclusão. Quando pesquisei "exceto" ou "a menos" ou "não interno" no SO, apenas uma resposta (sem votos) sugeriu isso, nenhuma das outras o fez. Eu não tinha visto suas respostas, a propósito, que são fantásticas. :)
zx81
2
Desculpe, mas o "Melhor truque" de Rex simplesmente não funciona (de forma confiável ). Digamos que você queira fazer a correspondência Tarzan, mas não quando estiver entre aspas duplas. O: /no|no|(yes)/trick regex seria algo como: /"[^"]*"|Tarzan/(ignorando caracteres escapados). Isto irá funcionar para muitos casos, mas falha completamente quando aplicado ao seguinte texto JavaScript válido: var bug1 = 'One " quote here. Should match this Tarzan'; var bug2 = "Should not match this Tarzan";. O truque de Rex só funciona quando TODAS as estruturas possíveis são combinadas - em outras palavras - você precisa analisar totalmente o texto para garantir 100% de precisão.
ridgerunner
1
Desculpe se pareci rude - essa certamente não era minha intenção. Meu ponto (como em meu segundo comentário à questão original acima) é que uma solução correta depende muito do texto alvo que está sendo pesquisado. Meu exemplo tem o código-fonte JavaScript como texto de destino, que tem uma aspa dupla dentro de uma string entre aspas simples. Poderia facilmente ter sido um RegExp literal, como: var bug1 = /"[^"]*"|(Tarzan)/gi;e teve o mesmo efeito (e este segundo exemplo certamente não é um caso extremo). Há muitos outros exemplos que eu poderia citar onde essa técnica não funciona de maneira confiável.
ridgerunner
1
@ridgerunner Sempre gostei de ouvir de você, soa injustificadamente rude para mim. Quando sabemos que nossas strings podem conter "alertas falsos", todos ajustamos nossos padrões. Por exemplo, para corresponder a uma string que pode conter aspas de escape que podem desequilibrar um matcher de string, você pode usar (?<!\\)"(?:\\"|[^"\r\n])*+" Você não puxa as armas grandes a menos que tenha um motivo. O princípio da solução ainda é válido. Se não formos capazes de expressar um padrão para colocar no lado esquerdo, é uma história diferente, precisamos de uma solução diferente. Mas a solução faz o que anuncia.
zx81
1
Esta resposta foi adicionada às Perguntas frequentes sobre expressões regulares do Stack Overflow pelo usuário @funkwurm.
aliteralmind
11

Faça três combinações diferentes e lide com a combinação das três situações usando a lógica condicional no programa. Você não precisa lidar com tudo em um regex gigante.

EDITAR: deixe-me expandir um pouco porque a questão se tornou mais interessante :-)

A ideia geral que você está tentando capturar aqui é comparar com um determinado padrão de regex, mas não quando há certos outros (pode ser qualquer número) padrões presentes na string de teste. Felizmente, você pode tirar proveito de sua linguagem de programação: mantenha as regexes simples e use apenas uma condicional composta. Uma prática recomendada seria capturar essa ideia em um componente reutilizável, então vamos criar uma classe e um método para implementá-lo:

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

Portanto, acima, configuramos a string de pesquisa (os cinco dígitos), várias strings de exceção (seu s1 , s2 e s3 ) e, em seguida, tentamos comparar com várias strings de teste. Os resultados impressos devem ser mostrados nos comentários ao lado de cada string de teste.

Yawar
fonte
2
Quer dizer, talvez, combinar três regex em uma fileira? Regex 1 elimina a situação 1 (talvez apenas exclua o dígito incorreto), r2 remove s2, r3 remove s3 e corresponde aos dígitos restantes? É uma ideia interessante.
Hans Schindler
Ha, claro, é por isso que eu votei contra você. :) Não me interpretem mal, ainda acho que neste caso particular minha resposta é mais eficiente e sustentável. Você viu a versão em espaço livre que adicionei ontem? Isso é uma passagem e excepcionalmente fácil de ler e manter. Mas gosto do seu trabalho e da sua resposta ampliada. Desculpe, não posso votar positivamente novamente, caso contrário, eu faria. :)
zx81
2

Sua exigência de que não esteja entre parênteses é impossível de satisfazer em todos os casos. Ou seja, se você pode de alguma forma encontrar um (à esquerda e )à direita, nem sempre significa que você está dentro dos parênteses. Por exemplo.

(....) + 55555 + (.....)- não dentro dos parênteses ainda existem (e )à esquerda e à direita

Agora você pode se achar inteligente e olhar para (a esquerda apenas se não encontrar )antes e vice-versa para a direita. Isso não funcionará neste caso:

((.....) + 55555 + (.....))- dentro dos parênteses, embora haja fechamento )e (à esquerda e à direita.

É impossível descobrir se você está dentro dos parênteses usando regex, pois a regex não pode contar quantos parênteses foram abertos e quantos fechados.

Considere esta tarefa mais fácil: usando regex, descubra se todos os parênteses (possivelmente aninhados) em uma string estão fechados, ou seja, para cada que (você precisa encontrar ). Você vai descobrir que é impossível resolver e se você não puder resolver isso com regex, então você não pode descobrir se uma palavra está dentro de parênteses para todos os casos, já que você não pode descobrir em alguma posição na string se todos os precedentes (têm um correspondente ).

RokL
fonte
2
Ninguém disse nada sobre parênteses aninhados, e seu caso nº 1 foi tratado muito bem pela resposta de zx81.
Dan Bechard
Obrigado por bons pensamentos :) mas parênteses aninhados não me preocupam para esta questão, é mais sobre a ideia de situações ruins s1 s2 s3
Hans Schindler
Claro que não é impossível! É exatamente por isso que você precisa controlar o nível de parênteses em que está analisando atualmente.
MrWonderful
Bem, se você estiver analisando algum tipo de CFG como o OP parece estar fazendo, será melhor gerar um LALR ou analisador semelhante que não tenha problemas com isso.
RokL
2

Hans, se você não se importa, eu usei a máquina de lavar do seu vizinho chamada perl :)

Editado: Abaixo de um pseudocó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 o arquivo 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

E o 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";
    }
}

Execução:

tiago @ dell: ~ $ cat input.txt | perl validator.pl
deve corresponder a 12345
deve corresponder a 12345
deve corresponder a 12345
Tiago Lopo
fonte
2

Não tenho certeza se isso ajudaria você ou não, mas estou fornecendo uma solução considerando as seguintes suposições -

  1. Você precisa de uma solução elegante para verificar todas as condições
  2. As condições podem mudar no futuro e a qualquer momento.
  3. Uma condição não deve depender de outras.

No entanto, considerei também o seguinte -

  1. O arquivo fornecido contém erros mínimos. Se isso acontecer, meu código pode precisar de algumas modificações para lidar com isso.
  2. Usei Stack para controlar os if(blocos.

Ok, aqui está a solução -

Usei C # e com ele MEF (Microsoft Extensibility Framework) para implementar os analisadores configuráveis. A ideia é usar um único analisador para analisar e uma lista de classes validadoras configuráveis ​​para validar a linha e retornar verdadeiro ou falso com base na validação. Em seguida, você pode adicionar ou remover qualquer validador a qualquer momento ou adicionar novos, se desejar. Até agora eu já implementei para S1, S2 e S3 que você mencionou, verifique as classes no ponto 3. Você terá que adicionar classes para s4, s5 se precisar no futuro.

  1. Primeiro, crie as 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. Em seguida, vem o leitor e verificador de arquivos -

    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. Em seguida, vem a implementação de verificadores individuais, os nomes das classes são autoexplicativos, então não acho que eles precisem de mais descrições.

    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. O 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 o teste peguei o arquivo de amostra do @Tiago Test.txtque tinha as seguintes linhas -

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

Dá a saída -

it should match 12345
it should match 12345
it should match 12345

Não sei se isso te ajudaria ou não, eu me diverti brincando com isso .... :)

A melhor parte é que, para adicionar uma nova condição, tudo o que você precisa fazer é fornecer uma implementação de IPatternMatcher, ela será chamada automaticamente e, portanto, será validada.

codificador sem cérebro
fonte
2

O mesmo que @ zx81, (*SKIP)(*F)mas com o uso de uma afirmação antecipada negativa.

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

DEMO

Em python, eu faria facilmente assim,

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.

Resultado:

000
111
222
333
Avinash Raj
fonte