在C#中使用正则表达式

摘要:描述正则表达式在项目中的实际应用,介绍如何利用它来解析字符串。

 

一、一点背景

谌总和老谭这两个人,有不少相似的地方。比如说,在软件设计和实现的时候,都希望系统的逻辑能清晰地呈现出来,也就是说,使软件具备清晰的结构。

但这一次,老谭走向了自己的反面。

讨论的是公式的管理问题。项目中的节点量、指标值等数据的计算,都会用到公式。公式中包含四则运算,也可以用括弧提高运算的优先级,参加运算的对象都是系统中的其他数值,如罐量、仪表量之类。

谌总建议将公式的结构保存在数据库中,一个公式用到若干记录和字段,这样便于了解相互之间的引用关系。老谭则建议干脆一个公式一个字符串,用名字标识要引用的对象。由于老谭是具体干活的,谌总也乐于这样。

 于是,定义了公式的格式,如下面的例子所示:

($B'1车间一号表'+10-$G'一罐区二号罐'+$J'N-213号节点')*5

其中 $ 是引导符,B、G、J是对象类型,分别表示仪表、罐、节点(用汉语拼音总是觉得有点土,是么?),后面跟上用引号引起来的对象名称。

计算公式值的思路是,将公式中的对象找出来,用对象的值替换后,得到一个纯数字的计算公式,再找一个工具来计算这个公式的值。

 

二、最初的做法

现在的问题是怎么解析公式,找出对象,再用对象的值替换公式的内容。

这活儿交给了玉龙,他最初采用的是传统的切割字符串的做法:

    private double EvaluateFormula(string formula)
    {
        foreach (var items in formula.Split('+'))
        {
            foreach (var item in items.Split('-'))
            {
                if (item.Trim()[0] != '$')
                {
                    continue;
                }

                var name = item.Replace("$", string.Empty);
                 
                string[] s = name.Split('\'');
                switch (s[0])
                {
                    case "b":
                        formula = formula.Replace(item, GetMeterValue(s[1]));
                        break;
                    case "g":
                        formula = formula.Replace(item, GetTankValue(s[1]));
                        break;
                    case "j":
                        formula = formula.Replace(item, GetNodeValue(s[1]));
                        break;
                }
            }
        }

        return EvaluateExpression(formula);
    }

 

实现的思路是,用运算符(加号和减号)切分字符串,在切分结果中去掉引导符($)得到所引用的对象的类型和名称,据此获得对象的值。最后,通过调用第三方的函数计算替换后的表达式的值。

这种方案的缺陷是明显的。首先,只考虑了部分运算符(加号和减号),其次,没有考虑括弧;再次,没有考虑对象名称中有运算符的问题;……设想一下处理了这些问题,实现会变得比这个复杂得多。

老谭建议用正则表达式解析公式。

 

 三、利用正则表达式

利用正则表达式,可以从公式中提取所关注的对象,称参与运算的对象为公式项。获取公式项的值后,用它替换公式中的内容。为此,首先要确定公式项的模式(Pattern)。

3.1 公式项的模式

在前面所给出的公式例子(($B'1车间一号表'+10-$G'一罐区二号罐'+$J'N-213号节点')*5))中,有三个运算对象,即三个公式项,它们的模式为:

\$[B|G|J]'.+'

其中,\$ 是原来的引导符,由于$是正则表达式中的保留字,需要转义;[B|G|J]表示B、G、J三者之一,.+表示一个或多个任意字符。

由于我们要提取公式项中的内容,需要建立分组,方法同上一篇《在VS 2012中使用正则表达式 》介绍的一样,用括号将要提取的内容括起来。我们要提取的内容包括两方面:对象类型以及对象名称:

\$([B|G|J])'(.+)'

使用正则表达式时要注意一个很隐蔽的问题。如果用上面的模式去匹配公式例子,我们本希望能匹配成功三次,匹配上的字符串分别是三个公式项,即:

$B'1车间一号表'

$G'一罐区二号罐'

$J'N-213号节点'

但实际上只匹配成功一次,匹配上的字符串是:

$B'1车间一号表'+10-$G'一罐区二号罐'+$J'N-213号节点'

其中,\$([B|G|J]) 匹配到$B,这没有问题。但 (.+) 匹配到 1车间一号表'+10-$G'一罐区二号罐'+$J'N-213号节点。出现这个问题的原因,是正则表达式用了贪婪匹配算法,它总是设法匹配到最多的内容。阻止贪婪匹配的方法是在分组中加上问号运算符:

\$([B|G|J])'(.+?)'

3.2 实现公式解析

利用正则表达式求解公式的代码如下:

    
    private const string FormulaItemPattern = @"\$([B|G|J])'(.+?)'";
 
    private double EvaluateFormula(string formula)
    {
        foreach (Match match in new Regex(FormulaItemPattern).Matches(formula))
        {
            var item = match.Groups[0].ToString();
            var type = match.Groups[1].ToString();
            var name = match.Groups[2].ToString();

            switch (type.ToLower())
            {
                case "b":
                    formula = formula.Replace(item, GetMeterValue(name));
                    break;
                case "g":
                    formula = formula.Replace(item, GetTankValue(name));
                    break;
                case "j":
                    formula = formula.Replace(item, GetNodeValue(name));
                    break;
            }
        }

        return EvaluateExpression(formula);
    }

首先根据模式创建正则表达式,即Regex对象,利用该对象的Matches方法获取公式中符合该模式的所有匹配(顺便鄙视一下.NET的命名问题,这时候你把Match当作名词还是动词用呀?)。对于每个匹配,获取我们所需要的信息,将公式项的值计算出来,用值计算替换项。

与字符串拆分的方法比起来,使用正则表达式有很多优势。我们只需要关注感兴趣的内容(公式项),把公式中的其他内容排除在代码之外,如不必理睬运算符中是否还包含乘除,是否还包含括弧,等等。这样不仅解决了上段代码中存在的问题,还使得这个版本的代码更简洁。

注意从每次匹配中获取分组中内容的方法。Groups[0]表示整个模式匹配到的内容,即公式项本身,如 $B'1车间一号表'。Groups[i],其中 i 大于0,则表示模式中第 i 个括弧中的内容。为了防止被序号搞晕,还可以对分组命名。

    
    private const string FormulaItemPattern = @"\$(?<type>[B|G|J])'(?<name>.+?)'";
 
    private double EvaluateFormula(string formula)
    {
        foreach (Match match in new Regex(FormulaItemPattern).Matches(formula))
        {

            var item = match.Groups[0].ToString();
            var type = match.Groups["type"].ToString();
            var name = match.Groups["name"].ToString();

            ...
    }

 

 这样修改后,我们过上了幸福的生活,直到有一天……

四、产品组要求修改格式

我们有一个神秘的组织,叫产品组。这天该组织突然发布通知,说 $ 符号已作他用,不能当作本项目的引导符,而应当用 #。而且,类型和对象名称之间应当用半角句号分割开来。原来的公式

($B'1车间一号表'+10-$G'一罐区二号罐'+$J'N-213号节点')*5

应当变成这个样子的:

(#B.'1车间一号表'+10-#G.'一罐区二号罐'+#J.'N-213号节点')*5

不管这样的要求有没有道理,开发方面遵照执行。用了正则表达式,面对这样的修改要求,变得简单了,只需要修改公式项模式即可:

    

    private const string FormulaItemPattern = @"#(?<type>[B|G|J])\.'(?<name>.+?)'";

 

  这样修改后,我们又过上了幸福的生活。

五、总结

实际项目中,关于公式定义的故事还有续集。公式项模式的定义,以及代码处理,也远比这里复杂。但关于正则表达式的使用,核心内容都提到了。

再喊一遍口号吧:使用正则表达式解析字符串,可以使代码编得更为简洁,可以灵活应对情况的变化,可以使程序员的生活不那么苦逼。

六、不算题外话

有一天,玉龙问老谭:“博士,上次您说的我忘了,怎么用正则表达式表示一个或多个字符呀?”

老谭没有回答他的问题,反问要用正则表达式做什么。玉龙说要拆分这样的字符串:ABC/XYZ,得到用斜杠分开的前后两部分。

那用string.Split不是更简单么?玉龙则回答:“是您说的一定要用正则表达式呀?”

老谭真不记得这样说过。

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页