Регулярное выражение шаблон для соответствия, исключая когда... / кроме между
-- 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 ответов:
Ганс, я возьму наживку и изложу свой предыдущий ответ. Вы сказали, что хотите "что-то более полное", поэтому я надеюсь, что вы не будете возражать против длинного ответа-просто пытаясь угодить. Давайте начнем с фона.
во-первых, это отличный вопрос. Часто возникают вопросы о соответствии определенным шаблонам, за исключением определенных контекстов (например, внутри блока кода или внутри круглых скобок). Эти вопросы часто приводят к довольно неудобным решениям. Так что ваш вопрос о несколько контекстов - это особый вызов.
сюрприз
Удивительно, но есть по крайней мере одно эффективное решение, которое является общим, легко реализовать и приятно поддерживать. Это работает со всеми вкусами регулярных выражений которые позволяют вам проверять группы захвата в вашем коде. И это случается, чтобы ответить на ряд общих вопросов, которые могут на первый взгляд звучать иначе, чем у вас: "матч все, кроме пончиков", " заменить все, кроме.....", "сопоставить все слова, кроме тех, что в черном списке моей мамы", "игнорировать теги", "сопоставить температуру, если не выделено курсивом"...
к сожалению, метод не очень хорошо известен: я оцениваю, что в двадцати 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.
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 для этой версии
приложения
вот некоторые общие проблемы, которые эта техника часто может легко решать. Вы заметите, что выбор слова может сделать эти проблемы звучат по-разному, а на самом деле они практически идентичны.
- как я могу соответствовать foo, кроме как в любом месте тега, как
<a stuff...>...</a>
?- как я могу соответствовать foo, кроме как в
<i>
тег или фрагмент javascript (дополнительные условия)?- как я могу сопоставить все слова, которые не находятся в этом черном списке?
- как я могу игнорировать что-либо внутри суб... КОНЕЦ ПОДПРОГРАММЫ блок?
- как я могу соответствовать все, кроме... 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 за предложение этого замечательного дополнения.замены
это приводит нас к заменам, о котором я коснусь вкратце.
- при замене ничем, см. трюк "удаления" выше.
- при замене, если используется Perl или PCRE, используйте
(*SKIP)(*F)
вариация, упомянутая выше, чтобы соответствовать именно тому, что вы хотите, и сделать прямую замену.- в других вариантах в вызове функции замены проверьте соответствие с помощью обратного вызова или лямбды и замените, если установлена группа 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
не уверен, что это поможет вам или нет, но я предоставляю решение с учетом следующих предположений -
- вам нужно элегантное решение, чтобы проверить все условия
- условия могут измениться в будущем и в любое время.
- одно условие не должно зависеть от других.
однако я учел и следующее -
- данный файл имеет минимальные ошибки в нем. Если это крольчиха, то мой код может потребоваться некоторые изменения, чтобы справиться с этим.
- я использовал стек, чтобы отслеживать
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
, он будет автоматически вызываться и, таким образом, будет проверять.
то же, что и @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