商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > 在.NET中使用ANTLR4构建语法分析器的方法

在.NET中使用ANTLR4构建语法分析器的方法

  发布于2026-07-23 阅读(0)

扫一扫,手机访问

前言

在 .NET 生态中,ANTLR4 是一个相当好用的语法分析器生成工具,能帮你快速搭建编译器、解释器这类东西。这篇内容会从环境搭建、语法规则编写、生成分析器,到实际调用,完整走一遍基本流程。受篇幅限制,ANTLR4 的语法规则不会深入展开,更详细的说明可以去看官方文档或者相关资料。

例子都比较简单,也没有做严格测试,目的只是演示操作路径。实际开发中,有个不错的习惯——先去官方的 grammars-v4 仓库看看是不是已经有现成的 grammar 文件可以用:https://github.com/antlr/grammars-v4

代码示例已经传到了 GitHub:https://github.com/eventhorizon-cli/Antlr4Demo

ANTLR4 简介

ANTLR(全称 Another Tool for Language Recognition)是一个老牌的语法分析器生成器,属于编译技术里的前端工具。用它写编译器、解释器、翻译器都很顺手。

当前版本 ANTLR4 已经支持生成 Ja va、C#、Python、Ja vaScript 等多种语言的语法分析器。它的语法规则定义方式有点类似正则表达式,描述复杂语法结构时非常灵活。

它的工作流程大致分四步:

  • 编写语法规则:通常用 .g4 文件来定义
  • 生成语法分析器:用 ANTLR4 工具输出目标语言的代码
  • 使用分析器进行语法分析:调用生成的代码,得到抽象语法树(AST)
  • 访问 AST:通过 Visitor 模式或 Listener 模式做后续处理,比如解释执行或编译转换

语法分析基本概念

语法分析可以分成两个阶段:词法分析和语法分析。

  • 词法分析:把字符序列拆成 token,比如把 1 + 2 拆成 1+2 三个 token
  • 语法分析:根据语法规则把 token 组织成树形结构——抽象语法树(AST),树上的每个节点代表一个语法单元,比如表达式、语句等

举个例子,表达式 1 + 2 * 3 的 AST 长这样:

  +
 / \
1   *
   / \
  2   3

如何使用 ANTLR4

1. 安装 Antlr4.Runtime.Standard 包

下面用加减乘除四则运算来演示完整流程。先新建一个 C# 项目,然后添加包:

dotnet add package Antlr4.Runtime.Standard

2. 编写 ANTLR4 的语法规则文件

新建一个 .g4 文件,比如 Arithmetic.g4,内容如下:

grammar Arithmetic; // grammar name 需要和文件名一致

// 语法规则
// op=('*'|'/') 表示 op 将 ‘*' 或者 ‘/' 标记为一个操作符号
// # MulDiv 将这个规则命名为 MulDiv,访问 AST 时会用到
expr:
   expr op=('*'|'/') expr   # MulDiv
    |   expr op=('+'|'-') expr   # AddSub
    |   INT                      # Int
    |   '(' expr ')'             # Parens
    ;

// 词法规则
INT :   [0-9]+ ;
WS  :   [ \t\r\n]+ -> skip ; // 表示忽略空格

在.NET中使用ANTLR4构建语法分析器的方法

.g4 文件由两部分组成:词法规则(通常大写开头)和语法规则(通常小写开头)。词法规则定义 token,比如数字、运算符;语法规则定义语法结构,比如表达式、语句。

上面的例子定义了一个简单的四则运算语法,支持加减乘除和括号。整数 token 用 INT 表示(一个或多个数字)。expr 规则中用 | 分隔可选分支,乘除法写在加减法前面,这样乘除法优先级更高。每个分支可以用 # 命名,比如 # MulDiv,在 Visitor 访问时会用到这个名字。规则支持递归,比如乘法规则里又引用了 expr

这个例子比较简单,所以词法和语法写在一个文件里。复杂项目可以把它们分开。

安装 ANTLR4 的插件(Rider 或 VS Code)后,可以检查语法规则是否正确。在 Rider 中选中 expr 规则,右键选择 Test Rule expr,输入测试表达式就能看到树形结构的结果。

在.NET中使用ANTLR4构建语法分析器的方法

在.NET中使用ANTLR4构建语法分析器的方法

3. 生成语法分析器

ANTLR4 本身基于 Ja va,所以需要安装 Ja va 运行环境才能使用它的工具。有两种方式生成分析器,推荐使用 Antlr4BuildTasks 自动生成。

方式一:直接使用 ANTLR4 官方工具

从官网下载 jar 包(本文写作时最新版 4.13.2):

https://www.antlr.org/download/antlr-4.13.2-complete.jar

下载后放到 .g4 文件所在目录,然后运行:

ja va -jar antlr-4.13.2-complete.jar -Dlanguage=CSharp Arithmetic.g4

-Dlanguage=CSharp 表示生成 C# 语言的代码。执行后会生成 ArithmeticLexer.csArithmeticParser.cs 等文件,然后就可以在项目里使用了。

在.NET中使用ANTLR4构建语法分析器的方法

在.NET中使用ANTLR4构建语法分析器的方法

方式二:借助 Antlr4BuildTasks 自动生成

手动方式比较繁琐,推荐使用 Antlr4BuildTasks 这个 MSBuild 任务,它可以自动下载 ANTLR4 工具、生成分析器代码,并添加到项目中。它也会尝试下载 Ja va 运行环境,如果构建出错,可以手动安装全局 Ja va。

项目文件(.csproj)配置如下:


    
        Exe
        net9.0
        enable
        enable
    

    
        
        
    

    
        
    

表示项目里所有 .g4 文件都会参与编译。构建之后,生成的代码会放在 obj 文件夹中,每次构建都会重新生成,不需要手动添加。

在.NET中使用ANTLR4构建语法分析器的方法

4. 编写代码来使用语法分析器

访问 AST 的方式有两种:VisitorListener。ANTLR4 会为每个 .g4 文件生成对应的 BaseVisitor 和 BaseListener,咱们只需要继承并重写相关方法即可。

使用 Visitor 实现

继续用四则运算的例子。ANTLR4 为我们生成了一个 ArithmeticBaseVisitor 泛型基类,对应 .g4 中命名的规则(MulDiv、AddSub、Int、Parens),基类中有 VisitMulDivVisitAddSub 等方法。我们写一个自定义 Visitor 重写它们:

public class ArithmeticVisitor : ArithmeticBaseVisitor
{
    // 解析乘除法
    public override int VisitMulDiv(ArithmeticParser.MulDivContext context)
    {
        int left = Visit(context.expr(0));
        int right = Visit(context.expr(1));
        return context.op.Text switch
        {
            "*" => left * right,
            "/" => left / right,
            _ => throw new NotSupportedException($"Operator {context.op.Text} is not supported.")
        };
    }

    // 解析加减法
    public override int VisitAddSub(ArithmeticParser.AddSubContext context)
    {
        int left = Visit(context.expr(0));
        int right = Visit(context.expr(1));
        return context.op.Text switch
        {
            "+" => left + right,
            "-" => left - right,
            _ => throw new NotSupportedException($"Operator {context.op.Text} is not supported.")
        };
    }

    // 去掉括号,访问括号内的表达式
    public override int VisitParens(ArithmeticParser.ParensContext context) => Visit(context.expr());

    // 解析整数
    public override int VisitInt(ArithmeticParser.IntContext context) => int.Parse(context.INT().GetText());
}

然后写一个 Evaluate 方法,把字符串表达式传进去:

Console.WriteLine(Evaluate("1 + 2 * 3")); // 7
Console.WriteLine(Evaluate("(1 + 2) * 3")); // 9

int Evaluate(string expression)
{
    var lexer = new ArithmeticLexer(new AntlrInputStream(expression));
    var tokens = new CommonTokenStream(lexer);
    var parser = new ArithmeticParser(tokens);
    var visitor = new ArithmeticVisitor();
    return visitor.Visit(parser.expr());
}

使用 Listener 实现

Listener 模式通过 EnterXXXExitXXX 方法触发,我们可以在 ExitXXX 里把中间结果压入栈,最后弹出结果。

public class ArithmeticListener : ArithmeticBaseListener
{
    private readonly Stack _stack = new();
    public int Result => _stack.Pop();

    public override void ExitMulDiv(ArithmeticParser.MulDivContext context)
    {
        int right = _stack.Pop();
        int left = _stack.Pop();
        int result = context.op.Text switch
        {
            "*" => left * right,
            "/" => left / right,
            _ => throw new NotSupportedException($"Operator {context.op.Text} is not supported.")
        };
        _stack.Push(result);
    }

    public override void ExitAddSub(ArithmeticParser.AddSubContext context)
    {
        int right = _stack.Pop();
        int left = _stack.Pop();
        int result = context.op.Text switch
        {
            "+" => left + right,
            "-" => left - right,
            _ => throw new NotSupportedException($"Operator {context.op.Text} is not supported.")
        };
        _stack.Push(result);
    }

    public override void ExitInt(ArithmeticParser.IntContext context)
    {
        int value = int.Parse(context.INT().GetText());
        _stack.Push(value);
    }
}

使用方式类似:

Console.WriteLine(Evaluate("1 + 2 * 3")); // 7
Console.WriteLine(Evaluate("(1 + 2) * 3")); // 9

int Evaluate(string expression)
{
    var lexer = new ArithmeticLexer(new AntlrInputStream(expression));
    var tokens = new CommonTokenStream(lexer);
    var parser = new ArithmeticParser(tokens);
    var listener = new ArithmeticListener();
    parser.AddParseListener(listener);
    parser.expr();
    return listener.Result;
}

构建自定义 AST 以解决复杂问题

上面的例子直接在遍历时计算了值,对于简单场景够用了。但如果处理逻辑比较复杂,更好的做法是把 ANTLR4 生成的原始 AST 转换成一个自定义的 AST 结构,把解析逻辑和执行逻辑拆开,代码会更清晰,扩展起来也更方便。

下面看一个更接近实际的需求:指定一个文件夹,用 SQL 语句查询文件夹下的 CSV 文件,支持过滤、排序等。为了简化,只支持 SELECTFROMWHEREORDER BY,数据类型全当字符串处理,过滤支持 =!=LIKE,条件只能用 AND 连接,排序支持 ASCDESC

这次把词法和语法规则分开成两个文件:SqlLexer.g4SqlParser.g4

SqlLexer.g4

lexer grammar SqlLexer;

options { caseInsensitive = true; }

SELECT : 'SELECT' ;
FROM   : 'FROM' ;
WHERE  : 'WHERE' ;
ORDER  : 'ORDER' ;
BY     : 'BY'  ;
ASC    : 'ASC' ;
DESC   : 'DESC' ;
AND    : 'AND' ;
OR     : 'OR' ;
COMMA  : ',' ;
STAR   : '*'  ;

EQ     : '=' ;
NEQ    : '!=' ;
LIKE   : 'LIKE' ;

STRING_LITERAL : '\'' ( ~('\'' | '\\') | '\\' . )* '\'' ;
IDENTIFIER     : [a-z_][a-z0-9_]* ;
WS             : [ \t\r\n]+ -> skip ;

SqlParser.g4

parser grammar SqlParser;

options { tokenVocab=SqlLexer; }

query : SELECT selectList FROM tableName (WHERE whereClause)? (ORDER BY orderByClause)? ;

selectList : columnName (COMMA columnName)* | STAR ;

columnName: IDENTIFIER ;
tableName: IDENTIFIER ;

whereClause : whereCondition (AND whereCondition)* ;

whereCondition
    : columnName op=(EQ | NEQ) STRING_LITERAL
    | columnName op=LIKE STRING_LITERAL
    ;

orderByClause : orderByCondition (COMMA orderByCondition)* ;

orderByCondition : columnName (ASC | DESC)? ;

写完后可以在 Rider 里用插件测试 query 规则,验证语法正确性。

在.NET中使用ANTLR4构建语法分析器的方法

接下来定义一组类型来表示 SQL 的自定义 AST:

public abstract class Expression;

public class QueryExpression : Expression
{
    public required string TableName { get; init; }
    public required bool SelectAll { get; init; }
    public required IEnumerable SelectList { get; init; }
    public required IEnumerable WhereConditions { get; init; }
    public required IEnumerable OrderByConditions { get; init; }
}

public class WhereCondition : Expression
{
    public required string ColumnName { get; init; }
    public WhereConditionOperator Operator { get; init; }
    public required string Value { get; init; }
}

public enum WhereConditionOperator
{
    Equal, NotEqual, StartsWith, EndsWith, Contains
}

public class OrderByCondition : Expression
{
    public required string ColumnName { get; init; }
    public bool IsDescending { get; init; }
}

写一个 SqlAstBuilder 类(继承 SqlParserBaseVisitor)把原始 AST 转换成自定义结构:

public class SqlAstBuilder : SqlParserBaseVisitor
{
    public override QueryExpression VisitQuery(SqlParser.QueryContext context)
    {
        var selectList = context.selectList();
        bool selectAll = selectList?.STAR() != null;
        var columns = selectList?.columnName()
            .Select(c => c.GetText())
            .ToList() ?? [];

        var tableName = context.tableName().GetText();

        var whereConditions = context.whereClause()
            ?.whereCondition()
            .Select(c =>
            {
                var stringValue = c.STRING_LITERAL().GetText().Trim('\'');
                var opText = c.op.Text.ToUpperInvariant();
                var op = WhereConditionOperator.Equal;
                if (opText == "=") op = WhereConditionOperator.Equal;
                else if (opText == "!=") op = WhereConditionOperator.NotEqual;
                else if (opText == "LIKE")
                {
                    if (stringValue.StartsWith("%") && stringValue.EndsWith("%"))
                    {
                        op = WhereConditionOperator.Contains;
                        stringValue = stringValue.Substring(1, stringValue.Length - 2);
                    }
                    else if (stringValue.StartsWith("%"))
                    {
                        op = WhereConditionOperator.EndsWith;
                        stringValue = stringValue.Substring(1);
                    }
                    else if (stringValue.EndsWith("%"))
                    {
                        op = WhereConditionOperator.StartsWith;
                        stringValue = stringValue.Substring(0, stringValue.Length - 1);
                    }
                }
                else throw new NotSupportedException($"Operator {c.op.Text} is not supported.");

                return new WhereCondition
                {
                    ColumnName = c.columnName().GetText(),
                    Operator = op,
                    Value = stringValue
                };
            })
            .ToList() ?? [];

        var orderByConditions = context.orderByClause()
            ?.orderByCondition()
            .Select(c => new OrderByCondition
            {
                ColumnName = c.columnName().GetText(),
                IsDescending = c.DESC() != null
            })
            .ToList() ?? [];

        return new QueryExpression
        {
            SelectAll = selectAll,
            SelectList = columns,
            TableName = tableName,
            WhereConditions = whereConditions,
            OrderByConditions = orderByConditions
        };
    }
}

最后实现 SqlToCsvEngine 类,负责解析 SQL、读取 CSV、过滤排序并返回结果:

public class SqlToCsvEngine(DirectoryInfo csvDirectory)
{
    public IEnumerable> ExecuteQuery(string query)
    {
        var lexer = new SqlLexer(new AntlrInputStream(query));
        var tokens = new CommonTokenStream(lexer);
        var parser = new SqlParser(tokens);
        var astBuilder = new SqlAstBuilder();
        var expression = astBuilder.Visit(parser.query());

        var csvData = ReadCsv(expression.TableName);

        // 过滤
        var filteredData = csvData.Where(row =>
        {
            bool isMatch = true;
            foreach (var condition in expression.WhereConditions)
            {
                if (row.TryGetValue(condition.ColumnName, out var value))
                {
                    isMatch = condition.Operator switch
                    {
                        WhereConditionOperator.Equal => value == condition.Value,
                        WhereConditionOperator.NotEqual => value != condition.Value,
                        WhereConditionOperator.StartsWith => value.StartsWith(condition.Value),
                        WhereConditionOperator.EndsWith => value.EndsWith(condition.Value),
                        WhereConditionOperator.Contains => value.Contains(condition.Value),
                        _ => throw new ArgumentOutOfRangeException()
                    };
                }
                else throw new InvalidOperationException($"Column {condition.ColumnName} does not exist in CSV file.");
            }
            return isMatch;
        });

        // 排序
        foreach (var orderByCondition in expression.OrderByConditions)
        {
            Func>, Func, string>,
                IEnumerable>> orderByFunc;
            if (filteredData is IOrderedEnumerable> orderedData)
            {
                orderByFunc = orderByCondition.IsDescending
                    ? (_, keySelector) => orderedData.ThenByDescending(keySelector)
                    : (_, keySelector) => orderedData.ThenBy(keySelector);
            }
            else
            {
                orderByFunc = orderByCondition.IsDescending
                    ? Enumerable.OrderByDescending
                    : Enumerable.OrderBy;
            }
            filteredData = orderByFunc(filteredData, row =>
            {
                if (row.TryGetValue(orderByCondition.ColumnName, out var value)) return value;
                throw new InvalidOperationException($"Order by column {orderByCondition.ColumnName} does not exist in CSV file.");
            });
        }

        // 投影
        if (expression.SelectAll) return filteredData;

        var selectedData = filteredData.Select(row =>
        {
            var selectedRow = new Dictionary();
            foreach (var columnName in expression.SelectList)
            {
                if (row.TryGetValue(columnName, out var value)) selectedRow[columnName] = value;
                else throw new InvalidOperationException($"Column {columnName} does not exist in CSV file.");
            }
            return selectedRow;
        });
        return selectedData;
    }

    private IEnumerable> ReadCsv(string tableName)
    {
        var csvFile = new FileInfo(Path.Combine(csvDirectory.FullName, $"{tableName}.csv"));
        if (!csvFile.Exists) throw new FileNotFoundException($"CSV file {csvFile.FullName} does not exist.");
        using var reader = new StreamReader(csvFile.FullName);
        var headerLine = reader.ReadLine();
        if (headerLine == null) throw new InvalidOperationException($"CSV file {csvFile.FullName} is empty.");
        var headers = headerLine.Split(',');
        while (!reader.EndOfStream)
        {
            var line = reader.ReadLine();
            if (line == null) continue;
            var values = line.Split(',');
            yield return headers.Zip(values).ToDictionary(x => x.First, x => x.Second);
        }
    }
}

测试用的 CSV 文件内容:

Name,City,Occupation,Company
Alice,New York,Engineer,TechCorp
Bob,Los Angeles,Designer,Creative Inc
Ben,Atlanta,Writer,Publishing House
Charlie,Chicago,Manager,Finance Group
Da vid,Houston,Teacher,School District
Eve,Miami,Student,University
Frank,Seattle,Chef,Restaurant Co
Grace,San Francisco,Doctor,HealthCare
Hannah,Boston,Lawyer,Legal Partners
Ian,Denver,Architect,BuildIt

执行查询:

var directory = new DirectoryInfo("/Users/hkh/Desktop/test");
var engine = new SqlToCsvEngine(directory);
var sql =  """
  SELECT Name, City, Occupation, Company
  FROM Employee
  WHERE City != 'Miami'
    AND Occupation LIKE '%er'
  ORDER BY Name ASC, Company DESC
  """;
var result = engine.ExecuteQuery(sql);

// 打印头部
foreach (var column in result.First().Keys)
{
    Console.Write($"{column}\t");
}
foreach (var row in result)
{
    Console.WriteLine();
    foreach (var (_, value) in row)
    {
        Console.Write($"{value}\t");
    }
}

输出结果:

Name City Occupation Company

Alice New York Engineer TechCorp

Ben Atlanta Writer Publishing House

Bob Los Angeles Designer Creative Inc

Charlie Chicago Manager Finance Group

Da vid Houston Teacher School District

Hannah Boston Lawyer Legal Partners

参考资料

https://github.com/antlr/grammars-v4

https://wizardforcel.gitbooks.io/antlr4-short-course/content/basic-concept.html

https://github.com/antlr/antlr4/blob/master/doc/csharp-target.md

本文转载于:https://www.jb51.net/aspnet/338849ri8.htm 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注