最近发现一款文法分析神器,看完官网(http://goldparser.org/)的介绍后感觉很犀利的样子,于是就拿来测试了一番,写了一个数学表达式分析的小程序,支持的数学运算符如下所示:
常规运算:+ - * / ^ sqrt sqrt2(a,b) pow2(a) pow(a,b)
三角函数:sin cos tan cot asin acos atan acot
指数对数:log2(a) log10(a) ln(a) logn(a,b) e^
最大最小:max(a,b,...) min(a,b,...)
一、 GOLD Parser简介
GOLD Parser是一款强大的文法分析工具,支持c++, c, c#, Java, Python, Pascal等多种语言,详细信息请参见官网 http://goldparser.org/
使用该工具主要包括三个步骤:
- 编写我们要解析的语言的语法描述(采用GOLD Meta-Language编写)
- 使用GOLD Builder工具编译我们的语法文件,生成egt格式的表文件,该文件中存储了编译完成的文法表,供后面解析引擎使用
- 选择一种我们熟悉的编程语言,下载对应的与文法表解析引擎,然后在我们的程序中调用该引擎对我们需要解析的语言进行解析即可
二、 数学表达式语法定义
从官网上下载GOLD Parser Builder Tool,按照提示进行安装,安装完成后就可以编写语法定义了。主界面如下,工具中附带的测试工具十分强大,写完语法定义后,可以直接对语法进行测试,生成语法树状图。
在编写语法描述之前,首先我们先熟悉一下GOLD Meta-Language的基本特性,该语言主要由以下几部分组成:
1. 语法文件属性描述
这部分是用来描述我们即将编写的语法文件的相关信息的,如语法名称、作者、版本号等等。格式如下:
"Name" = ‘My Programming Language‘
"Version" = ‘1.0 beta‘
"Author" = ‘John Q. Public‘
"Start Symbol" = <Statement> //必不可少,表示定义的开始,上面的可以不写
2. 字符集定义
这部分是用来描述我们语言中所要用到的字符集,GOLD Meta-Language中预先定义了很多字符集,如常用的数字集合{Number}、字母集合{Letter}、可打印字符集合{Printable}等等,也可以使用Unicode码指定字符集范围{&4F00..&99E0},表示从4F00到99E0之间的所有字符。格式如下:
{String Char} = {Printable} – [”] //表示从可打印的字符中减去”字符
我们可以定义多个字符集供我们定义的语言使用
3. 终结符(Terminal)定义
终结符是指我们定义的语言中能被语法分析器识别的最小单元,举例说明一下,比如下面一个数学表达式:3.3+sin(a+b1),终结符为“3.3”“+”“sin”“(”“a”“b1”“)”,终结符通常是采用正则表达式定义的,如果我们对正则表达式不了解,那么强烈建议我们去补补正则表达式的相关知识了。在语法文件中,变量及数字的终结符采用如下方式定义
Variable = {Letter}{Number}* //表示一个字母后面跟0个或多个数字,如a,b,x1,y34
NumberValue = {Number}+ | ({Number}+‘.‘{Number}*) //表示整数或小数
4. Productions定义(这个不好翻译o(╯□╰)o,就用英文表示吧)
我们所描述的语言的语法是由一系列Production定义的,而一个Production是由若干个终结符(Terminal)和非终结符(Nonterminal)组成,非终结符通常是由尖括号<>界定,并由若干个终结符及非终节符定义。下图表示的是一个Production,表示语言中的if-then-end语句,其中<Stm>, <Exp>, <Stmts>是非终结符,if, then, end是终结符。
一系列相同类型的Production组成一个规则集(Role),我们所描述的语言的语法就是由规则集定义,下面两幅图两种表示是等价的,是同一个规则集。
在熟悉了GOLD Meta-Language的语法之后,就可以着手编写数学表达式的语法定义了。本人定义的语法文件如下:
! Welcome to GOLD Parser Builder 5.2 "Name" = ‘Calculator‘ "Version" = ‘v1.0‘ "Author" = ‘xxchen‘ "Start Symbol" = <Exp> Variable = {Letter}{Number}* NumberValue = {Number}+ | ({Number}+‘.‘{Number}*) <Exp> ::= <Exp> ‘+‘ <Exp Mult> | <Exp> ‘-‘ <Exp Mult> | <Exp Mult> <Exp Mult> ::= <Exp Mult> ‘*‘ <Value> | <Exp Mult> Variable | <Exp Mult> ‘/‘ <Value> | <Value> <Exp Func> ::= <Exp Func1> | <Exp Func2> | <Exp Funcn> <Exp Func1> ::= ‘sin‘ <Value> | ‘cos‘ <Value> | ‘tan‘ <Value> | ‘cot‘ <Value> | ‘asin‘ <Value> | ‘acos‘ <Value> | ‘atan‘ <Value> | ‘acot‘ <Value> | ‘sqrt‘ <Value> | ‘log2(‘ <Value> ‘)‘ | ‘log10(‘ <Value> ‘)‘ | ‘pow2(‘ <Value> ‘)‘ | ‘e^‘ <Value> | ‘ln‘ <Value> <Exp Func2> ::= <ExpValue> ‘^‘ <Value> | ‘pow(‘ <Exp> ‘,‘ <Exp> ‘)‘ | ‘sqrt2(‘ <Exp> ‘,‘ <Exp> ‘)‘ | ‘logn(‘ <Exp> ‘,‘ <Exp> ‘)‘ <Params> ::= <Params> ‘,‘ <Exp> | <Exp> <Exp Funcn> ::= ‘max(‘ <Params> ‘)‘ | ‘min(‘ <Params> ‘)‘ <Param> ::= NumberValue | Variable <ExpValue> ::= <Param> | ‘-‘ <Param> | ‘(‘ <Exp> ‘)‘ | ‘|‘ <Exp> ‘|‘ <Value> ::= <ExpValue> | <Exp Func>
写完后,直接点软件右下角的Next按钮,在没有提示错误后会生成一个.egt文法表文件,该文件在后面的程序编写过程中需要用到。
三、 利用解析引擎编写代码
由于个人比较熟悉c#语言,故采用了c#语言版本的解析引擎,其它语言版本的引擎在官网上也有提供。在正式编写代码之前,还可以利用Builder Tool来生成对应引擎的解析框架,在Project-Create a Skeleton Program菜单下可以打开向导进行设置,选择对应的语言及解析引擎,就可以生成相应的解析框架了。
自动生成出来的解析框架非常简单,如下所示,主要有两个函数需要注意,第一个是Parse函数,该函数接受一个TextReader类型的参数,用来读取需要解析的内容,里面的解析逻辑都已自动生成;第二个是CreateNewObject函数,我们需要修改的就是这个函数,在引擎解析过程中,我们需要根据每个步骤的解析结果生成我们需要的对象,以实现我们需要的逻辑。在不影响整体框架的前提下,其它部分可以任意修改,在这里我添加了一个带参数的构造函数,参数是文法表文件的路径,然后在构造函数中初始化解析引擎。
为了实现计算逻辑,这里定义了一个简单的表达式类,该类的构造函数可以接受一个常数,或者一个变量,或者接受若干个表达式。
/// <summary> /// 表达式类 /// </summary> public class Expression { /// <summary> /// Initializes a new instance of the <see cref="Expression"/> class. /// </summary> /// <param name="value">接受一个常数</param> public Expression(double value) { _value = t => value; } /// <summary> /// Initializes a new instance of the <see cref="Expression"/> class. /// </summary> /// <param name="variable">接受一个变量</param> public Expression(string variable) { _value = t => t[variable]; _varList = new List<string> { variable }; } /// <summary> /// Initializes a new instance of the <see cref="Expression"/> class. /// </summary> /// <param name="func">表达式计算函数</param> /// <param name="exps">接受若干个表达式</param> public Expression(Func<double[], double> func, params Expression[] exps) { _value = t => func(exps.Select(e => e._value(t)).ToArray()); foreach (var exp in exps) { if(exp._varList == null) continue; if(_varList == null) _varList = new List<string>(); _varList.AddRange(exp._varList); } if (_varList != null) _varList = _varList.Distinct().ToList(); } /// <summary> /// 存储变量名称的链表 /// </summary> private readonly List<string> _varList; /// <summary> /// 获取表达式中的变量 /// </summary> /// <returns></returns> public IEnumerable<string> GetVariables() { if(_varList == null) yield break; foreach (var var in _varList) yield return var; } /// <summary> /// The _value /// </summary> private readonly Func<Dictionary<string, double>, double> _value; /// <summary> /// 获取表达式的值,用于计算没有变量的表达式 /// </summary> /// <returns>System.Double.</returns> public double GetValue() { return GetValue(null); } /// <summary> /// 获取表达式的值,用于计算有变量的表达式 /// </summary> /// <param name="varTable">参数表</param> /// <returns>System.Double.</returns> public double GetValue(Dictionary<string, double> varTable) { try { return _value(varTable); } catch (Exception) { return double.NaN; } } }
再来看一下解析引擎中生成的CreateNewObject函数,下面只截取了部分代码,里面的逻辑也很简单,比如引擎在解析完数字后,可以根据注释,这里是// <Param> ::= NumberValue ,表示r中数据的个数为1,其中r[0].Data对应的就是NumberValue的值,这时我们只需要返回一个常数表达式即可。在解析完变量后,注释的代码是// <Param> ::= Variable,返回一个变量表达式即可。在解析完+号时,对应的注释代码是// <Exp> ::= <Exp> ‘+‘ <Exp Mult> 表明r中数据的个数是3,r[0].Data及r[2].Data是我们之前的数据解析完时返回的表达式,对应于解析树中的<Exp>及<Exp Mult>,r[1].Data是”+”号,故在这个节点我们需要生成一个新的加法表达式,然后返回该表达式即可。
Expression exp1, exp2; switch ((ProductionIndex)r.Parent.TableIndex()) { case ProductionIndex.Exp_Plus: // <Exp> ::= <Exp> ‘+‘ <Exp Mult> exp1 = r[0].Data as Expression; exp2 = r[2].Data as Expression; result = new Expression(t => t[0] + t[1], exp1, exp2); break; case ProductionIndex.Exp_Minus: // <Exp> ::= <Exp> ‘-‘ <Exp Mult> exp1 = r[0].Data as Expression; exp2 = r[2].Data as Expression; result = new Expression(t => t[0] - t[1], exp1, exp2); break; case ProductionIndex.Param_Numbervalue: // <Param> ::= NumberValue result = new Expression(double.Parse(r[0].Data.ToString())); break; case ProductionIndex.Param_Variable: // <Param> ::= Variable result = new Expression(r[0].Data.ToString()); break; ……省略类似部分
至此,数学表达式的解析引擎已经构造完成,使用方法如下:
//根据文发表文件构造解析引擎 var filePath = Path.Combine(Directory.GetCurrentDirectory(), "calculator.egt"); var parser = new CalculatorParser(filePath); //解析读入的字符串 parser.Parse(new StringReader(line)); //读取解析结果,即一个表达式 var exp = parser.Exp; //计算表达式的值 result = exp.GetValue();
四、 实验效果
程序可以计算用户任意输入的表达式,如果发现表达式有误,则会提示用户在哪个位置出现了错误。程序还可以识别变量,并且对数字后面紧接变量的表达方式理解为乘法运算,如3d表示3*d。图中的cos-3-4.d会理解为cos(-3)-4.0xd,其中d为变量
五、 总结
总的来说GOLD Parser是一个非常强大的文法分析工具,可以解析任意有规律的文本文件,如xml, json, html, c, c++, java, c#等等,这些语言的语法描述文件在官网上也都能找得到(不用自己重头再写了)。如果要想解析一门新的语言或者数据描述文件,那么就得自己写语法描述文件,对于语法不是很复杂的语言,在官网上找点资料,然后照着例子写两遍就能搞定了(从刚接触GOLD Parser到完成这个小程序一共花了不到1天时间)。语法写完后,借助现有的解析引擎,程序的编写就非常简单了。
源代码下载地址:http://vdisk.weibo.com/s/yVSnUWjONKKp0
【原创】转载请说明出处!
基于语法分析器GOLD Parser开发的数学表达式计算器