【问题标题】:Regex for matching Functions and Capturing their Arguments用于匹配函数并捕获其参数的正则表达式
【发布时间】:2013-09-19 23:20:44
【问题描述】:

我正在开发一个计算器,它接受字符串表达式并计算它们。我有一个函数,它使用正则表达式搜索数学函数的表达式,检索参数,查找函数名称并对其进行评估。我遇到的问题是,如果我知道会有多少参数,我只能这样做,我无法正确使用正则表达式。如果我只是将( 和) 字符的内容除以, 字符,那么我不能在该参数中调用其他函数。

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

它只适用于一个参数,我可以为每个参数创建一个组,不包括嵌套函数内部的参数吗?例如,它将匹配:func1(2 * 7, func2(3, 5)) 并为:2 * 7 和 func2(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 * 7 和func2(3, 5),但它也必须适用于具有不同参数计数的函数。如果有办法在不使用正则表达式的情况下做到这一点,那也很好。

【问题讨论】:

  • 嵌套元素是Regex 害怕的事情之一。
  • 看来您正在尝试编写一个计算器。你试过 NCalc 吗?
  • 不,但是我已经使用 MuParser 作为引擎制作了一个计算器,现在我正在尝试编写自己的。除了基本的数学解析之外,我正在做所有事情,当它被分解成只有数字和符号时,我将它传递给 ScriptControl 并进行评估。
  • @BrandonMiller,我不知道你是否感兴趣,但我写了一个可能适用于你的程序的小解析器:github.com/DDtMM/DDtMM.SIMPLY。它肯定会处理嵌套表达式并生成语法树......如果你愿意,也许我明天会用它来回答?

标签: c# regex


【解决方案1】:

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

要实现你贴的例子,我建议分两步做,第一步是提取参数(正则表达式在最后解释):

\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(正如我在此编辑之前提到的)。出于我们的目的,平衡组定义允许我们跟踪左括号的实例并减去右括号的实例。本质上,开括号和右括号将在搜索的平衡部分相互抵消,直到找到最终的右括号。也就是说,匹配将继续进行,直到括号平衡并找到最后的右括号。

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

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

总而言之,它以非逗号或方括号的字符开头。然后如果参数中有括号,它匹配并减去括号,直到它们平衡。然后它会尝试重复该匹配,以防参数中有其他函数。然后它进入下一个参数(在逗号之后)。详细:

  • [^,()]+ 匹配任何不是 ',()' 的东西
  • ?: 表示非捕获组,即不将匹配项存储在组中的括号内。
  • \( 表示从左括号开始。
  • ?&gt; 表示 atomic grouping - 本质上,这意味着它不记得回溯位置。这也有助于提高性能,因为尝试不同组合的退步次数更少。
  • [^()]+| 表示除左括号或右括号之外的任何内容。紧随其后的是 | (或)
  • \((?&lt;open&gt;)|这是好东西,说match '(' or
  • (?&lt;-open&gt;) 这是更好的东西,说匹配 ')' 并平衡 '('。这意味着匹配的这一部分(第一个括号之后的所有内容)将继续,直到所有内部括号匹配。没有平衡表达式,匹配将在第一个右括号结束。关键是引擎没有将这个 ')' 与最后的 ')' 匹配,而是从匹配的 '(' 中减去它。当没有进一步的未完成的 ' (',-open 失败,所以最后的 ')' 可以匹配。
  • 正则表达式的其余部分包含组的右括号和重复(、 和 +),它们分别是:重复内括号匹配 0 次或更多次,重复全括号搜索 0或多次(0 允许不带括号的参数)并重复完整匹配 1 次或多次(允许 foo(1)+foo(2))

最后一个点缀:

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

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

如果 open 捕获了某些内容(尚未减去), (?!) 将始终失败,即,如果有一个左括号而没有右括号,它将始终失败。这是测试平衡是否失败的有用方法。

一些注意事项:

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

希望对您有所帮助。

【讨论】:

  • 很好的答案,你一定是大师中的大师。
  • 如何修改它以考虑报价?我的意思是,如果其中一个参数实际上是这样的字符串值:"my, value"。此正则表达式会将my 视为一个参数,将value 视为另一个
  • 如何修改它以忽略字符串参数中的括号和逗号?
【解决方案2】:

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

^(?<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()
函数(123,a>2)

【讨论】:

  • 这个有一个所有参数的匹配组,保留引号!非常感谢朋友!
  • 这很好用,但是,一旦函数变得更复杂,它就会失败.._ func($("g").title() + Sometimes it's easier to have "tock's" rather than "tick's", even if ${func("Variables")} exist in it, or &lt;div style='display:none'&gt;HTML&lt;/div&gt;tags) 在这种情况下,即使它是一个参数,它会将其解析为三个!
【解决方案3】:

我很抱歉打破了 RegEx 的泡沫,但这是仅使用正则表达式无法有效完成的事情之一。

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

使用正确的代码,您可以在整个令牌流上进行迭代,但递归解析器也很常见。无论哪种方式,您都必须能够在每个子表达式入口点((、, 或 &lt;function_name&gt;( 令牌)有效地推送状态并重新开始解析,并将结果推送到子表达式的解析器链上表达式退出点 - ) 或 , 令牌。

【讨论】:

    【解决方案4】:

    正则表达式不会让你完全摆脱这个麻烦......

    由于您有嵌套括号,您需要修改您的代码以计算( 与)。当您遇到( 时,您需要记下位置然后向前看,为您找到的每个extra ( 增加一个计数器,并为您找到的每个) 减少一个计数器。当您的计数器为 0 并且您找到 ) 时,这就是您的函数参数块的结尾,然后您可以解析括号之间的文本。也可以在,,上的文字当计数器为0时得到函数参数。

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

    然后,您获取左括号和右括号之间的文本块和任何逗号,并对每个参数重复上述操作。

    【讨论】:

      【解决方案5】:

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

      最好使用解析器生成器,如 ANTLR、LEX+YACC、FLEX+BISON 或任何其他 commonly used parser generator。它们中的大多数都带有完整的示例,说明如何构建支持分组和函数调用的简单计算器。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-07-14
        • 2021-12-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多