Шаблон регулярного выражения для сопоставления, исключая когда... / За исключением
- Редактировать-- В текущих ответах есть некоторые полезные идеи, но я хочу что-то более полное, что я могу на 100% понять и использовать повторно; Вот почему я назначил награду. Также идеи, которые работают везде, лучше для меня, чем не стандартный синтаксис, такой как \K
Этот вопрос о том, как я могу сопоставить шаблон, за исключением некоторых ситуаций s1 s2 s3. Я привожу конкретный пример, чтобы показать свое значение, но предпочитаю общий ответ, который я могу понять на 100%, чтобы использовать его в других ситуациях.
пример
Я хочу сопоставить пять цифр, используя \b\d{5}\b
но не в трех ситуациях s1 s2 s3:
s1: не в строке, заканчивающейся точкой, как в этом предложении.
s2: нигде внутри паренов.
s3: не внутри блока, который начинается с if(
и заканчивается //endif
Я знаю, как решить любую из s1, s2, s3 с предвкушением и взглядом назад, особенно в C# взглядом назад или \K
в PHP.
Например
s1 (?m)(?!\d+.*?\.$)\d+
S3 с C# смотреть назад (?<!if\(\D*(?=\d+.*?//endif))\b\d+\b
s3 с PHP \K (?:(?:if\(.*?//endif)\D*)*\K\d+
Но сочетание условий заставляет мою голову взорваться. Еще более плохая новость заключается в том, что мне может понадобиться добавить другие условия s4 s5 в другое время.
Хорошая новость в том, что мне все равно, обрабатывать ли файлы с использованием наиболее распространенных языков, таких как PHP, C#, Python или стиральная машина моего соседа.:) Я довольно новичок в Python & Java, но мне интересно узнать, есть ли у него решение.
Так что я пришел сюда, чтобы посмотреть, если кто-то думает о гибком рецепте.
Подсказки в порядке: вам не нужно давать мне полный код.:)
Спасибо.
6 ответов
Ганс, я возьму наживку и уточню свой предыдущий ответ. Вы сказали, что хотите "что-то более полное", поэтому я надеюсь, что вы не будете возражать против длинного ответа - просто пытаться угодить. Давайте начнем с некоторого фона.
Во-первых, это отличный вопрос. Часто возникают вопросы о сопоставлении определенных шаблонов, за исключением определенных контекстов (например, внутри блока кода или внутри скобок). Эти вопросы часто приводят к довольно неловким решениям. Так что ваш вопрос о множественных контекстах - это особый вызов.
сюрприз
Удивительно, но есть по крайней мере одно эффективное решение, общее, простое в реализации и приятное в обслуживании. Он работает со всеми разновидностями регулярных выражений, которые позволяют вам проверять группы захвата в вашем коде. И это случается, чтобы ответить на ряд общих вопросов, которые на первый взгляд могут звучать иначе, чем у вас: "соответствовать всем, кроме пончиков", "заменить все, кроме...", "соответствовать всем словам, кроме тех, которые находятся в черном списке моей мамы", "игнорировать теги "," соответствуют температуре, если они не выделены курсивом "...
К сожалению, методика не очень хорошо известна: по моим оценкам, в двадцати SO-вопросах, которые могли бы ее использовать, только один имеет один ответ, в котором упоминается об этом, а это, возможно, один из пятидесяти или шестидесяти ответов. Смотрите мой обмен с Коби в комментариях. Техника подробно описана в этой статье, которая (с оптимизмом) называет ее "лучшим из когда-либо описанных регулярных выражений". Не вдаваясь в подробности, я попытаюсь дать вам четкое представление о том, как работает техника. Для более подробной информации и примеров кода на разных языках я советую вам обратиться к этому ресурсу.
Лучше известное изменение
Существует вариант с использованием синтаксиса, специфичного для Perl и PHP, который выполняет то же самое. Вы увидите это на SO в руках мастеров регулярных выражений, таких как HamZa и HamZa. Я расскажу вам больше об этом ниже, но я сосредоточусь здесь на общем решении, которое работает со всеми разновидностями регулярных выражений (при условии, что вы можете проверять группы захвата в своем коде).
Спасибо за весь фон, zx81... Но какой рецепт?
Ключевой факт
Метод возвращает совпадение в захвате группы 1. Это вообще не волнует общий матч.
Фактически, хитрость заключается в том, чтобы соответствовать различным контекстам, которые мы не хотим (объединяя эти контексты, используя |
ИЛИ / чередование), чтобы "нейтрализовать их". После сопоставления всех нежелательных контекстов последняя часть чередования совпадает с тем, что мы действительно хотим, и записывает его в группу 1.
Общий рецепт
Not_this_context|Not_this_either|StayAway|(WhatYouWant)
Это будет соответствовать Not_this_context
, но в некотором смысле это совпадение входит в мусорное ведро, потому что мы не будем смотреть на общие совпадения: мы рассматриваем только захваты группы 1.
В вашем случае, игнорируя ваши цифры и три контекста, мы можем сделать:
s1|s2|s3|(\b\d+\b)
Обратите внимание, что, поскольку мы фактически сопоставляем s1, s2 и s3 вместо того, чтобы пытаться избежать их с помощью обходных путей, отдельные выражения для s1, s2 и s3 могут оставаться ясными как день. (Они являются подвыражениями на каждой стороне |
)
Все выражение может быть написано так:
(?m)^.*\.$|\([^\)]*\)|if\(.*?//endif|(\b\d+\b)
Посмотрите эту демонстрацию (но обратите внимание на группы захвата в нижней правой панели.)
Если вы мысленно пытаетесь разделить это регулярное выражение на каждом |
разделитель, это на самом деле только серия из четырех очень простых выражений.
Для ароматов, которые поддерживают свободное пространство, это выглядит особенно хорошо.
(?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.
Вариант 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)(?!)
демо для этой версии
Приложения
Вот некоторые общие проблемы, которые этот метод часто может легко решить. Вы заметите, что выбор слова может заставить некоторые из этих проблем звучать по-разному, хотя на самом деле они практически идентичны.
- Как я могу сопоставить foo, кроме как где-нибудь в теге, как
<a stuff...>...</a>
? - Как я могу соответствовать Foo, кроме как в
<i>
тег или фрагмент Ja vaScript (больше условий)? - Как я могу сопоставить все слова, которых нет в этом черном списке?
- Как я могу игнорировать что-либо внутри блока SUB... END SUB?
- Как я могу сопоставить все, кроме... s1 s2 s3?
Как запрограммировать захваты группы 1
Вы не относились к коду, но, для завершения... Код для проверки группы 1, очевидно, будет зависеть от вашего языка. В любом случае, он не должен добавлять более пары строк в код, который вы бы использовали для проверки совпадений.
Если вы сомневаетесь, я рекомендую вам взглянуть на раздел примеров кода в статье, упомянутой ранее, где представлен код для нескольких языков.
альтернативы
В зависимости от сложности вопроса и используемого механизма регулярных выражений, существует несколько альтернатив. Вот два из них, которые могут применяться к большинству ситуаций, включая множественные условия. На мой взгляд, ни один из них не так привлекателен, как s1|s2|s3|(whatYouWant)
рецепт, хотя бы потому, что ясность всегда побеждает.
1. Замените затем Матч.
Хорошее решение, которое звучит смешно, но хорошо работает во многих средах, - это работать в два этапа. Первое регулярное выражение нейтрализует контекст, который вы хотите игнорировать, заменяя потенциально конфликтующие строки. Если вы хотите только сопоставить, то вы можете заменить его пустой строкой, а затем выполнить сопоставление на втором шаге. Если вы хотите заменить, вы можете сначала заменить игнорируемые строки чем-то отличительным, например, окружив ваши цифры цепочкой фиксированной ширины: @@@
, После этой замены вы можете заменить то, что вы действительно хотели, тогда вам придется вернуть свой отличительный @@@
строки.
2. Lookarounds.
Ваш оригинальный пост показал, что вы понимаете, как исключить одно условие, используя обходные пути. Вы сказали, что C# отлично подходит для этого, и вы правы, но это не единственный вариант..NET регулярные выражения, например, в C#, VB.NET и Visual C++, а также все еще экспериментальный regex
модуль для замены re
в Python есть только два известных мне движка, которые поддерживают просмотр бесконечной ширины. С помощью этих инструментов одно условие в одном взгляде может позаботиться о том, чтобы смотреть не только за спиной, но и за матчем и за его пределами, избегая необходимости координировать свои действия с прогнозом. Больше условий? Больше взглядов.
Повторно использовав регулярное выражение для s3 в C#, весь шаблон будет выглядеть следующим образом.
(?!.*\.)(?<!\([^()]*(?=\d+[^)]*\)))(?<!if\(\D*(?=\d+.*?//endif))\b\d+\b
Но теперь вы знаете, что я не рекомендую это, верно?
Пропуски
@HamZa и @Jerry предложили упомянуть дополнительный прием для случаев, когда вы пытаетесь просто удалить WhatYouWant
, Вы помните, что рецепт, чтобы соответствовать WhatYouWant
(захват его в группу 1) был s1|s2|s3|(WhatYouWant)
, право? Удалить все экземпляры WhatYouWant
, вы измените регулярное выражение на
(s1|s2|s3)|WhatYouWant
Для замены строки вы используете $1
, Здесь происходит то, что для каждого экземпляра s1|s2|s3
что подобрано, замена $1
заменяет этот экземпляр самим собой (ссылается на $1
). С другой стороны, когда WhatYouWant
сопоставляется, заменяется пустой группой и ничем иным - и, следовательно, удаляется. Посмотрите эту демонстрацию, спасибо @HamZa и @Jerry за предложение этого замечательного дополнения.
Замены
Это приводит нас к заменам, о которых я кратко коснусь.
- При замене на ничто, см. Трюк "Удаление" выше.
- При замене, если используется Perl или PCRE, используйте
(*SKIP)(*F)
упомянутый выше вариант, чтобы точно соответствовать тому, что вы хотите, и сделать прямую замену. - В других вариантах в вызове функции замены проверьте соответствие с помощью обратного вызова или лямбда-выражения и замените, если установлена группа 1. Если вам нужна помощь в этом, статья, на которую вы ссылаетесь, даст вам код на разных языках.
Повеселись!
Нет, подождите, это еще не все!
Ах, нет, я сохраню это для моих мемуаров в двадцати томах, которые будут выпущены следующей весной.
Сделайте три разных совпадения и обработайте комбинацию трех ситуаций, используя условную логику в программе. Вам не нужно обрабатывать все в одном гигантском регулярном выражении.
РЕДАКТИРОВАТЬ: позвольте мне немного расширить, потому что вопрос стал более интересным:-)
Общая идея, которую вы пытаетесь уловить, заключается в сопоставлении с определенным шаблоном регулярных выражений, но не в тех случаях, когда в тестовой строке присутствуют определенные другие (могут быть любые) шаблоны. К счастью, вы можете воспользоваться вашим языком программирования: упростите регулярные выражения и просто используйте сложное условие. Лучше всего было бы зафиксировать эту идею в повторно используемом компоненте, поэтому давайте создадим класс и метод, который ее реализует:
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), а затем пытаемся сопоставить несколько тестовых строк. Результаты печати должны быть такими, как показано в комментариях рядом с каждой тестовой строкой.
Ганс, если вы не возражаете, я использовал стиральную машину вашего соседа под названием 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
Учитывая файл 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
И скрипт 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 должно соответствовать 12345 должно соответствовать 12345 должно соответствовать 12345
Не уверен, поможет ли это вам или нет, но я предоставляю решение с учетом следующих предположений:
- Вам нужно элегантное решение, чтобы проверить все условия
- Условия могут измениться в будущем и в любое время.
- Одно условие не должно зависеть от других.
Однако я учел и следующее -
- Данный файл имеет минимальные ошибки в нем. Если это так, то мой код может потребовать некоторых модификаций, чтобы справиться с этим.
- Я использовал Stack, чтобы отслеживать
if(
блоки.
Хорошо, вот решение -
Я использовал C# и вместе с ним MEF (Microsoft Extensibility Framework) для реализации настраиваемых парсеров. Идея состоит в том, чтобы использовать один синтаксический анализатор для анализа и список настраиваемых классов валидатора для проверки строки и возврата значения true или false на основе проверки. Затем вы можете добавить или удалить любой валидатор в любое время или добавить новые, если хотите. До сих пор я уже реализовал для S1, S2 и S3, о которых вы упомянули, проверьте классы в пункте 3. Вы должны добавить классы для s4, s5, если вам это нужно в будущем.
Сначала создайте интерфейсы -
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); } }
Затем идет программа для чтения файлов и проверки -
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(); } } }
Затем идет реализация отдельных контролеров, имена классов говорят сами за себя, поэтому я не думаю, что им нужно больше описаний.
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); } } }
Программа -
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
, он будет автоматически вызван и, следовательно, будет подтвержден.
Ваше требование, что это не внутри паренов, невозможно удовлетворить во всех случаях. А именно, если вы можете как-то найти (
влево и )
вправо, это не всегда означает, что вы находитесь внутри паренсов. Например.
(....) + 55555 + (.....)
- не внутри пары, пока есть (
а также )
влево и вправо
Теперь вы можете считать себя умным и искать (
влево, только если вы не сталкиваетесь )
до и наоборот направо. Это не будет работать для этого случая:
((.....) + 55555 + (.....))
- внутри паренса, хотя есть закрытие )
а также (
влево и вправо.
Невозможно определить, находитесь ли вы внутри паренов, используя регулярное выражение, поскольку регулярное выражение не может сосчитать, сколько паренов было открыто и сколько закрыто.
Рассмотрим эту более простую задачу: с помощью регулярных выражений выясните, все ли (возможно, вложенные) парены в строке закрыты, то есть для каждого (
вам нужно найти )
, Вы обнаружите, что это невозможно решить, и если вы не можете решить это с помощью регулярных выражений, то вы не сможете выяснить, находится ли слово в скобках для всех случаев, так как вы не можете найти какую-то позицию в строке, если все предшествующее (
иметь соответствующий )
,
То же, что и у @zx81 (*SKIP)(*F)
но с использованием отрицательного прогнозного утверждения.
(?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