Шаблон регулярного выражения для соответствия, Исключая, когда.../За исключением между
- Изменить -. В текущих ответах есть несколько полезных идей, но я хочу что-то более полное, что я могу на 100% понять и повторно использовать; почему я поставил щедрость. Кроме того, идеи, которые работают повсюду, лучше для меня, чем не стандартный синтаксис, например \K
Этот вопрос касается того, как я могу сопоставить шаблон, кроме некоторых ситуаций s1 s2 s3. Я даю конкретный пример, чтобы показать свой смысл, но предпочитаю общий ответ, который я могу на 100% понять, поэтому я могу его повторно использовать в других ситуациях.
Пример
Я хочу сопоставить пять цифр с помощью \b\d{5}\b
, но не в трех ситуациях: s1 s2 s3:
s1: Не на строке, заканчивающейся периодом, подобным этому предложению.
s2: Не где-либо внутри parens.
s3: Не внутри блока, который начинается с if(
и заканчивается на //endif
Я знаю, как решить любой из s1 s2 s3 с lookahead и lookbehind, особенно в С# lookbehind или \K
в PHP.
Например
s1 (?m)(?!\d+.*?\.$)\d+
s3 с С# lookbehind (?<!if\(\D*(?=\d+.*?//endif))\b\d+\b
s3 с PHP\K (?:(?:if\(.*?//endif)\D*)*\K\d+
Но смешивание условий вместе заставляет мою голову взорваться. Еще более плохая новость заключается в том, что мне может потребоваться добавить другие условия s4 s5 в другое время.
Хорошая новость: мне все равно, обрабатываю ли я файлы с использованием наиболее распространенных языков, таких как PHP, С#, Python или моя соседняя стиральная машина.:) Я довольно много начинаю на Python и Java, но мне интересно узнать, есть ли у него решение.
Итак, я пришел сюда, чтобы узнать, думает ли кто-нибудь о гибком рецепте.
Подсказки в порядке: вам не нужно указывать полный код.:)
Спасибо.
Ответы
Ответ 1
Ганс, я возьму наживку и уточню свой предыдущий ответ. Вы сказали, что хотите "что-то более полное", поэтому я надеюсь, что вы не будете возражать против длинного ответа - просто постарайтесь угодить. Давайте начнем с некоторого фона.
Во-первых, это отличный вопрос. Часто возникают вопросы о сопоставлении определенных шаблонов, за исключением определенных контекстов (например, внутри блока кода или внутри скобок). Эти вопросы часто приводят к довольно неловким решениям. Так что ваш вопрос о множественных контекстах - это особый вызов.
сюрприз
Удивительно, но есть по крайней мере одно эффективное решение, общее, простое в реализации и приятное в обслуживании. Он работает со всеми разновидностями регулярных выражений, которые позволяют вам проверять группы захвата в вашем коде. И это происходит, чтобы ответить на ряд общих вопросов, которые на первый взгляд могут звучать не так, как у вас: "соответствовать всем, кроме пончиков", "заменить все, кроме...", "соответствовать всем словам, кроме тех, которые находятся в черном списке моей мамы", "игнорировать теги "," соответствуют температуре, если они не выделены курсивом "...
К сожалению, методика не очень хорошо известна: по моим оценкам, в двадцати SO-вопросах, которые могли бы ее использовать, только один имеет один ответ, в котором упоминается об этом, а это, возможно, один из пятидесяти или шестидесяти ответов. Смотрите мой обмен с Коби в комментариях. Техника подробно описана в этой статье, которая (оптимистично) называет ее "лучшим из когда-либо существовавших регулярных выражений". Не вдаваясь в подробности, я попытаюсь дать вам четкое представление о том, как работает техника. Для получения более подробной информации и примеров кода на разных языках я советую вам обратиться к этому ресурсу.
Лучше известное изменение
Существует вариант с использованием синтаксиса, специфичного для Perl и PHP, который выполняет то же самое. Вы увидите его на SO в руках регулярных выражений мастеров, таких как 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 вместо того, чтобы пытаться избежать их с помощью обходных путей, отдельные выражения для 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.
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)(?!)
демо для этой версии
Приложения
Вот некоторые общие проблемы, которые этот метод часто может легко решить. Вы заметите, что выбор слова может заставить некоторые из этих проблем звучать по-разному, хотя на самом деле они практически идентичны.
- Как я могу сопоставить foo, кроме как где-нибудь в теге типа
<a stuff...>...</a>
? - Как мне сопоставить foo, кроме
<i>
или фрагмента javascript (дополнительные условия)? - Как я могу сопоставить все слова, которых нет в этом черном списке?
- Как я могу игнорировать что-либо внутри блока SUB... END SUB?
- Как я могу сопоставить все, кроме... s1 s2 s3?
Как запрограммировать захваты группы 1
Вы не относились к коду, но для завершения... Код для проверки группы 1, очевидно, будет зависеть от вашего языка. В любом случае, он не должен добавлять более пары строк в код, который вы бы использовали для проверки совпадений.
Если вы сомневаетесь, я рекомендую вам взглянуть на раздел примеров кода в статье, упомянутой ранее, где представлен код для нескольких языков.
альтернативы
В зависимости от сложности вопроса и используемого механизма регулярных выражений, существует несколько альтернатив. Вот два из них, которые могут применяться к большинству ситуаций, включая множественные условия. На мой взгляд, ни один из них не так привлекателен, как рецепт s1|s2|s3|(whatYouWant)
, хотя бы потому, что ясность всегда побеждает.
1. Замените затем Матч.
Хорошее решение, которое звучит смешно, но хорошо работает во многих средах, - это работать в два этапа. Первое регулярное выражение нейтрализует контекст, который вы хотите игнорировать, заменяя потенциально конфликтующие строки. Если вы хотите только сопоставить, вы можете заменить его пустой строкой, а затем выполнить сопоставление на втором шаге. Если вы хотите заменить, вы можете сначала заменить игнорируемые строки чем-то отличительным, например, окружив ваши цифры цепочкой фиксированной ширины @@@
. После этой замены вы можете заменить то, что действительно хотели, тогда вам придется вернуть свои отличительные @@@
строки.
2. Lookarounds.
Ваш оригинальный пост показал, что вы понимаете, как исключить одно условие, используя обходные пути. Вы сказали, что С# отлично подходит для этого, и вы правы, но это не единственный вариант. Например, разновидности регулярных выражений .NET, встречающиеся в С#, VB.NET и Visual C++, а также все еще экспериментальный модуль regex
для замены re
в Python - это единственные два движка, которые я знаю, которые поддерживают просмотр бесконечной ширины. С помощью этих инструментов одно условие в одном взгляде может заботиться о том, чтобы смотреть не только за спиной, но и за матчем и за его пределами, избегая необходимости координировать свои действия с предвидением. Больше условий? Больше взглядов.
Повторно использовав регулярное выражение для s3 в С#, весь шаблон будет выглядеть следующим образом.
(?!.*\.)(?<!\([^()]*(?=\d+[^)]*\)))(?<!if\(\D*(?=\d+.*?//endif))\b\d+\b
Но теперь вы знаете, что я не рекомендую это, верно?
Пропуски
@HamZa и @Jerry предложили упомянуть дополнительный прием для случаев, когда вы WhatYouWant
просто удалить 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. Если вам нужна помощь в этом, уже упоминавшаяся статья предоставит вам код на разных языках.
Повеселись!
Нет, подожди, там больше!
Ах, нет, я сохраню это для моих мемуаров в двадцати томах, которые выйдут следующей spring.
Ответ 2
Сделайте три разных соответствия и обработайте комбинацию из трех ситуаций, используя условную логику в программе. Вам не нужно обрабатывать все в одном гигантском регулярном выражении.
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)., а затем попытайтесь сопоставить несколько тестовых строк. Распечатанные результаты должны быть такими, как показано в комментариях рядом с каждой тестовой строкой.
Ответ 3
Ваше требование, чтобы оно не было внутри parens в невозможном для удовлетворения всех случаев.
А именно, если вы можете как-то найти (
влево и )
вправо, это не всегда означает, что вы находитесь внутри parens. Например.
(....) + 55555 + (.....)
- не внутри parens, но есть (
и )
влево и вправо
Теперь вы можете подумать, что вы умны и смотрите (
влево, только если вы не встретите )
до и наоборот справа. Это не будет работать для этого случая:
((.....) + 55555 + (.....))
- внутри парнов, даже если есть закрытие )
и (
слева и справа.
Невозможно узнать, используете ли вы внутри parens, используя регулярное выражение, поскольку регулярное выражение не может подсчитать, сколько парнеров было открыто и сколько закрыто.
Рассмотрим эту более легкую задачу: с помощью regex выясните, закрыты ли все (возможно, вложенные) парсеры в строке, то есть для каждого (
вам нужно найти )
. Вы узнаете, что это невозможно решить, и если вы не можете решить это с помощью регулярного выражения, тогда вы не можете понять, находится ли слово внутри parens для всех случаев, так как вы не можете найти какую-либо позицию в строке, если все предыдущие (
имеют соответствующий )
.
Ответ 4
Ханс, если вы не против, я использовал вашу соседнюю стиральную машину под названием 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:
[email protected]:~$ 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
И script validator.pl:
[email protected]:~$ 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";
}
}
Исполнение:
[email protected]:~$ cat input.txt | perl validator.pl
it should match 12345
it should match 12345
it should match 12345
Ответ 5
Не уверен, что это поможет вам или нет, но я предлагаю решение, учитывая следующие предположения -
- Вам нужно элегантное решение для проверки всех условий.
- Условия могут меняться в будущем и в любое время.
- Одно условие не должно зависеть от других.
Однако я также рассмотрел следующее:
- Указанный файл содержит минимальные ошибки. Если это произойдет, то моему коду могут потребоваться некоторые изменения, чтобы справиться с этим.
- Я использовал Stack для отслеживания блоков
if(
.
Хорошо, вот решение -
Я использовал С# и с ним 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
, оно будет автоматически вызвано и, таким образом, будет проверяться.
Ответ 6
То же, что и @zx81 (*SKIP)(*F)
, но с использованием отрицательного утверждения.
(?m)(?:if\(.*?\/\/endif|\([^()]*\))(*SKIP)(*F)|\b\d+\b(?!.*\.$)
DEMO
В 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