设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间13 现在时间13 缓存数据 到现在一共是295天,有了人生中第一张迷你专辑,我期许自己这不会是句号,只会是个逗号,会一直一直一直突破的,直到我唱不动的那天。

到现在一共是295天,有了人生中第一张迷你专辑,我期许自己这不会是句号,只会是个逗号,会一直一直一直突破的,直到我唱不动的那天。 -- 一种原谅

查看: 296|回复: 0

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

[复制链接]

  离线 

TA的专栏

  • 打卡等级:无名新人
  • 打卡总天数:1
  • 打卡月天数:0
  • 打卡总奖励:15
  • 最近打卡:2025-02-12 07:08:06
等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
39
主题
35
精华
0
金钱
138
积分
78
注册时间
2023-9-30
最后登录
2025-6-1

发表于 2025-6-1 05:29:17 | 显示全部楼层 |阅读模式

前言

本文将介绍如何在 .NET 中使用 ANTLR4 构建语法分析器。由于篇幅限制,本文不会深入讲解 ANTLR4 的语法规则,相关内容可参考 ANTLR4 的官方文档或其他资料。本文将涵盖以下内容:ANTLR4 的开发环境搭建、语法规则编写、语法分析器生成以及语法分析器的使用。

本文中的例子相对简单,且未经过详细测试,旨在演示 ANTLR4 的基本用法。

实际开发的过程中,建议先去官方的这个 repo 查看是否已经有现成的 grammar 文件可以使用:https://github.com/antlr/grammars-v4

文中的代码示例已上传到 GitHub:
https://github.com/eventhorizon-cli/Antlr4Demo

ANTLR4 简介

ANTLR(Another Tool for Language Recognition)是一个强大的语法分析器生成器,属于编译技术中的前端工具。它可以用来构建语法分析器,并借此开发编译器、解释器和翻译器等。

ANTLR4 是 ANTLR 的最新版本,它支持多种编程语言的语法分析器生成,包括 Java、C#、Python、JavaScript 等。ANTLR4 的语法规则使用一种类似于正则表达式的语法来定义,可以很方便地描述复杂的语法结构。

ANTLR4 的工作流程如下:

  • 编写语法规则:通常使用 ANTLR4 的语法规则文件(.g4 文件)来定义语法规则。
  • 生成语法分析器:使用 ANTLR4 工具来生成目标语言的语法分析器。
  • 使用语法分析器进行语法分析:编写代码来使用生成的语法分析器进行语法分析。分析的结果通常是一个抽象语法树(AST)。
  • 访问 AST:可以使用访问者模式(Visitor Pattern)或者监听器模式(Listener Pattern)来访问 AST,进行后续的处理,例如解释执行、编译等。

语法分析基本概念

语法分析的过程分为两个阶段:词法分析(Lexical Analysis)和语法分析(Syntax Analysis)。

  • 词法分析:将字符聚集为单词或者符号(token),例如将 

    1. 1 + 2
    复制代码
     分解为 
    1. 1
    复制代码
    1. +
    复制代码
    1. 2
    复制代码
     三个 token。

  • 语法分析:输入的 token 被组织成一个树形结构,称为抽象语法树(Abstract Syntax Tree,AST),它表示了输入的语法结构。树的每个节点表示一个语法单元,这个单元的构成规则就叫做语法规则。每个节点还可以有子节点。

例如,表达式 

  1. 1 + 2 * 3
复制代码
 的抽象语法树如下:

  1. +
  2. / \
  3. 1 *
  4. / \
  5. 2 3
复制代码

如何使用 ANTLR4

1. 安装 Antlr4.Runtime.Standard 包

我们以加减乘除四则运算为例来介绍如何使用 ANTLR4 来构建语法分析器。

新建一个C#项目,在项目中添加 

  1. Antlr4.Runtime.Standard
复制代码
 包。

  1. dotnet add package Antlr4.Runtime.Standard
复制代码

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

接着我们需要编写一个 ANTLR4 的语法规则文件,文件的后缀名为 .g4,例如 Arithmetic.g4,文件的内容如下:

  1. grammar Arithmetic; // grammar name 需要和文件名一致
  2. // 语法规则
  3. // op=('*'|'/') 表示 op 将 ‘*' 或者 ‘/' 标记为一个操作符号
  4. // # MulDiv 将这个规则命名为 MulDiv,访问 AST 时会用到
  5. expr: expr op=('*'|'/') expr # MulDiv
  6. | expr op=('+'|'-') expr # AddSub
  7. | INT # Int
  8. | '(' expr ')' # Parens
  9. ;
  10. // 词法规则
  11. INT : [0-9]+ ;
  12. WS : [ \t\r\n]+ -> skip ; // 表示忽略空格
复制代码

1.jpeg

g4 文件的内容分为两部分:词法规则(Lexer Rules) 和 语法规则(Parser Rules)

词法规则是用来定义词法单元的,例如数字、运算符、括号等。词法规则通常以大写字母开头。

语法规则是用来定义语法结构的,例如表达式、语句等。语法规则通常以小写字母开头。

在上面的例子中,我们定义了一个简单的四则运算语法规则,支持加减乘除和括号运算。我们还定义了一个整数类型的词法规则 

  1. INT
复制代码
,表示一个或多个数字。

expr 规则表示一个表达式,用 | 分隔的部分表示或的关系,例如 

  1. expr op=('*'|'/') expr | expr op=('+'|'-') expr
复制代码
 表示一个表达式可以是乘法或除法,也可以是加法或减法。

而加减乘除的优先级通过定义的顺序来决定,乘除法的规则在加减法之前,所以乘除法的优先级高于加减法。

在语法规则中,我们还可以使用 

  1. #
复制代码
 来为规则命名,例如 
  1. # MulDiv
复制代码
,表示这个规则的名字是 
  1. MulDiv
复制代码
。这个名字在访问 AST 时会用到。

规则支持递归定义,例如 

  1. expr: expr op=('*'|'/') expr
复制代码
 。

这边因为举的例子比较简单,可以直接在一个 g4 文件中同时定义语法规则和词法规则。对于复杂的语法规则,可以将语法规则和词法规则分开定义。

在 Rider 或 VS Code 中安装 ANTLR4 的插件,可以检查语法规则的正确性。

2.jpeg

在 Rider 中安装 ANTLR4 的插件后,可以在 g4 文件选中 expr 规则,右键选择 

  1. Test Rule expr
复制代码
 来测试语法规则是否正确。

3.jpeg

左侧的输入框中输入要测试的表达式,右侧的输出框中会以树形结构的方式显示语法分析的结果。

3. 生成语法分析器

ANTLR4 是基于 Java 开发的,所以我们需要安装 Java 运行环境才能使用 ANTLR4 工具来生成语法分析器。

我们有两种方式来使用 ANTLR4 生成语法分析器,优先推荐使用 

  1. Antlr4BuildTasks
复制代码
 项目来自动生成语法分析器。

直接使用 ANTLR4 官方提供的工具来生成语法分析器。

首先,我们需要下载 ANTLR4 工具,可以从 ANTLR4 的官方网站下载:https://www.antlr.org/download.html

写本文时,最新的版本是 4.13.2,下载地址为:
https://www.antlr.org/download/antlr-4.13.2-complete.jar

本文为方便演示,将 antlr-4.13.2-complete.jar 下载到 g4 文件所在的目录下。

4.jpeg

接着就可以使用 Java 运行 ANTLR4 工具来生成语法分析器。

  1. java -jar antlr-4.13.2-complete.jar -Dlanguage=CSharp Arithmetic.g4
复制代码

其中,Arithmetic.g4 是我们编写的语法规则文件,-Dlanguage=CSharp 表示生成 C# 语言的语法分析器。

执行上面的命令后,会生成一些文件,其中包括 

  1. ArithmeticLexer.cs
复制代码
  1. ArithmeticParser.cs
复制代码

5.jpeg

后面我们就可以使用生成的语法分析器来进行语法分析了。

借助 Antlr4BuildTasks 项目自动生成语法分析器。

上面的方式需要手动下载 ANTLR4 工具,然后使用 Java 运行 ANTLR4 工具来生成语法分析器,还会生成一些必须需要添加到项目中的文件。这样的方式比较繁琐,我们可以使用 

  1. Antlr4BuildTasks
复制代码
 项目来自动生成语法分析器。

  1. Antlr4BuildTasks
复制代码
 的 GitHub 地址为:
https://github.com/kaby76/Antlr4BuildTasks

  1. Antlr4BuildTasks
复制代码
 是一个 MSBuild 任务,它可以自动下载 ANTLR4 工具,然后使用 ANTLR4 工具来生成语法分析器,最后将生成的语法分析器添加到项目中。它也会尝试下载 java 运行环境,如果 build 过程中出现错误,可以尝试手动安装全局的 java 运行环境。

除了安装 Antlr4BuildTasks 的包之外,我们还需要在项目文件(.csproj)中添加一些配置,完整 .csproj 文件如下:

  1. <Project Sdk="Microsoft.NET.Sdk">
  2. <PropertyGroup>
  3. <OutputType>Exe</OutputType>
  4. <TargetFramework>net9.0</TargetFramework>
  5. <ImplicitUsings>enable</ImplicitUsings>
  6. <Nullable>enable</Nullable>
  7. </PropertyGroup>
  8. <ItemGroup>
  9. <PackageReference Include="Antlr4.Runtime.Standard" Version="4.13.1"/>
  10. <PackageReference Include="Antlr4BuildTasks" Version="12.8.0" PrivateAssets="all"/>
  11. </ItemGroup>
  12. <ItemGroup>
  13. <Antlr4 Include="**\*.g4"/>
  14. </ItemGroup>
  15. </Project>
复制代码

  1. <Antlr4 Include="**\*.g4"/>
复制代码
 表示将项目中所有的 .g4 文件都添加到 Antlr4 任务中。当然也可以指定具体的 .g4 文件路径。

在 build 项目时,Antlr4BuildTasks 会将 .g4 文件编译成的文件放在 obj 文件夹下,我们可以在 obj 文件夹下找到生成的语法分析器。

6.jpeg

obj 文件夹下的文件是临时文件,会在每次 build 时重新生成,我们不需要将 obj 文件夹下的文件添加到项目中。

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

接下来我们就可以编写代码来使用生成的语法分析器了。

访问 AST 的方式有两种:Visitor和 Listener。我们可以选择其中一种方式来访问 AST。

ANTLR4 会为我们生成一个 ParserParser 在遍历 AST 时会调用 Visitor 的 

  1. VisitXXX
复制代码
 方法,或者 Listener 的 
  1. EnterXXX
复制代码
 和 
  1. ExitXXX
复制代码
 方法。

使用 Visitor 实现

下面我们以访问者模式为例,编写一个简单的 C# 程序来使用语法分析器。

ANTLR4 会为我们生成一个 

  1. ArithmeticBaseVisitor
复制代码
 类,我们可以继承这个类来完成对 AST 的访问。

在前面的 g4 文件中,我们为每个 AST 节点定义了一个名字, 

  1. MulDiv
复制代码
  1. AddSub
复制代码
  1. Int
复制代码
  1. Parens
复制代码
 这些,对应 
  1. ArithmeticBaseVisitor
复制代码
 中的 
  1. VisitMulDiv
复制代码
  1. VisitAddSub
复制代码
  1. VisitInt
复制代码
  1. VisitParens
复制代码
 方法。

我们可以通过重写这些方法来实现对 AST 的访问:

  1. public class ArithmeticVisitor : ArithmeticBaseVisitor<int>
  2. {
  3. // 解析乘除法
  4. public override int VisitMulDiv(ArithmeticParser.MulDivContext context)
  5. {
  6. // context 包含了当前节点的信息
  7. // context.expr(0) 和 context.expr(1) 分别表示乘除法的两个操作数
  8. // 访问子节点,获取操作数的值
  9. int left = Visit(context.expr(0));
  10. int right = Visit(context.expr(1));
  11. return context.op.Text switch
  12. {
  13. "*" => left * right,
  14. "/" => left / right,
  15. _ => throw new NotSupportedException($"Operator {context.op.Text} is not supported.")
  16. };
  17. }
  18. // 解析加减法
  19. public override int VisitAddSub(ArithmeticParser.AddSubContext context)
  20. {
  21. int left = Visit(context.expr(0));
  22. int right = Visit(context.expr(1));
  23. return context.op.Text switch
  24. {
  25. "+" => left + right,
  26. "-" => left - right,
  27. _ => throw new NotSupportedException($"Operator {context.op.Text} is not supported.")
  28. };
  29. }
  30. // 去掉括号,访问括号内的表达式
  31. public override int VisitParens(ArithmeticParser.ParensContext context) => Visit(context.expr());
  32. // 解析整数
  33. public override int VisitInt(ArithmeticParser.IntContext context) => int.Parse(context.INT().GetText());
  34. }
复制代码

定义好了 visitor 之后,我们就可以使用它来解析表达式了。

  1. Console.WriteLine(Evaluate("1 + 2 * 3")); // 7
  2. Console.WriteLine(Evaluate("(1 + 2) * 3")); // 9
  3. int Evaluate(string expression)
  4. {
  5. // 创建词法分析器
  6. var lexer = new ArithmeticLexer(new AntlrInputStream(expression));
  7. var tokens = new CommonTokenStream(lexer);
  8. // 创建语法分析器,传入词法分析器的输出的token流
  9. var parser = new ArithmeticParser(tokens);
  10. // 用 visitor 模式解析表达式
  11. var visitor = new ArithmeticVisitor();
  12. return visitor.Visit(parser.expr());
  13. }
复制代码

使用 Listener 实现

ANTLR4 的 Parser 在遍历 AST 时会调用 Listener 的 

  1. EnterXXX
复制代码
 和 
  1. ExitXXX
复制代码
 方法,我们可以通过重写这些方法来实现对 AST 的访问。

  1. EnterXXX
复制代码
 方法在进入节点时调用,
  1. ExitXXX
复制代码
 方法在离开节点时调用。
我们可以在 
  1. ExitXXX
复制代码
 方法里将操作数压入栈中,下次访问时就可以从栈中弹出操作数进行计算。

  1. public class ArithmeticListener : ArithmeticBaseListener
  2. {
  3. // 使用栈来存储操作数
  4. private readonly Stack<int> _stack = new();
  5. public int Result => _stack.Pop();
  6. public override void ExitMulDiv(ArithmeticParser.MulDivContext context)
  7. {
  8. int right = _stack.Pop();
  9. int left = _stack.Pop();
  10. int result = context.op.Text switch
  11. {
  12. "*" => left * right,
  13. "/" => left / right,
  14. _ => throw new NotSupportedException($"Operator {context.op.Text} is not supported.")
  15. };
  16. _stack.Push(result);
  17. }
  18. public override void ExitAddSub(ArithmeticParser.AddSubContext context)
  19. {
  20. int right = _stack.Pop();
  21. int left = _stack.Pop();
  22. int result = context.op.Text switch
  23. {
  24. "+" => left + right,
  25. "-" => left - right,
  26. _ => throw new NotSupportedException($"Operator {context.op.Text} is not supported.")
  27. };
  28. _stack.Push(result);
  29. }
  30. public override void ExitParens(ArithmeticParser.ParensContext context)
  31. {
  32. // ExitParens 方法在这里不需要做任何操作,因为我们已经在 MulDiv 和 AddSub 中处理了括号内的表达式
  33. }
  34. public override void ExitInt(ArithmeticParser.IntContext context)
  35. {
  36. int value = int.Parse(context.INT().GetText());
  37. _stack.Push(value);
  38. }
  39. }
复制代码
  1. Console.WriteLine(Evaluate("1 + 2 * 3")); // 7
  2. Console.WriteLine(Evaluate("(1 + 2) * 3")); // 9
  3. int Evaluate(string expression)
  4. {
  5. // 创建词法分析器
  6. var lexer = new ArithmeticLexer(new AntlrInputStream(expression));
  7. var tokens = new CommonTokenStream(lexer);
  8. // 创建语法分析器,传入词法分析器的输出的token流
  9. var parser = new ArithmeticParser(tokens);
  10. var listener = new ArithmeticListener();
  11. // 解析表达式
  12. parser.AddParseListener(listener);
  13. parser.expr();
  14. // 获取结果
  15. return listener.Result;
  16. }
复制代码

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

上面的例子中,我们在遍历 AST 时直接计算了表达式的值,这种方式在简单的表达式中是可以的,但如果表达式的处理逻辑比较复杂,更建议将 原始AST 转换成一个我们自定义的 AST,然后在后续的处理逻辑中使用这个自定义的 AST,将解析和处理逻辑分开,可以让代码更清晰,功能也容易实现。

下面我们定义一个比加减乘除法更复杂的需求:指定一个文件夹,用 sql 语句来查询文件夹下的 csv 文件,支持过滤条件、排序等操作。表名是文件名,字段名是 csv 文件的列名。

为简化起见,我们只支持简单的查询语句,支持 

  1. SELECT
复制代码
  1. FROM
复制代码
  1. WHERE
复制代码
  1. ORDER BY
复制代码
 等关键字。数据类型仅用字符串类型做示范,支持的过滤方式有 
  1. =
复制代码
  1. !=
复制代码
  1. LIKE
复制代码
,过滤条件之间只能用 
  1. AND
复制代码
 连接,排序方式支持 
  1. ASC
复制代码
 和 
  1. DESC
复制代码

这里我们将词法规则和语法规则分开定义,词法规则定义在 

  1. SqlLexer.g4
复制代码
 文件中,语法规则定义在 
  1. SqlParser.g4
复制代码
 文件中。

  1. SqlLexer.g4
复制代码
 文件的内容如下:

  1. lexer grammar SqlLexer;
  2. options {
  3. caseInsensitive = true; // 忽略大小写
  4. }
  5. // 关键字
  6. SELECT : 'SELECT' ;
  7. FROM : 'FROM' ;
  8. WHERE : 'WHERE' ;
  9. ORDER : 'ORDER' ;
  10. BY : 'BY' ;
  11. ASC : 'ASC' ;
  12. DESC : 'DESC' ;
  13. AND : 'AND' ;
  14. OR : 'OR' ;
  15. COMMA : ',' ;
  16. STAR : '*' ;
  17. // 运算符
  18. EQ : '=' ;
  19. NEQ : '!=' ;
  20. LIKE : 'LIKE' ;
  21. // 字面量
  22. STRING_LITERAL
  23. : '\'' ( ~('\'' | '\\') | '\\' . )* '\'' // 字符串字面量
  24. ;
  25. // 标识符
  26. IDENTIFIER
  27. : [a-z_][a-z0-9_]* // 用于表名、列名等
  28. ;
  29. WS : [ \t\r\n]+ -> skip ; // 忽略空格
复制代码

  1. SqlParser.g4
复制代码
 文件的内容如下:

  1. parser grammar SqlParser;
  2. options { tokenVocab=SqlLexer; }
  3. query
  4. : SELECT selectList FROM tableName (WHERE whereClause)? (ORDER BY orderByClause)?
  5. ;
  6. selectList
  7. : columnName (COMMA columnName)*
  8. | STAR
  9. ;
  10. columnName: IDENTIFIER ;
  11. tableName: IDENTIFIER ;
  12. whereClause
  13. : whereCondition (AND whereCondition)*
  14. ;
  15. whereCondition
  16. : columnName op=(EQ | NEQ) STRING_LITERAL
  17. | columnName op=LIKE STRING_LITERAL
  18. ;
  19. orderByClause
  20. : orderByCondition (COMMA orderByCondition)*
  21. ;
  22. orderByCondition
  23. : columnName (ASC | DESC)?
  24. ;
复制代码

定义完后可以在 Rider 中使用 ANTLR4 的插件来检查语法规则的正确性,选中 

  1. query
复制代码
 规则,右键选择 
  1. Test Rule query
复制代码
 来测试语法规则是否正确。

7.jpeg

下面我们先定义一组类型用来表示 SQL 语句的 AST:

  1. public abstract class Expression;
  2. public class QueryExpression : Expression
  3. {
  4. public required string TableName { get; init; }
  5. public required bool SelectAll { get; init; }
  6. public required IEnumerable<string> SelectList { get; init; }
  7. public required IEnumerable<WhereCondition> WhereConditions { get; init; }
  8. public required IEnumerable<OrderByCondition> OrderByConditions { get; init; }
  9. }
  10. public class WhereCondition : Expression
  11. {
  12. public required string ColumnName { get; init; }
  13. public WhereConditionOperator Operator { get; init; }
  14. public required string Value { get; init; }
  15. }
  16. public enum WhereConditionOperator
  17. {
  18. Equal,
  19. NotEqual,
  20. StartsWith,
  21. EndsWith,
  22. Contains
  23. }
  24. public class OrderByCondition : Expression
  25. {
  26. public required string ColumnName { get; init; }
  27. public bool IsDescending { get; init; }
  28. }
复制代码

我们定义一个 

  1. SqlAstBuilder
复制代码
 类来实现对 SQL 语句的解析:

  1. public class SqlAstBuilder : SqlParserBaseVisitor<QueryExpression>
  2. {
  3. public override QueryExpression VisitQuery(SqlParser.QueryContext context)
  4. {
  5. var selectList = context.selectList();
  6. bool selectAll = selectList?.STAR() != null;
  7. var columns = selectList?.columnName()
  8. .Select(c => c.GetText())
  9. .ToList() ?? [];
  10. var tableName = context.tableName().GetText();
  11. var whereConditions = context.whereClause()
  12. ?.whereCondition()
  13. .Select(c =>
  14. {
  15. var stringValue = c.STRING_LITERAL().GetText().Trim('\'');
  16. var opText = c.op.Text.ToUpperInvariant();
  17. var op = WhereConditionOperator.Equal;
  18. if (opText == "=")
  19. {
  20. op = WhereConditionOperator.Equal;
  21. }
  22. else if (opText == "!=")
  23. {
  24. op = WhereConditionOperator.NotEqual;
  25. }
  26. else if (opText == "LIKE")
  27. {
  28. if (stringValue.StartsWith("%") && stringValue.EndsWith("%"))
  29. {
  30. op = WhereConditionOperator.Contains;
  31. stringValue = stringValue.Substring(1, stringValue.Length - 2);
  32. }
  33. else if (stringValue.StartsWith("%"))
  34. {
  35. op = WhereConditionOperator.EndsWith;
  36. stringValue = stringValue.Substring(1);
  37. }
  38. else if (stringValue.EndsWith("%"))
  39. {
  40. op = WhereConditionOperator.StartsWith;
  41. stringValue = stringValue.Substring(0, stringValue.Length - 1);
  42. }
  43. }
  44. else
  45. {
  46. throw new NotSupportedException($"Operator {c.op.Text} is not supported.");
  47. }
  48. return new WhereCondition
  49. {
  50. ColumnName = c.columnName().GetText(),
  51. Operator = op,
  52. Value = stringValue
  53. };
  54. })
  55. .ToList() ?? [];
  56. var orderByConditions = context.orderByClause()
  57. ?.orderByCondition()
  58. .Select(c => new OrderByCondition
  59. {
  60. ColumnName = c.columnName().GetText(),
  61. IsDescending = c.DESC() != null
  62. })
  63. .ToList() ?? [];
  64. return new QueryExpression
  65. {
  66. SelectAll = selectAll,
  67. SelectList = columns,
  68. TableName = tableName,
  69. WhereConditions = whereConditions,
  70. OrderByConditions = orderByConditions
  71. };
  72. }
  73. }
复制代码

  1. SqlToCsvEngine
复制代码
 类用来执行 SQL 语句并从 CSV 文件中读取数据:

  1. public class SqlToCsvEngine(DirectoryInfo csvDirectory)
  2. {
  3. public IEnumerable<Dictionary<string, string>> ExecuteQuery(string query)
  4. {
  5. // 创建词法分析器
  6. var lexer = new SqlLexer(new AntlrInputStream(query));
  7. // 创建语法分析器,传入词法分析器的输出的token流
  8. var tokens = new CommonTokenStream(lexer);
  9. var parser = new SqlParser(tokens);
  10. // 将查询语句解析为自定义的 AST
  11. var astBuilder = new SqlAstBuilder();
  12. var expression = astBuilder.Visit(parser.query());
  13. // 处理 AST,执行查询
  14. if (expression is not { } queryExpression)
  15. {
  16. throw new InvalidOperationException("Expected a query expression");
  17. }
  18. // 读取 CSV 文件
  19. var csvData = ReadCsv(queryExpression.TableName);
  20. // 过滤数据
  21. var filteredData = csvData.Where(row =>
  22. {
  23. // 处理 WHERE 条件
  24. // 处理 WHERE 条件
  25. bool isMatch = true;
  26. foreach (var condition in queryExpression.WhereConditions)
  27. {
  28. if (row.TryGetValue(condition.ColumnName, out var value))
  29. {
  30. isMatch = condition.Operator switch
  31. {
  32. WhereConditionOperator.Equal => value == condition.Value,
  33. WhereConditionOperator.NotEqual => value != condition.Value,
  34. WhereConditionOperator.StartsWith => value.StartsWith(condition.Value),
  35. WhereConditionOperator.EndsWith => value.EndsWith(condition.Value),
  36. WhereConditionOperator.Contains => value.Contains(condition.Value),
  37. _ => throw new ArgumentOutOfRangeException()
  38. };
  39. }
  40. else
  41. {
  42. throw new InvalidOperationException($"Column {condition.ColumnName} does not exist in CSV file.");
  43. }
  44. }
  45. return isMatch;
  46. });
  47. // 处理 ORDER BY 条件
  48. foreach (var orderByCondition in queryExpression.OrderByConditions)
  49. {
  50. Func<IEnumerable<Dictionary<string, string>>, Func<Dictionary<string, string>, string>,
  51. IEnumerable<Dictionary<string, string>>> orderByFunc;
  52. if (filteredData is IOrderedEnumerable<Dictionary<string, string>> orderedData)
  53. {
  54. orderByFunc = orderByCondition.IsDescending
  55. ? (_, keySelector) => orderedData.ThenByDescending(keySelector)
  56. : (_, keySelector) => orderedData.ThenBy(keySelector);
  57. }
  58. else
  59. {
  60. orderByFunc = orderByCondition.IsDescending
  61. ? Enumerable.OrderByDescending
  62. : Enumerable.OrderBy;
  63. }
  64. filteredData = orderByFunc(filteredData, row =>
  65. {
  66. if (row.TryGetValue(orderByCondition.ColumnName, out var value))
  67. {
  68. return value;
  69. }
  70. throw new InvalidOperationException(
  71. $"Order by column {orderByCondition.ColumnName} does not exist in CSV file.");
  72. });
  73. }
  74. // 处理 SELECT 条件
  75. if (queryExpression.SelectAll)
  76. {
  77. return filteredData;
  78. }
  79. var selectedData = filteredData.Select(row =>
  80. {
  81. var selectedRow = new Dictionary<string, string>();
  82. foreach (var columnName in queryExpression.SelectList)
  83. {
  84. if (row.TryGetValue(columnName, out var value))
  85. {
  86. selectedRow[columnName] = value;
  87. }
  88. else
  89. {
  90. throw new InvalidOperationException($"Column {columnName} does not exist in CSV file.");
  91. }
  92. }
  93. return selectedRow;
  94. });
  95. return selectedData;
  96. }
  97. private IEnumerable<Dictionary<string, string>> ReadCsv(string tableName)
  98. {
  99. var csvFile = new FileInfo(Path.Combine(csvDirectory.FullName, $"{tableName}.csv"));
  100. if (!csvFile.Exists)
  101. {
  102. throw new FileNotFoundException($"CSV file {csvFile.FullName} does not exist.");
  103. }
  104. using var reader = new StreamReader(csvFile.FullName);
  105. var headerLine = reader.ReadLine();
  106. if (headerLine == null)
  107. {
  108. throw new InvalidOperationException($"CSV file {csvFile.FullName} is empty.");
  109. }
  110. var headers = headerLine.Split(',');
  111. while (!reader.EndOfStream)
  112. {
  113. var line = reader.ReadLine();
  114. if (line == null) continue;
  115. var values = line.Split(',');
  116. yield return headers.Zip(values).ToDictionary(x => x.First, x => x.Second);
  117. }
  118. }
  119. }
复制代码

接下来我们就可以开始测试了:

测试用的 CSV 文件内容如下:

  1. Name,City,Occupation,Company
  2. Alice,New York,Engineer,TechCorp
  3. Bob,Los Angeles,Designer,Creative Inc
  4. Ben,Atlanta,Writer,Publishing House
  5. Charlie,Chicago,Manager,Finance Group
  6. David,Houston,Teacher,School District
  7. Eve,Miami,Student,University
  8. Frank,Seattle,Chef,Restaurant Co
  9. Grace,San Francisco,Doctor,HealthCare
  10. Hannah,Boston,Lawyer,Legal Partners
  11. Ian,Denver,Architect,BuildIt
复制代码
  1. var directory = new DirectoryInfo("/Users/hkh/Desktop/test");
  2. var engine = new SqlToCsvEngine(directory);
  3. var sql =
  4. """
  5. SELECT Name, City, Occupation, Company
  6. FROM Employee
  7. WHERE City != 'Miami'
  8. AND Occupation LIKE '%er'
  9. ORDER BY Name ASC, Company DESC
  10. """;
  11. var result = engine.ExecuteQuery(sql);
  12. // 打印头部
  13. foreach (var column in result.First().Keys)
  14. {
  15. Console.Write($"{column}\t");
  16. }
  17. foreach (var row in result)
  18. {
  19. Console.WriteLine();
  20. foreach (var (_, value) in row)
  21. {
  22. Console.Write($"{value}\t");
  23. }
  24. }
复制代码

输出结果如下:

  1. 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   David   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

到此这篇关于如何在 .NET 中 使用 ANTLR4的文章就介绍到这了,更多相关.NET 使用 ANTLR4内容请搜索晓枫资讯以前的文章或继续浏览下面的相关文章希望大家以后多多支持晓枫资讯!


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表