Регулярное выражение шаблон для соответствия, исключая когда... / кроме между


-- Edit-- текущие ответы есть некоторые полезные идеи, но я хочу что-то более полное, что я могу 100% понять и использовать; вот поэтому я и баунти. Также идеи, которые работают везде, лучше для меня, чем не стандартный синтаксис, такой как K

этот вопрос о том, как я могу соответствовать шаблону, за исключением некоторых ситуаций s1 s2 s3. Я даю конкретный пример, чтобы показать мой смысл, но предпочитаю общий ответ, который я могу понять на 100%, чтобы я мог использовать его в других положения.

пример

Я хочу, чтобы соответствовать пять цифр с помощью bd{5}b но не в трех ситуациях s1 s2 s3:

s1: не на строке, которая заканчивается точкой, как это предложение.

s2: не в любом месте внутри скобок.

s3: не внутрь блока, который начинается с if( и заканчивается //endif

Я знаю, как решить любой из s1 s2 s3 с a просмотра вперед и просмотра назад, особенно в C# 'назад' или K в PHP.

например

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

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

s3 с PHP K (?:(?:if(.*?//endif)D*)*Kd+

но сочетание условий вместе заставляет мою голову взорваться. Еще более плохая новость заключается в том, что мне, возможно, придется добавить другие условия s4 s5 в другое время.

хорошая новость заключается в том, что мне все равно, если я обрабатываю файлы, используя наиболее распространенные языки, такие как PHP, C#, Python или стиральная машина моего соседа. :) Я в значительной степени новичок в Python & Java, но мне интересно узнать, есть ли у него решение.

поэтому я пришел сюда, чтобы увидеть, если кто-то думает о гибком рецепте.

подсказки в порядке: вам не нужно давать мне полный код. :)

спасибо.

6 96

6 ответов:

Ганс, я возьму наживку и изложу свой предыдущий ответ. Вы сказали, что хотите "что-то более полное", поэтому я надеюсь, что вы не будете возражать против длинного ответа-просто пытаясь угодить. Давайте начнем с фона.

во-первых, это отличный вопрос. Часто возникают вопросы о соответствии определенным шаблонам, за исключением определенных контекстов (например, внутри блока кода или внутри круглых скобок). Эти вопросы часто приводят к довольно неудобным решениям. Так что ваш вопрос о несколько контекстов - это особый вызов.

сюрприз

Удивительно, но есть по крайней мере одно эффективное решение, которое является общим, легко реализовать и приятно поддерживать. Это работает со всеми вкусами регулярных выражений которые позволяют вам проверять группы захвата в вашем коде. И это случается, чтобы ответить на ряд общих вопросов, которые могут на первый взгляд звучать иначе, чем у вас: "матч все, кроме пончиков", " заменить все, кроме.....", "сопоставить все слова, кроме тех, что в черном списке моей мамы", "игнорировать теги", "сопоставить температуру, если не выделено курсивом"...

к сожалению, метод не очень хорошо известен: я оцениваю, что в двадцати SO вопросы, которые могли бы использовать его, только один имеет один ответ, который упоминает его-что означает, может быть, один из пятидесяти или шестидесяти ответов. Смотрите мой обмен с Коби в комментариях. Методика описана в некоторой глубине в в этой статье который называет его (оптимистично) "лучшее выражение трюк". Не вдаваясь в подробности, я постараюсь дать вам четкое представление о том, как работает эта техника. Для более подробной информации и примеров кода на различных языках, я рекомендую вам ознакомиться с этим ресурсом.

Более Известная Вариация

существует вариант, использующий синтаксис, специфичный для Perl и PHP, который выполняет то же самое. Вы увидите это на так в руках мастеров регулярных выражений, таких как CasimiretHippolyte и Хамза. Я расскажу вам об этом ниже, но мое внимание здесь сосредоточено на общем решении, которое работает со всеми вкусами регулярных выражений (пока вы можете проверять группы захвата в своем коде).

Спасибо за весь фон, zx81... Но каков же рецепт?

Ключевой Факт

метод возвращает совпадение в группе 1 захвата. Его это не волнует все об общем матче.

на самом деле, фокус в том, чтобы соответствовать различным контекстам, которые мы не хотим (связывание этих контекстов с помощью | или / чередование) чтобы "нейтрализовать". после сопоставления всех нежелательных контекстов, заключительная часть чередования соответствует тому, что мы сделать хочу и захватывает его в группу 1.

общий рецепт

Not_this_context|Not_this_either|StayAway|(WhatYouWant)

это будет соответствовать Not_this_context, но в каком-то смысле этот матч идет в мусорное ведро, потому что мы не будем смотреть в общих матчах: мы смотрим только на 1-ю группу.

в вашем случае, с вашими цифрами и тремя контекстами, которые нужно игнорировать, мы можем сделать:

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

обратите внимание, что поскольку мы фактически сопоставляем s1, s2 и s3 вместо того, чтобы пытаться избежать их с помощью lookarounds, отдельные выражения для s1, s2 и s3 могут оставаться ясными как день. (Они являются подвыражениями с каждой стороны a |)

все выражение может быть написано как это:

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

посмотреть этот demo (но сосредоточьтесь на группах захвата в нижней правой панели.)

если вы мысленно попытаетесь разделить это регулярное выражение на каждое | разделитель, это на самом деле только четыре очень простых выражений.

для ароматов, которые поддерживают свободное расстояние, это читается особенно хорошо.

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

это исключительно легко читать и поддерживать.

расширения регулярное выражение

когда вы хотите игнорировать больше ситуаций s4 и s5, вы добавляете их в большее количество чередований слева:

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

как это работает?

контексты, которые вы не хотите, добавляются в список чередований слева: они будут совпадать, но эти общие совпадения никогда не рассматриваются, поэтому их сопоставление-это способ поместить их в "мусорное ведро".

содержимое, которое вы хотите, однако, захватывается в группу 1. Вы затем нужно проверить программно, что Группа 1 установлена и не пуста. Это тривиальная задача программирования (и мы позже поговорим о том, как это делается), особенно учитывая, что она оставляет вам простое регулярное выражение, которое вы можете понять с первого взгляда и пересмотреть или расширить по мере необходимости.

я не всегда поклонник визуализаций, но этот хорошо показывает, насколько прост метод. Каждая "линия" соответствует потенциальному совпадению, но только нижняя строка захватывается в Группа 1.

Debuggex Demo

Perl/PCRE вариация

в отличие от общего решения выше, существует вариация для Perl и PCRE, которая часто наблюдается на SO, по крайней мере, в руках богов регулярных выражений, таких как @CasimiretHippolyte и @HamZa. Это:

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

в вашем случае:

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

этот вариант немного проще в использовании, потому что содержимое соответствует контексты s1, s2 и s3 просто пропускаются, поэтому вам не нужно проверять захваты группы 1 (Обратите внимание, что скобки исчезли). Матчи содержат только whatYouWant

отметим, что (*F),(*FAIL) и (?!) это все одно и то же. Если вы хотите быть более неясным, вы можете использовать (*SKIP)(?!)

demo для этой версии

приложения

вот некоторые общие проблемы, которые эта техника часто может легко решать. Вы заметите, что выбор слова может сделать эти проблемы звучат по-разному, а на самом деле они практически идентичны.

  1. как я могу соответствовать foo, кроме как в любом месте тега, как <a stuff...>...</a>?
  2. как я могу соответствовать foo, кроме как в <i> тег или фрагмент javascript (дополнительные условия)?
  3. как я могу сопоставить все слова, которые не находятся в этом черном списке?
  4. как я могу игнорировать что-либо внутри суб... КОНЕЦ ПОДПРОГРАММЫ блок?
  5. как я могу соответствовать все, кроме... s1 s2 s3?

как запрограммировать группу 1 захватывает

вы не для кода, но для завершения... Код для проверки группы 1, очевидно, будет зависеть от вашего языка выбора. Во всяком случае, он не должен добавлять больше нескольких строк в код, который вы будете использовать для проверки совпадений.

если вы сомневаетесь, я рекомендую вам посмотреть на примеры кода раздел из статьи, упомянутой ранее, в которой представлен код для довольно многих языков.

варианты

в зависимости от сложности вопроса и от используемого механизма регулярных выражений существует несколько альтернатив. Вот два, которые могут применяться к большинству ситуаций, включая несколько условий. На мой взгляд, ни один из них не так привлекателен, как s1|s2|s3|(whatYouWant) рецепт, хотя бы потому, что ясность всегда побеждает.

1. Замените тогда матч.

хорошее решение, которое звучит хаки, но хорошо работает во многих средах, чтобы работать в два этапа. Первое регулярное выражение нейтрализует контекст, который вы хотите игнорировать, заменяя потенциально конфликтующие строки. Если вы хотите только соответствовать, то вы можете заменить пустой строкой, а затем запустить матч на втором шаге. Если вы хотите заменить, Вы можете сначала заменить строки, которые будут игнорироваться, чем-то отличительным, например, окружающим ваши цифры цепочка фиксированной ширины @@@. После этой замены вы можете заменить то, что вы действительно хотели, тогда вам придется вернуть свой отличительный @@@ строки.

2. Lookarounds.

ваш оригинальный пост показал, что вы понимаете, как исключить одно условие с помощью lookarounds. Вы сказали, что c# отлично подходит для этого, и вы правы, но это не единственный вариант. Вкусы регулярных выражений .NET, найденные в C#, VB.NET и Visual C++ для пример, как и до сих пор-экспериментальный regex модуль, чтобы заменить re в Python есть только два двигателя, которые я знаю, которые поддерживают бесконечную ширину lookbehind. С помощью этих инструментов одно условие в одном lookbehind может позаботиться о том, чтобы смотреть не только сзади, но и на матч и за его пределами, избегая необходимости координировать свои действия с lookahead. Еще условия? Еще больше ищеек.

переработка регулярного выражения, которое у вас было для s3 в C#, весь шаблон будет выглядеть так этот.

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

но теперь вы знаете, я не рекомендую это, верно?

удаления

@HamZa и @Jerry предложили мне упомянуть дополнительный трюк для случаев, когда вы пытаетесь просто удалить WhatYouWant. Вы помните, что рецепт, чтобы соответствовать WhatYouWant (захват его в группу 1) был s1|s2|s3|(WhatYouWant), да? Чтобы удалить все экземпляры WhatYouWant, вы меняете регулярное выражение на

(s1|s2|s3)|WhatYouWant

для замены строки, вы используете . Здесь происходит то, что для каждого экземпляра s1|s2|s3 это соответствует, замена заменяет этот экземпляр с собой (на который ссылается ). С другой стороны, когда WhatYouWant сопоставляется, он заменяется пустой группой и ничем другим-и поэтому удаляется. Смотрите это demo, спасибо @HamZa и @Jerry за предложение этого замечательного дополнения.

замены

это приводит нас к заменам, о котором я коснусь вкратце.

  1. при замене ничем, см. трюк "удаления" выше.
  2. при замене, если используется Perl или PCRE, используйте (*SKIP)(*F) вариация, упомянутая выше, чтобы соответствовать именно тому, что вы хотите, и сделать прямую замену.
  3. в других вариантах в вызове функции замены проверьте соответствие с помощью обратного вызова или лямбды и замените, если установлена группа 1. Если вам нужна помощь с этим, то статья уже ссылка даст вам код на разных языках.

удачи!

нет, подождите, это еще не все!

сделайте три разных совпадения и обработайте комбинацию из трех ситуаций, используя условную логику в программе. Вам не нужно обрабатывать все в одном гигантском регулярном выражении.

EDIT: позвольте мне немного расширить, потому что вопрос просто стал более интересным : -)

общая идея, которую вы пытаетесь захватить здесь, состоит в том, чтобы соответствовать определенному шаблону регулярного выражения, но не тогда, когда в тестовой строке присутствуют определенные другие (могут быть любые числа) шаблоны. К счастью, вы можете воспользоваться вашим языком программирования: держите регулярные выражения простыми и просто используйте составное условие. Лучше всего было бы захватить эту идею в многоразовом компоненте, поэтому давайте создадим класс и метод, который его реализует:

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

Итак, выше мы настроили строку поиска (пять цифр), несколько строк исключений (ваш s1, s2 и s3), а затем попробуйте сопоставить несколько тестовых строк. Результат печати должно быть, как показано в комментариях рядом с каждой строкой теста.

ваше требование, что это не внутри parens в Невозможно насытить для всех случаев. А именно, если вы можете как-то найти ( слева и ) справа, это не всегда означает, что вы находитесь внутри скобок. Например.

(....) + 55555 + (.....) - не внутри parens еще есть ( и ) и

теперь вы можете считать себя умным и искать ( влево, только если вы не сталкиваетесь ) перед и наоборот справа. Этот не будет работать в этом случае:

((.....) + 55555 + (.....)) - внутри скобок, даже если есть закрывающий тег ) и ( влево и вправо.

невозможно узнать, находитесь ли вы внутри parens с помощью regex, так как regex не может подсчитать, сколько parens было открыто и сколько закрыто.

рассмотрим эту более простую задачу: используя регулярное выражение, выясните, закрыты ли все (возможно, вложенные) парены в строке, то есть для каждого ( вам нужно найти ). Вы найдете из того, что это невозможно решить, и если вы не можете решить это с помощью регулярного выражения, то вы не можете выяснить, находится ли слово внутри parens для всех случаев, так как вы не можете выяснить в какой-то позиции в строке, если все предшествующие ( иметь соответствующую ).

Ганс, если вы не возражаете, я использовал стиральную машину вашего соседа под названием perl:)

редактировать: Ниже псевдо код:

  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

дан входной файл.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

и сценарий 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";
    }
}

исполнение:

tiago@dell:~$ cat input.txt | perl validator.pl 
it should match 12345
it should match 12345
it should match 12345

не уверен, что это поможет вам или нет, но я предоставляю решение с учетом следующих предположений -

  1. вам нужно элегантное решение, чтобы проверить все условия
  2. условия могут измениться в будущем и в любое время.
  3. одно условие не должно зависеть от других.

однако я учел и следующее -

  1. данный файл имеет минимальные ошибки в нем. Если это крольчиха, то мой код может потребоваться некоторые изменения, чтобы справиться с этим.
  2. я использовал стек, чтобы отслеживать if( блоки.

ок, вот решение -

я использовал C# и с ним MEF (Microsoft Extensibility Framework) для реализации настраиваемых парсеров. Идея заключается в том, чтобы использовать один синтаксический анализатор для анализа и список настраиваемых классов валидаторов для проверки строки и возврата true или false на основе проверки. Затем вы можете добавить или удалить любой валидатор в любое время или добавьте новые, если хотите. До сих пор я уже реализовал для S1, S2 и S3 вы упомянули, проверьте классы в точке 3. Вы должны добавить классы для s4, s5, если вам нужно в будущем.

  1. во - первых, создайте интерфейсы -

    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. затем идет средство чтения файлов и проверки -

    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. затем идет реализация отдельных шашек, имена классов не требуют пояснений, поэтому я не думаю, что они нужно больше описаний.

    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. программы

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

для тестирования я взял образец файла @Tiago как Test.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

дает на выходе -

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

не знаю, поможет ли это вам или нет, я весело провел время, играя с ним.... :)

лучшая часть заключается в том, что для добавления нового состояния все, что вам нужно сделать, это предоставить реализация IPatternMatcher, он будет автоматически вызываться и, таким образом, будет проверять.

то же, что и @zx81 (*SKIP)(*F) но с использованием отрицательного утверждения lookahead.

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

демо

в python, я бы сделал легко, как это,

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.

выход:

000
111
222
333