web-dev-qa-db-ja.com

C#でパーサーを記述する方法

C#でパーサー(再帰降下?)を記述する方法を教えてください。今のところは、算術式を解析する(そして変数を読み取る?)シンプルなパーサーが必要です。後で私は(学習目的で)xmlおよびhtmlパーサーを書くつもりです。これは、パーサーが役立つさまざまなものがあるためです:Web開発、プログラミング言語インタープリター、社内ツール、ゲームエンジン、マップエディター、タイルエディターなど。 C#で実装しますか? C#はパーサーに適した言語ですか(私はC++で簡単な算術パーサーを書いたことがありますが、効率的でした。JITコンパイルも同様に良いでしょうか?)。役立つリソースと記事。そして何よりも、コード例(またはコード例へのリンク)。

注:好奇心から、この質問に答える人はC#でパーサーを実装したことがありますか?

61

C#でいくつかのパーサーを実装しました-手書きおよびツール生成。

一般的な解析に関する非常に優れた入門チュートリアルは、 Let's Build a Compiler -再帰降下パーサーの構築方法を示しています。また、有能な開発者のために、コンセプトは彼の言語(Pascalだったと思います)からC#に簡単に翻訳されます。これにより、再帰降下型パーサーの仕組みがわかりますが、完全なプログラミング言語パーサーを手動で記述することは完全に非現実的です。

いくつかのツールを調べてコードを生成する必要があります- classical recursive descent parserTinyPGCoco/R =、 Irony )。現在、パーサーを作成する他の方法があり、通常はパフォーマンスが向上し、定義が簡単であることに注意してください(例 TDOP parsing または Monadic Parsing )。

C#がタスクに対応しているかどうかのトピック-C#には、最高のテキストライブラリがいくつかあります。今日の(他の言語の)パーサーの多くは、Unicodeなどに対処するためにわいせつな量のコードを持っています。JITtedコードは非常に宗教的になる可能性があるため、あまりコメントしません。 IronJS は、CLRのパーサー/ランタイムの良い例であり(F#で記述されている場合でも)、そのパフォーマンスはGoogle V8に恥ずかしがり屋です。

サイドノート:マークアップパーサーは、言語パーサーと比較すると完全に異なる獣です-ほとんどの場合、手で書かれています-そしてスキャナー/パーサーレベルは非常に単純です。通常、再帰降下ではありません-特にXMLの場合は、再帰降下パーサーを記述しない方が適切です(スタックオーバーフローを回避するため、およびSAX /プッシュモードで「フラット」パーサーを使用できるため)。

82

Sprache は、.NETでパーサーを作成するための強力かつ軽量なフレームワークです。 Sprache NuGetパッケージ もあります。ここでフレームワークのアイデアを示すために、単純な算術式を.NET式ツリーに解析できる samples の1つを紹介します。かなり驚くべきことです。

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));
    }
}
17

C#はほぼ適切な関数型言語なので、Parsecのようなものを実装することはそれほど大したことではありません。これを行う方法の例の1つを次に示します。 http://jparsec.codehaus.org/NParsec+Tutorial

コンビネータベースの Packrat を非常によく似た方法で実装することもできますが、今回は純粋な機能を実行する代わりにどこかでグローバルな解析状態を維持します。私の(非常に基本的かつアドホックな)実装では、かなり高速でしたが、もちろん this のようなコードジェネレーターの方がパフォーマンスが良くなければなりません。

3
SK-logic

少し遅れていることは知っていますが、Ve Parserという名前のパーサー/文法/ ASTジェネレーターライブラリを公開しました。 http://veparser.codeplex.com で見つけるか、パッケージマネージャーコンソールで「Install-Package veparser」と入力してプロジェクトに追加します。このライブラリは、使いやすく柔軟なことを目的とした一種の再帰的降下パーサーです。ソースが利用できるので、ソースコードから学ぶことができます。役に立てば幸いです。

2
000

私の意見では、従来の方法よりもパーサーを実装する方が良い方法があり、コードがよりシンプルで理解しやすくなり、特に新しいクラスを非常にオブジェクトにプラグインするだけで、解析する言語を簡単に拡張できます。指向の方法。私が書いたより大きなシリーズの1つの記事は、この解析方法に焦点を当てており、C#2.0パーサーの完全なソースコードが含まれています。 http://www.codeproject.com/Articles/492466/Object-Oriented-Parsing -Breaking-With-Tradition-Pa

1
Ken Beckett

まあ...これから始める場所...

まず、パーサーを作成します。これは、特にあなたの質問に対する非常に広範な声明です。

冒頭の声明では、単純な算術的な「パーサー」が必要であり、厳密にはパーサーではなく、字句アナライザーであり、新しい言語を作成するために使用するものに似ています。 ( http://en.wikipedia.org/wiki/Lexical_analysis )しかし、それらが同じものであるという混乱がどこから来たのかを正確に理解しています。言語/スクリプトパーサーを作成する場合も、字句解析は理解したいものです。これは、命令を使用するのではなく解釈するため、厳密には解析ではありません。

構文解析の質問に戻ります。

これは、厳密に定義されたファイル構造を使用して情報を抽出する場合に行うことです。

一般に、XML/HTMLのパーサーを記述する必要はありません。既に大量に存在しているためです。さらに、.NETランタイムによって生成されたXMLの構文解析を行う場合は、その必要さえありません。解析するには、「シリアル化」と「非シリアル化」が必要です。

ただし、学習のために、ほとんどの場合、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");

その後、 'movies'ルート要素に到達できます 'myXML.Root'

しかし、興味深いことに、Linqを簡単に使用して、ネストされたタグを取得できます。

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

次のようなものを使用して取得できる1つ '...'を含むXElementの変数を提供します。

    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 / )または最新の同様のツールの1つ。

また、.NET正規表現オブジェクトに精通する必要があります( http://www.codeproject.com/KB/dotnet/regextutorial.aspx )は、良いスタートを切るはずです。

Reg-exがどのように機能するかがわかったら、ほとんどの場合、一度に1行ずつファイルを読み取り、どの方法を使用しても快適であると感じる単純なケースです。

想像できるほとんどすべてのファイル形式の無料ソースは、( http://www.wotsit.org/ )にあります。

0
shawty

レコードについては、適切に動作するものやYACCに類似するものが見つからなかったため、C#でパーサージェネレーターを実装しました( http://sourceforge.net/projects/naivelangtools/ を参照)。

しかし、ANTLRでのいくつかの経験の後、LLではなくLALRを使用することにしました。理論的にはLLの方が簡単に実装できること(ジェネレーターまたはパーサー)を知っていますが、演算子の優先順位を表現するだけの式のスタックで生きることはできません(「2 + 5 * 3の*+ ")。 LLでは、mult_exprはadd_exprに埋め込まれていると言いますが、これは私には自然ではないようです。

0
greenoldman