用于匹配函数和捕获其参数的正则表达式

时间:2013-09-19 23:20:45

标签: c# regex

我正在研究一个计算器,它需要字符串表达式并对它们进行评估。我有一个函数,使用Regex在表达式中搜索数学函数,检索参数,查找函数名称并对其进行求值。我遇到的问题是,如果我知道将会有多少参数,我只能做到这一点,我无法正确使用正则表达式。如果我只是用(字符拆分),字符的内容,那么我就不能在该参数中进行其他函数调用。

以下是函数匹配模式:\b([a-z][a-z0-9_]*)\((..*)\)\b

它只适用于一个参数,我可以为除嵌套函数内部的每个参数创建一个组吗?例如,它匹配:func1(2 * 7, func2(3, 5))并创建捕获组:2 * 7func2(3, 5)

这里我用来评估表达式的函数:

    /// <summary>
    /// Attempts to evaluate and store the result of the given mathematical expression.
    /// </summary>
    public static bool Evaluate(string expr, ref double result)
    {
        expr = expr.ToLower();

        try
        {
            // Matches for result identifiers, constants/variables objects, and functions.
            MatchCollection results = Calculator.PatternResult.Matches(expr);
            MatchCollection objs = Calculator.PatternObjId.Matches(expr);
            MatchCollection funcs = Calculator.PatternFunc.Matches(expr);

            // Parse the expression for functions.
            foreach (Match match in funcs)
            {
                System.Windows.Forms.MessageBox.Show("Function found. - " + match.Groups[1].Value + "(" + match.Groups[2].Value + ")");

                int argCount = 0;
                List<string> args = new List<string>();
                List<double> argVals = new List<double>();
                string funcName = match.Groups[1].Value;

                // Ensure the function exists.
                if (_Functions.ContainsKey(funcName)) {
                    argCount = _Functions[funcName].ArgCount;
                } else {
                    Error("The function '"+funcName+"' does not exist.");
                    return false;
                }

                // Create the pattern for matching arguments.
                string argPattTmp = funcName + "\\(\\s*";

                for (int i = 0; i < argCount; ++i)
                    argPattTmp += "(..*)" + ((i == argCount - 1) ? ",":"") + "\\s*";
                argPattTmp += "\\)";

                // Get all of the argument strings.
                Regex argPatt = new Regex(argPattTmp);

                // Evaluate and store all argument values.
                foreach (Group argMatch in argPatt.Matches(match.Value.Trim())[0].Groups)
                {
                    string arg = argMatch.Value.Trim();
                    System.Windows.Forms.MessageBox.Show(arg);

                    if (arg.Length > 0)
                    {
                        double argVal = 0;

                        // Check if the argument is a double or expression.
                        try {
                            argVal = Convert.ToDouble(arg);
                        } catch {
                            // Attempt to evaluate the arguments expression.
                            System.Windows.Forms.MessageBox.Show("Argument is an expression: " + arg);

                            if (!Evaluate(arg, ref argVal)) {
                                Error("Invalid arguments were passed to the function '" + funcName + "'.");
                                return false;
                            }
                        }

                        // Store the value of the argument.
                        System.Windows.Forms.MessageBox.Show("ArgVal = " + argVal.ToString());
                        argVals.Add(argVal);
                    }
                    else
                    {
                        Error("Invalid arguments were passed to the function '" + funcName + "'.");
                        return false;
                    }
                }

                // Parse the function and replace with the result.
                double funcResult = RunFunction(funcName, argVals.ToArray());
                expr = new Regex("\\b"+match.Value+"\\b").Replace(expr, funcResult.ToString());
            }

            // Final evaluation.
            result = Program.Scripting.Eval(expr);
        }
        catch (Exception ex)
        {
            Error(ex.Message);
            return false;
        }

        return true;
    }

    ////////////////////////////////// ---- PATTERNS ---- \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

    /// <summary>
    /// The pattern used for function calls.
    /// </summary>
    public static Regex PatternFunc = new Regex(@"([a-z][a-z0-9_]*)\((..*)\)");

正如您所看到的,构建正则表达式以匹配参数的尝试非常糟糕。它不起作用。

我要做的就是从表达式2 * 7中提取func2(3, 5)func1(2 * 7, func2(3, 5)),但它也必须适用于具有不同参数计数的函数。如果有一种方法可以在不使用同样好的正则表达式的情况下执行此操作。

5 个答案:

答案 0 :(得分:31)

有一个简单的解决方案和一个更高级的解决方案(在编辑之后添加)来处理更复杂的功能。

为了实现您发布的示例,我建议分两步完成,第一步是提取参数(最后解释正则表达式):

\b[^()]+\((.*)\)$

现在,要解析参数。

简单解决方案

使用以下方法提取参数:

([^,]+\(.+?\))|([^,]+)

以下是一些C#代码示例(所有断言传递):

string extractFuncRegex = @"\b[^()]+\((.*)\)$";
string extractArgsRegex = @"([^,]+\(.+?\))|([^,]+)";

//Your test string
string test = @"func1(2 * 7, func2(3, 5))";

var match = Regex.Match( test, extractFuncRegex );
string innerArgs = match.Groups[1].Value;
Assert.AreEqual( innerArgs, @"2 * 7, func2(3, 5)" );
var matches = Regex.Matches( innerArgs, extractArgsRegex );            
Assert.AreEqual( matches[0].Value, "2 * 7" );
Assert.AreEqual( matches[1].Value.Trim(), "func2(3, 5)" );

正则表达式的解释。将参数提取为单个字符串:

\b[^()]+\((.*)\)$

其中:

  • [^()]+字符不是开头,结束括号。
  • \((.*)\)括号内的所有内容

args提取:

([^,]+\(.+?\))|([^,]+)

其中:

  • ([^,]+\(.+?\))不是逗号的字符,后跟括号中的字符。这会获取func参数。注意+?所以这场比赛是懒惰的并且在第一场比赛时停止了。
  • |([^,]+)如果前一个不匹配,则匹配不是逗号的连续字符。这些比赛分组。

更高级的解决方案

现在,这种方法存在一些明显的局限性,例如它匹配第一个右括号,因此它不能很好地处理嵌套函数。对于更全面的解决方案(如果需要),我们需要使用balancing group definitions(正如我在此编辑之前提到的)。出于我们的目的,平衡组定义允许我们跟踪开括号的实例并减去右括号实例。本质上,开启和关闭括号将在搜索的平衡部分中相互抵消,直到找到最终的结束括号。也就是说,匹配将继续,直到括号平衡并找到最后的右括号。

所以,现在提取parms的正则表达式(函数提取可以保持不变):

(?:[^,()]+((?:\((?>[^()]+|\((?<open>)|\)(?<-open>))*\)))*)+

以下是一些测试用例,以显示它的实际效果:

string extractFuncRegex = @"\b[^()]+\((.*)\)$";
string extractArgsRegex = @"(?:[^,()]+((?:\((?>[^()]+|\((?<open>)|\)(?<-open>))*\)))*)+";

//Your test string
string test = @"func1(2 * 7, func2(3, 5))";

var match = Regex.Match( test, extractFuncRegex );
string innerArgs = match.Groups[1].Value;
Assert.AreEqual( innerArgs, @"2 * 7, func2(3, 5)" );
var matches = Regex.Matches( innerArgs, extractArgsRegex );
Assert.AreEqual( matches[0].Value, "2 * 7" );
Assert.AreEqual( matches[1].Value.Trim(), "func2(3, 5)" );

//A more advanced test string
test = @"someFunc(a,b,func1(a,b+c),func2(a*b,func3(a+b,c)),func4(e)+func5(f),func6(func7(g,h)+func8(i,(a)=>a+2)),g+2)";
match = Regex.Match( test, extractFuncRegex );
innerArgs = match.Groups[1].Value;
Assert.AreEqual( innerArgs, @"a,b,func1(a,b+c),func2(a*b,func3(a+b,c)),func4(e)+func5(f),func6(func7(g,h)+func8(i,(a)=>a+2)),g+2" );
matches = Regex.Matches( innerArgs, extractArgsRegex );
Assert.AreEqual( matches[0].Value, "a" );
Assert.AreEqual( matches[1].Value.Trim(), "b" );            
Assert.AreEqual( matches[2].Value.Trim(), "func1(a,b+c)" );
Assert.AreEqual( matches[3].Value.Trim(), "func2(a*b,func3(a+b,c))" );
Assert.AreEqual( matches[4].Value.Trim(), "func4(e)+func5(f)" );
Assert.AreEqual( matches[5].Value.Trim(), "func6(func7(g,h)+func8(i,(a)=>a+2))" );
Assert.AreEqual( matches[6].Value.Trim(), "g+2" );

特别注意该方法现在非常先进:

someFunc(a,b,func1(a,b+c),func2(a*b,func3(a+b,c)),func4(e)+func5(f),func6(func7(g,h)+func8(i,(a)=>a+2)),g+2)

所以,再看看正则表达式:

(?:[^,()]+((?:\((?>[^()]+|\((?<open>)|\)(?<-open>))*\)))*)+

总之,它从不是逗号或括号的字符开始。然后,如果参数中有括号,则匹配并减去括号直到它们平衡。然后,如果参数中有其他函数,它会尝试重复该匹配。然后进入下一个参数(逗号后面)。详细说明:

  • [^,()]+匹配任何非&#39;,()&#39;
  • ?:表示非捕获组,即不在组中的括号内存储匹配项。
  • \(表示从一个开放式括号开始。
  • ?>表示atomic grouping - 基本上,这意味着它不记得回溯位置。这也有助于提高性能,因为尝试不同组合的步骤较少。
  • [^()]+|表示除开始或结束括号外的任何内容。接下来是| (或)
  • \((?<open>)|这是好事,并说匹配&#39;(&#39;或
  • (?<-open>)这是更好的东西,说匹配&#39;)&#39;并平衡了&#39;(&#39;。这意味着匹配的这一部分(第一个括号后的所有内容)将继续,直到所有内部括号都匹配。没有平衡表达式,匹配将在第一个完成关闭括号。关键是发动机不符合这个&#39;)&#39;反对最后的&#39;而不是从匹配中删除(&#39;。当没有进一步突出的&#39;(&#39;,&#39;,-open失败)所以最终的&#39;)可以匹配。
  • 正则表达式的其余部分包含组的右括号和重复(和+),分别为:重复内括号匹配0次或更多次,重复全括号搜索0或更多次(0允许不带括号的参数)并重复完整匹配1次或多次(允许foo(1)+ foo(2))

最后一个点缀:

如果您将(?(open)(?!))添加到正则表达式:

(?:[^,()]+((?:\((?>[^()]+|\((?<open>)|\)(?<-open>))*(?(open)(?!))\)))*)+

如果打开已捕获的东西(没有被删除),则(?!)将始终失败,即如果有没有右括号的左括号,它将始终失败。这是测试平衡是否失败的有用方法。

一些注意事项:

    当最后一个角色是&#39;)时,
  • \ b将不匹配。因为它不是单词字符和\ b tests for word character boundaries所以你的正则表达式不匹配。
  • 虽然正则表达式很强大,但除非你是大师中的大师,否则最好保持表达式简单,否则它们难以维护并且难以让其他人理解。这就是为什么有时最好将问题分解为子问题和更简单的表达式,并让语言执行一些擅长的非搜索/匹配操作。因此,您可能希望将简单的正则表达式与更复杂的代码混合使用,反之亦然,具体取决于您的舒适程度。
  • 这将匹配一些非常复杂的函数,但它不是函数的词法分析器。
  • 如果您可以在参数中包含字符串,并且字符串本身可以包含括号,例如&#34; go(...&#34;然后你将需要修改正则表达式以从比较中取出字符串。与评论相同。
  • 用于平衡组定义的一些链接:herehereherehere

希望有所帮助。

答案 1 :(得分:4)

我很抱歉破坏了RegEx泡沫,但这是你无法单独使用正则表达式有效的事情之一。

您实施的内容基本上是Operator-Precedence Parser,支持子表达式和参数列表。该语句作为标记流处理 - 可能使用正则表达式 - 子表达式作为高优先级操作处理。

使用正确的代码,您可以将此作为完整令牌流的迭代,但递归解析器也很常见。无论哪种方式,您都必须能够有效地推送状态并重新启动每个子表达式入口点的解析 - (,<function_name>(令牌 - 并将结果推送到子表达式出口点处的解析器链 - ),令牌。

答案 2 :(得分:2)

这个正则表达式做你想要的:

listView.getAdapter().notifyDataSetChanged();

在代码中粘贴它时,不要忘记转义反斜杠和双引号。

它将正确匹配双引号,内部函数和数字中的参数,如下所示:
f1(123,&#34; df&#34;&#34; j&#34;&#34;,dhf&#34;,abc12,func2(),func(123,a&gt; 2))
<登记/> param堆栈将包含
123个
&#34; DF&#34;&#34; J&#34;&#34; ,dhf&#34;
ABC12
FUNC2()
FUNC(123,→2)

答案 3 :(得分:0)

正则表达式不会让你彻底摆脱这个......

由于您有嵌套的括号,因此您需要修改代码以对(计算)。当您遇到(时,您需要记下该位置,然后向前看,为您找到的每个额外 (递增一个计数器,并为每个{{递减} { 1}}你找到了。当您的计数器为0并且找到)时,这是函数参数块的结尾,然后您可以在括号之间解析文本。当计数器为0 时,您还可以在) 上拆分文本以获取函数参数。

如果在计数器为0时遇到字符串的结尾,则会出现,错误。

然后在开始括号和右括号之间以及任何逗号之间取出文本块,并对每个参数重复上述步骤。

答案 4 :(得分:0)

有一些新的(相对非常新的)特定于语言的enhancements to regex可以将无上下文语言与“正则表达式”相匹配,但在使用更常用的工具时,您会发现更多资源和更多帮助对于这种任务:

最好使用像ANTLR,LEX + YACC,FLEX + BISON或任何其他commonly used parser generator这样的解析器生成器。他们中的大多数都提供了关于如何构建支持分组和函数调用的简单计算器的完整示例。