【问题标题】:Regular expression to check if a string is within certain pattern that may contain nested parentheses in c#正则表达式检查字符串是否在 C# 中可能包含嵌套括号的特定模式内
【发布时间】:2012-11-14 20:54:23
【问题描述】:

我一直在尝试编写一个代码来检查给定的字符串是否包含具有特定模式的特定字符串。 准确来说,例如:

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);
   }
 }

但这种模式并不适用于所有情况...... 谁能建议我如何做到这一点?

【问题讨论】:

    标签: c# regex


    【解决方案1】:

    我编写了一个简单的解析器,非常适合您给出的示例。

    我不知道以这种模式结尾的字符串的预期行为是什么:~(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);
    }
    

    【讨论】:

      【解决方案2】:

      平衡括号的语言不规则,因此您无法使用 RegEx 完成您想要的。更好的方法是使用带有几个计数器的传统字符串解析(一个用于打开括号,一个用于关闭括号)或堆栈来创建类似于下推自动机的模型。

      要更好地了解这个概念,请查看维基百科上的 PDA。 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);
       }
      

      当然,这是一个人为的示例,需要一些工作才能进行更严格的解析,但它为您提供了如何执行此操作的基本概念。我遗漏了类似的东西;正确的函数名称(现在不想查找它们),如何在嵌套括号中获取文本,例如从字符串“(外部(内部))”中获取“内部”(该函数将返回“外部(内部) )"),以及如何存储返回的字符串。

      【讨论】:

        【解决方案3】:

        出于学术原因,我也想介绍一下正则表达式解决方案。主要是因为您可能正在使用唯一能够解决此问题的正则表达式引擎。

        在清理了一些关于 .NET 独特功能组合的 interesting issues 之后,下面是为您提供所需结果的代码:

        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 支持平衡组,允许检测正确嵌套的模式。每次我们使用命名捕获组捕获某些东西时 (如(?&lt;Depth&gt;somepattern))它不会覆盖最后一次捕获,而是被推入堆栈。我们可以使用(?&lt;-Depth&gt;) 从该堆栈中弹出一个捕获。如果堆栈为空(就像在当前位置不匹配的东西),这将失败。并且我们可以通过(?(Depth)patternIfNotEmpty|patternIfEmpty)检查栈是否为空。

        除此之外,.NET 还拥有唯一支持可变长度后视的正则表达式引擎。如果我们可以同时使用这两个功能,我们可以查看所需字符串之一的左侧,看看当前嵌套结构之外的某处是否有 ~(

        但这里有一个问题(见上面的链接)。 Lookbehinds 在 .NET 中从右到左执行,这意味着我们需要推动关闭括号并在遇到打开括号时弹出,而不是相反。

        所以这里是对那个凶残的正则表达式的一些解释(如果你从下到上阅读后面的内容会更容易理解,就像 .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
        

        【讨论】:

          【解决方案4】:

          括号检查是context-free language 或语法,它需要一个堆栈进行检查。正则表达式适用于regular languages。它们没有内存,因此不能用于此类目的。

          要检查这一点,您需要扫描字符串并计算括号:

          • count初始化为0
          • 扫描字符串
            • 如果当前字符是(,则递增count
            • 如果当前字符是),则递减count
            • 如果count为负数,则抛出括号不一致的错误;例如,)(
          • 最后,如果count 是正数,那么有一些不闭合的括号
          • 如果count 为零,则测试通过

          或者在 C# 中:

          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;
          }
          

          你看,因为正则表达式不能计数,所以它们不能检查这种模式。

          【讨论】:

            【解决方案5】:

            使用正则表达式是不可能的。 你应该放弃使用它们的想法并使用像IndexOf这样的普通字符串操作。

            【讨论】:

            • 虽然丑陋,但绝对可以使用像 .NET 一样强大的正则表达式引擎(PCRE 也可以)。
            • 是的,我想我应该放弃使用正则表达式的想法。我的另一个想法是修剪〜和以下单词之间的空格,并根据空格打破句子,并为破碎的以~开头的单词,检查列表中的单词是否存在。
            猜你喜欢
            • 1970-01-01
            • 2012-09-13
            • 2014-04-25
            • 2022-09-27
            • 1970-01-01
            • 2013-11-19
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多