最近发现一款文法分析神器,看完官网(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 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
【原创】转载请说明出处!
新闻热点
疑难解答