Регулярное выражение, чтобы проверить, находится ли строка в определенном шаблоне, который может содержать вложенные круглые скобки в С#

Я пытаюсь написать код, который будет проверять, содержит ли данная строка определенные строки с определенным шаблоном. Если быть точным, например:

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.