如何用C#编写解析器?

时间:2011-09-11 09:24:21

标签: c# parsing xml-parsing interpreter

如何在C#中编写Parser(递归下降?)?现在我只想要一个解析算术表达式(并读取变量?)的简单解析器。虽然后来我打算编写一个xml和html解析器(用于学习目的)。我这样做是因为解析器很有用的东西:Web开发,编程语言解释器,内部工具,游戏引擎,地图和平铺编辑​​器等。那么编写解析器的基本理论是什么呢?我该怎么做在C#中实现一个? C#是解析器的正确语言(我曾经在C ++中编写了一个简单的算术解析器并且它很有效.JIT编译是否同样适用?)。任何有用的资源和文章。最重要的是,代码示例(或代码示例的链接)。

注意:出于好奇,回答这个问题的人是否曾在C#中实现过解析器?

7 个答案:

答案 0 :(得分:80)

我在C#中实现了几个解析器 - 手写和工具生成。

关于解析的一个非常好的入门教程是Let's Build a Compiler - 它演示了如何构建递归下降解析器;对于任何有能力的开发人员来说,这些概念很容易从他的语言(我认为是Pascal)转换为C#。这将教你如何使用递归下降解析器,但手动编写完整的编程语言解析器是完全不切实际的。

如果您决定撰写classical recursive descent parserTinyPGCoco/RIrony),则应该查看一些工具来为您生成代码。请记住,现在还有其他方法可以编写解析器,通常效果更好 - 并且定义更简单(例如TDOP parsingMonadic Parsing)。

关于C#是否适合任务的主题 - C#有一些最好的文本库。今天很多解析器(在其他语言中)都有大量的代码来处理Unicode等。我不会对JITted代码做太多评论,因为它可能非常虔诚 - 但是你应该没问题。 IronJS是CLR上解析器/运行时的一个很好的例子(即使它是用F#编写的),它的性能与谷歌V8相差无几。

侧注:与语言解析器相比,标记解析器是完全不同的野兽 - 在大多数情况下,它们是手工编写的 - 并且在扫描器/解析器级别非常简单;它们通常不是递归下降 - 特别是在XML的情况下,如果不编写递归下降解析器(以避免堆栈溢出,并且因为可以在SAX /推模式中使用'平'解析器),则更好。

答案 1 :(得分:17)

Sprache是一个功能强大但轻量级的框架,用于在.NET中编写解析器。还有一个Sprache NuGet package。为了让您了解这里的框架,samples之一可以将一个简单的算术表达式解析为.NET表达式树。我会说很漂亮。

using System;
using System.Linq.Expressions;
using Sprache;

namespace LinqyCalculator
{
    static class ExpressionParser
    {
        public static Expression<Func<decimal>> ParseExpression(string text)
        {
            return Lambda.Parse(text);
        }

        static Parser<ExpressionType> Operator(string op, ExpressionType opType)
        {
            return Parse.String(op).Token().Return(opType);
        }

        static readonly Parser<ExpressionType> Add = Operator("+", ExpressionType.AddChecked);
        static readonly Parser<ExpressionType> Subtract = Operator("-", ExpressionType.SubtractChecked);
        static readonly Parser<ExpressionType> Multiply = Operator("*", ExpressionType.MultiplyChecked);
        static readonly Parser<ExpressionType> Divide = Operator("/", ExpressionType.Divide);

        static readonly Parser<Expression> Constant =
            (from d in Parse.Decimal.Token()
             select (Expression)Expression.Constant(decimal.Parse(d))).Named("number");

        static readonly Parser<Expression> Factor =
            ((from lparen in Parse.Char('(')
              from expr in Parse.Ref(() => Expr)
              from rparen in Parse.Char(')')
              select expr).Named("expression")
             .XOr(Constant)).Token();

        static readonly Parser<Expression> Term = Parse.ChainOperator(Multiply.Or(Divide), Factor, Expression.MakeBinary);

        static readonly Parser<Expression> Expr = Parse.ChainOperator(Add.Or(Subtract), Term, Expression.MakeBinary);

        static readonly Parser<Expression<Func<decimal>>> Lambda =
            Expr.End().Select(body => Expression.Lambda<Func<decimal>>(body));
    }
}

答案 2 :(得分:3)

C#几乎是一种不错的函数式语言,所以实现像Parsec这样的东西并不是什么大问题。以下是如何执行此操作的示例之一:http://jparsec.codehaus.org/NParsec+Tutorial

也可以以非常类似的方式实现基于组合器的Packrat,但这次在某处保持全局解析状态而不是执行纯函数。在我的(非常基本的和临时的)实现中它的速度相当快,但当然像this这样的代码生成器必须表现得更好。

答案 3 :(得分:2)

我知道我有点晚了,但我刚刚发布了一个名为Ve Parser的解析器/语法/ AST生成器库。您可以在http://veparser.codeplex.com找到它,或者通过在Package Manager Console中键入“Install-Package veparser”来添加到您的项目中。该库是一种递归下降解析器,旨在易于使用和灵活。由于您可以使用其源代码,因此您可以从其源代码中学习。我希望它有所帮助。

答案 4 :(得分:1)

在我看来,有一种更好的方法来实现解析器而不是传统的方法,这些方法可以使代码更简单易懂,特别是通过插入一个新的类,可以更容易地扩展您正在解析的语言。非常面向对象的方式。我写的一篇大型系列文章重点介绍了这种解析方法,并为C#2.0解析器提供了完整的源代码: http://www.codeproject.com/Articles/492466/Object-Oriented-Parsing-Breaking-With-Tradition-Pa

答案 5 :(得分:0)

嗯......从这个开始......

首先,写一个解析器,这是一个非常广泛的陈述,尤其是你提出的问题。

你的开场陈述是你想要一个简单的算术“解析器”,技术上不是解析器,它是一个词法分析器,类似于你可能用来创建一种新语言。 (http://en.wikipedia.org/wiki/Lexical_analysis)但我完全理解他们的混淆可能来自哪里。重要的是要注意,如果你要编写语言/脚本解析器,那么词法分析也是你想要理解的,这是严格的解析,因为你正在解释指令而不是使用它们。

回到解析问题......

如果您采用严格定义的文件结构从中提取信息,那么您将会这样做。

一般来说,你真的不需要为XML / HTML编写解析器,因为已经有大量的解析器了,如果解析XML由.NET生成的运行时间更多,那么你就不会甚至需要解析,你只需要“序列化”和“反序列化”。

然而,为了学习,在大多数情况下,解析XML(或类似html之类的东西)非常简单。

如果我们从以下XML开始:

    <movies>
      <movie id="1">
        <name>Tron</name>
      </movie>
      <movie id="2">
        <name>Tron Legacy</name>
      </movie>
    <movies>

我们可以按如下方式将数据加载到XElement中:

    XElement myXML = XElement.Load("mymovies.xml");

然后,您可以使用'myXML.Root'

来获取'电影'根元素

然而,有趣的是,您可以轻松地使用Linq来获取嵌套标签:

    var myElements = from p in myXML.Root.Elements("movie")
                     select p;

将为您提供一系列XElements,每个XElements包含一个'...',您可以使用以下内容获取:

    foreach(var v in myElements)
    {
      Console.WriteLine(string.Format("ID {0} = {1}",(int)v.Attributes["id"],(string)v.Element("movie"));
    }

对于除数据结构之类的XML以外的任何其他内容,我恐怕你必须开始学习正则表达式的艺术,像“正则表达式教练”这样的工具会帮助你(http://weitz.de/regex-coach/)或者是最新的类似工具之一。

您还需要熟悉.NET正则表达式对象,(http://www.codeproject.com/KB/dotnet/regextutorial.aspx)应该为您提供良好的开端。

一旦你知道你的注册资料如何运作,那么在大多数情况下,这是一个简单的案例,一次一行地阅读文件,并使用你感觉舒服的方法来理解它们。

您可以在(http://www.wotsit.org/

找到几乎所有可想象的文件格式的免费文件格式

答案 6 :(得分:0)

对于记录,我在C#中实现了解析器生成器,因为我找不到任何正常工作或与YACC类似的工作(参见:http://sourceforge.net/projects/naivelangtools/)。

然而,经过一些ANTLR的经验,我决定选择LALR而不是LL。我知道理论上LL更容易实现(生成器或解析器)但我只是不能用表达式来表达运算符的优先级(比如*在“{+ 1}}之前”2 + 5 * 3 “)。在LL中,你说mult_expr嵌入在add_expr中,这对我来说似乎并不自然。