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

19

我正在开发一个计算器,它可以接收字符串表达式并进行求值。我有一个函数使用正则表达式在表达式中查找数学函数、检索参数、查找函数名称并对其求值。我的问题是,只有当我知道要有多少个参数才能实现这一点,而我却无法正确地使用正则表达式来解决这个问题。如果我只是通过逗号将 () 字符之间的内容分开,那么我就无法在该参数中调用其他函数。

这里是函数匹配模式:\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_]*)\((..*)\)");

正如您所看到的,这里有一个相当糟糕的尝试来构建正则表达式以匹配参数。它不起作用。

我想做的就是从表达式 func1(2 * 7, func2(3, 5)) 中提取出2 * 7func2(3, 5),但它也必须适用于具有不同参数计数的函数。如果有一种不使用正则表达式的方法也可以。


4
“嵌套元素”是正则表达式害怕的事情之一。 - King King
1
看起来你正在尝试编写一个计算器。你尝试过NCalc吗? - Simon Whitehead
不过,我已经使用MuParser作为引擎制作了一个计算器,现在我正在尝试编写自己的引擎。我正在做除了基本数学解析之外的所有事情,当它被分解成数字和符号时,然后我将其传递给ScriptControl进行评估。 - Brandon Miller
1
@BrandonMiller,我不知道你是否感兴趣,但我写了一个小解析器,可能适用于你的程序:https://github.com/DDtMM/DDtMM.SIMPLY。它肯定可以处理嵌套表达式并生成语法树...如果你想要的话,也许明天我会用它来回答问题? - Daniel Gimenez
https://groups.google.com/forum/?hl=en#!msg/alt.religion.emacs/DR057Srw5-c/Co-2L2BKn7UJ - Preston Guillot
5个回答

37

针对更为复杂的函数,既有简单解决方案,也有更高级的解决方案(在编辑后添加)。

为了实现您发布的示例,建议分两步进行,第一步是提取参数(正则表达式在结尾处解释):

\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[^()]+\((.*)\)$

含义:

  • [^()]+:匹配非小括号的字符。
  • \((.*)\):匹配小括号内的任何字符。

参数提取:

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

其中:

  • ([^,]+\(.+?\)) 匹配不包含逗号的字符后跟方括号中的字符。这会提取函数参数。请注意使用了+?以使匹配变为非贪婪的,因此它会在遇到第一个)时停止匹配。
  • |([^,]+) 如果前面的表达式不匹配,则匹配不包含逗号的连续字符。这些匹配项将进入组中。

更高级的解决方案

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

因此,提取参数的正则表达式现在是(提取函数的部分保持不变):

(?:[^,()]+((?:\((?>[^()]+|\((?<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>))*\)))*)+

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

  • [^,()] +匹配除',()'之外的任何内容
  • ?:表示非捕获组,即不要在组内存储匹配项。
  • \(表示从打开括号开始。
  • ?>表示原子分组 - 本质上,这意味着它不记住回溯位置。这也有助于提高性能,因为有更少的步进来尝试不同的组合。
  • [^()]+|表示除打开或关闭括号之外的任何内容。接下来是|(或者)
  • \((?<open>)|这是好东西,表示匹配'('或
  • (?<-open>)这是更好的东西,表示匹配')'并平衡'('。这意味着匹配的这部分(第一个括号后面的所有内容)将继续进行,直到所有内部括号匹配。如果没有平衡表达式,则匹配将在第一个结束括号处完成。关键是引擎不会将此“)”与最终的“)”匹配,而是从匹配的“(”中减去它。当没有其他未完成的“(”时,-open失败,因此可以匹配最终的“)”。
  • 正则表达式的其余部分包含组的闭合括号和重复项(和+),这些分别是:重复内部括号匹配0次或更多次,重复完整括号搜索0次或更多次(0允许没有括号的参数),并重复完整匹配1次或更多次(允许foo(1)+foo(2))。

最后一个装饰:

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

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

如果open捕获到了某些内容(但没有被减去),则(?!)将始终失败,也就是说,如果存在一个开括号没有对应的闭括号,它将始终失败。这是测试平衡是否失败的一种有用方式。

  • \b不会匹配最后一个字符为")"的情况,因为它不是单词字符,\b仅测试单词字符的边界,所以你的正则表达式将无法匹配。
  • 虽然正则表达式功能强大,但除非您是一位高手,否则最好保持表达式的简单性,因为否则它们很难维护,也很难让其他人理解。这就是为什么有时最好将问题分解成子问题和简单表达式,并让语言执行一些非搜索/匹配操作,这是它擅长的。因此,您可能需要混合使用简单的正则表达式和更复杂的代码,或者反之,具体取决于您的熟练程度。
  • 这将匹配一些非常复杂的函数,但它并不是函数的词法分析器。
  • 如果参数中可以包含字符串,而字符串本身可以包含括号,例如"go(......",则需要修改正则表达式以将字符串从比较中排除。注释也是如此。
  • 一些平衡组定义链接:这里这里这里这里

希望这能有所帮助。


2
非常好的答案,您一定是大师中的大师。 - Fredrick Gauss
2
我该如何修改代码以考虑引号?我的意思是,如果其中一个参数实际上是像这样的字符串值:"my, value"。这个正则表达式将把my视为一个参数,而value视为另一个参数。 - Dabbas
1
你会如何修改代码以忽略字符串参数中的括号和逗号? - tichra

5
这个正则表达式可以满足你的需求:
^(?<FunctionName>\w+)\((?>(?(param),)(?<param>(?>(?>[^\(\),"]|(?<p>\()|(?<-p>\))|(?(p)[^\(\)]|(?!))|(?(g)(?:""|[^"]|(?<-g>"))|(?!))|(?<g>")))*))+\)$

不要忘记在粘贴到代码中时转义反斜杠和双引号。
它将正确匹配双引号中的参数、内部函数和像这样的数字: f1(123,"df""j"" , dhf",abc12,func2(),func(123,a>2))
参数堆栈将包含: 123 "df""j"" , dhf" abc12 func2() func(123,a>2)

1
这个正则表达式为所有参数都设置了匹配组,保留引号!非常感谢你啊伙计! - BananaAcid
这个很好用,但一旦函数变得更加复杂,它就失效了。func($("g").title() + 有时候拥有"tock"比"tick"更容易, 即使${func("Variables")}存在其中,或者<div style='display:none'>HTML</div>标签)在这种情况下,尽管它只有一个参数,但它将其解析为三个! - Shiroy

4

很抱歉打破正则表达式的神话,但这是一件您无法仅凭正则表达式就能有效地完成的事情。

你正在实现的基本上是支持子表达式和参数列表的操作符优先级解析器。该语句被处理为一个标记流,可能使用正则表达式,其中子表达式作为高优先级操作进行处理。

通过正确的代码,可以将其作为对完整标记流的迭代来完成,但递归解析器也很常见。无论哪种方式,您都必须能够有效地推动状态,并在子表达式进入点((,<function_name>(标记)处重新启动解析,并在子表达式退出点(),标记)处将结果推送到解析器链上方。


0

正则表达式并不能完全解决这个问题...

由于你有嵌套的括号,你需要修改你的代码来计算 () 的数量。当你遇到一个 ( 时,你需要记下它的位置,然后向前查找,对于每个额外的 (,你需要增加一个计数器,并对每个 ) 减少一个计数器。当你的计数器为0且你找到一个 ) 时,那就是你函数参数块的结尾,然后你可以解析括号之间的文本。当计数器为0时,你也可以在 , 上分割文本以获取函数参数。

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

然后,你需要取出开头和结尾括号之间以及逗号之间的文本块,并对每个参数重复上述步骤。


0

有一些相对较新的针对特定语言的 正则表达式增强功能,使得可以使用“正则表达式”匹配上下文无关语言,但是当执行这类任务时,更好的选择是使用更常用的工具,因为你将会找到更多资源和更多帮助:

最好使用像 ANTLR、LEX+YACC、FLEX+BISON 或其他常用的解析器生成器。它们中的大多数都附带有构建支持分组和函数调用的简单计算器的完整示例。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接