Регулярное выражение, чтобы проверить, находится ли строка в определенном шаблоне, который может содержать вложенные круглые скобки в С#
Я пытаюсь написать код, который будет проверять, содержит ли данная строка определенные строки с определенным шаблоном.
Если быть точным, например:
string mainString = @"~(Homo Sapiens means (human being)) or man or ~woman"
List<string> checkList = new List<string>{"homo sapiens","human","man","woman"};
Теперь я хочу извлечь
"homo sapiens", "human" and "woman" but NOT "man"
из приведенного выше списка, поскольку они следуют за шаблоном, т.е. строка, за которой следует ~ или одна из строк внутри скобок, которая начинается с ~.
До сих пор я придумал:
string mainString = @"~(Homo Sapiens means (human being)) or man or ~woman"
List<string> checkList = new List<string>{"homo sapiens","human","man","woman"};
var prunedList = new List<string>();
foreach(var term in checkList)
{
var pattern = @"~(\s)*(\(\s*)?(\(?\w\s*\)?)*" + term + @"(\s*\))?";
Match m = Regex.Match(mainString, pattern);
if(m.success)
{
prunedList.Add(term);
}
}
Но этот шаблон не работает для всех случаев...
Может ли кто-нибудь предложить мне, как это можно сделать?
Ответы
Ответ 1
Проверка Paranthesis - это контекстно-свободный язык или грамматика, для которой требуется проверка стека. Регулярные выражения подходят для обычных языков. У них нет памяти, поэтому они не могут использоваться для таких целей.
Чтобы проверить это, вам нужно отсканировать строку и подсчитать круглые скобки:
- инициализировать
count
до 0
- сканировать строку
- если текущий символ
(
, то increment count
- если текущий символ
)
, то декремент count
- если
count
отрицательно, вызовите ошибку, которая несовместима в скобках; например, )(
- В конце, если
count
положительно, то есть некоторая закрытая скобка
- Если
count
равно нулю, тогда тест передается
Или в С#:
public static bool CheckParentheses(string input)
{
int count = 0;
foreach (var ch in input)
{
if (ch == '(') count++;
if (ch == ')') count--;
// if a parenthesis is closed without being opened return false
if(count < 0)
return false;
}
// in the end the test is passed only if count is zero
return count == 0;
}
Вы видите, поскольку регулярные выражения не способны подсчитать, тогда они не могут проверять такие шаблоны.
Ответ 2
Я написал простой парсер, который хорошо работает для примера, который вы дали.
Я не знаю, что ожидаемое поведение для строки, которая заканчивается в этом шаблоне: ~(some words
(т.е. закрывающая скобка с допустимым открытием)
Я уверен, что вы могли бы очистить это...
private bool Contains(string source, string given)
{
return ExtractValidPhrases(source).Any(p => RegexMatch(p, given));
}
private bool RegexMatch(string phrase, string given)
{
return Regex.IsMatch(phrase, string.Format(@"\b{0}\b", given), RegexOptions.IgnoreCase);
}
private IEnumerable<string> ExtractValidPhrases(string source)
{
bool valid = false;
var parentheses = new Stack<char>();
var phrase = new StringBuilder();
for(int i = 0; i < source.Length; i++)
{
if (valid) phrase.Append(source[i]);
switch (source[i])
{
case '~':
valid = true;
break;
case ' ':
if (valid && parentheses.Count == 0)
{
yield return phrase.ToString();
phrase.Clear();
}
if (parentheses.Count == 0) valid = false;
break;
case '(':
if (valid)
{
parentheses.Push('(');
}
break;
case ')':
if (valid)
{
parentheses.Pop();
}
break;
}
}
//if (valid && !parentheses.Any()) yield return phrase.ToString();
if (valid) yield return phrase.ToString();
}
Вот те тесты, которые я использовал:
// NUnit tests
[Test]
[TestCase("Homo Sapiens", true)]
[TestCase("human", true)]
[TestCase("woman", true)]
[TestCase("man", false)]
public void X(string given, bool shouldBeFound)
{
const string mainString = @"~(Homo Sapiens means (human being)) or man or ~woman";
Assert.AreEqual(shouldBeFound, Contains(mainString, given));
}
[Test]
public void Y()
{
const string mainString = @"~(Homo Sapiens means (human being)) or man or ~woman";
var checkList = new List<string> {"homo sapiens", "human", "man", "woman"};
var expected = new List<string> { "homo sapiens", "human", "woman" };
var filtered = checkList.Where(s => Contains(mainString, s));
CollectionAssert.AreEquivalent(expected, filtered);
}
Ответ 3
Язык сбалансированной круглой скобки не является регулярным, и в результате вы не можете выполнить то, что хотите, используя RegEx. Лучшим подходом было бы использовать традиционный синтаксический анализ строк с несколькими счетчиками - один для открытого парнера и один для близких парнеров - или стек для создания модели, аналогичной Push Down Automaton.
Чтобы лучше понять концепцию, ознакомьтесь с КПК в Википедии. http://en.wikipedia.org/wiki/Pushdown_automaton
Ниже приведен пример использования стека для получения строк внутри большинства парсеров (псевдокод).
Stack stack = new Stack();
char[] stringToParse = originalString.toCharArray();
for (int i = 0; i < stringToParse.Length; i++)
{
if (stringToParse[i] == '(')
stack.push(i);
if (stringToParse[i] == ')')
string StringBetweenParens = originalString.GetSubstring(stack.pop(), i);
}
Теперь, конечно, это надуманный пример, и для выполнения более серьезного разбора потребуется некоторая работа, но он дает вам основную идею о том, как это сделать. Я забыл такие вещи; правильные имена функций (не хотелось бы смотреть их прямо сейчас), как получить текст в вложенных паранах, например, получить "внутреннюю" из строки "(внешняя (внутренняя))" (эта функция вернет "внешний (внутренний) )" ) и как сохранить строки, которые вы вернетесь.
Ответ 4
Просто по академическим причинам я хотел бы также представить решение для регулярных выражений. В основном, потому что вы, вероятно, используете единственный механизм регулярных выражений, способный решить это.
После устранения некоторых интересных вопросов о сочетании уникальных возможностей .NET, вот код, который дает вам желаемые результаты:
string mainString = @"~(Homo Sapiens means (human being)) or man or ~woman";
List<string> checkList = new List<string> { "homo sapiens", "human", "man", "woman" };
// build subpattern "(?:homo sapiens|human|man|woman)"
string searchAlternation = "(?:" + String.Join("|", checkList.ToArray()) + ")";
MatchCollection matches = Regex.Matches(
mainString,
@"(?<=~|(?(Depth)(?!))~[(](?>[^()]+|(?<-Depth>)?[(]|(?<Depth>[)]))*)"+searchAlternation,
RegexOptions.IgnoreCase
);
Как это работает? Во-первых,.NET поддерживает балансировочные группы, которые позволяют обнаруживать правильно вложенные шаблоны. Каждый раз, когда мы захватываем что-то с именованной группой захвата
(например, (?<Depth>somepattern)
) он не перезаписывает последний захват, а вместо этого помещается в стек. Мы можем вытащить один захват из этого стека с помощью (?<-Depth>)
. Это не удастся, если стек пуст (точно так же, как то, что не соответствует текущей позиции). И мы можем проверить, пустой ли стек или нет с (?(Depth)patternIfNotEmpty|patternIfEmpty)
.
В дополнение к этому,.NET имеет единственный механизм регулярных выражений, который поддерживает переменные длины lookbehinds. Если мы сможем использовать эти две функции вместе, мы можем посмотреть слева от одной из наших желаемых строк и посмотреть, есть ли ~(
где-то вне текущей структуры вложенности.
Но вот улов (см. ссылку выше). Lookbehinds выполняются справа налево в .NET, что означает, что нам нужно нажимать закрывающие парсеры и всплывать при встрече с открытыми парадами, а не наоборот.
Итак, вот для некоторого объяснения этого убийственного регулярного выражения (проще понять, если вы читаете lookbehind снизу вверх, как и .NET):
(?<= # lookbehind
~ # if there is a literal ~ to the left of our string, we're good
| # OR
(?(Depth)(?!)) # if there is something left on the stack, we started outside
# of the parentheses that end end "~("
~[(] # match a literal ~(
(?> # subpattern to analyze parentheses. the > makes the group
# atomic, i.e. suppresses backtracking. Note: we can only do
# this, because the three alternatives are mutually exclusive
[^()]+ # consume any non-parens characters without caring about them
| # OR
(?<-Depth>)? # pop the top of stack IF possible. the last ? is necessary for
# like "human" where we start with a ( before there was a )
# which could be popped.
[(] # match a literal (
| # OR
(?<Depth>[)]) # match a literal ) and push it onto the stack
)* # repeat for as long as possible
) # end of lookbehind
(?:homo sapiens|human|man|woman)
# match one of the words in the check list
Ответ 5
Это невозможно с помощью регулярных выражений.
Вы должны отказаться от идеи использовать их и использовать обычные строковые операции, такие как IndexOf
.