在 .NET 中 使用 ANTLR4构建语法分析器的方法
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">前言</a></li><li><a href="#_label1">ANTLR4 简介</a></li><li><a href="#_label2">语法分析基本概念</a></li><li><a href="#_label3">如何使用 ANTLR4</a></li><ul class="second_class_ul"><li><a href="#_lab2_3_0">1. 安装 Antlr4.Runtime.Standard 包</a></li><li><a href="#_lab2_3_1">2. 编写 ANTLR4 的语法规则文件</a></li><li><a href="#_lab2_3_2">3. 生成语法分析器</a></li><li><a href="#_lab2_3_3">直接使用 ANTLR4 官方提供的工具来生成语法分析器。</a></li><li><a href="#_lab2_3_4">借助 Antlr4BuildTasks 项目自动生成语法分析器。</a></li><li><a href="#_lab2_3_5">4. 编写代码来使用语法分析器</a></li><li><a href="#_lab2_3_6">使用 Visitor 实现</a></li><li><a href="#_lab2_3_7">使用 Listener 实现</a></li></ul><li><a href="#_label4">构建自定义 AST 以解决复杂问题</a></li><ul class="second_class_ul"></ul><li><a href="#_label5">参考资料</a></li><ul class="second_class_ul"></ul></ul></div><p class="maodian"><a name="_label0"></a></p><h2>前言</h2><p>本文将介绍如何在 .NET 中使用 ANTLR4 构建语法分析器。由于篇幅限制,本文不会深入讲解 ANTLR4 的语法规则,相关内容可参考 ANTLR4 的官方文档或其他资料。本文将涵盖以下内容:ANTLR4 的开发环境搭建、语法规则编写、语法分析器生成以及语法分析器的使用。</p>
<p>本文中的例子相对简单,且未经过详细测试,旨在演示 ANTLR4 的基本用法。</p>
<p>实际开发的过程中,建议先去官方的这个 repo 查看是否已经有现成的 grammar 文件可以使用:<a href="https://github.com/antlr/grammars-v4" rel="external nofollow"rel="external nofollow" target="_blank">https://github.com/antlr/grammars-v4</a></p>
<p>文中的代码示例已上传到 GitHub:<br /><a href="https://github.com/eventhorizon-cli/Antlr4Demo" rel="external nofollow" target="_blank">https://github.com/eventhorizon-cli/Antlr4Demo</a></p>
<p class="maodian"><a name="_label1"></a></p><h2>ANTLR4 简介</h2>
<p>ANTLR(Another Tool for Language Recognition)是一个强大的语法分析器生成器,属于编译技术中的前端工具。它可以用来构建语法分析器,并借此开发编译器、解释器和翻译器等。</p>
<p>ANTLR4 是 ANTLR 的最新版本,它支持多种编程语言的语法分析器生成,包括 Java、C#、Python、JavaScript 等。ANTLR4 的语法规则使用一种类似于正则表达式的语法来定义,可以很方便地描述复杂的语法结构。</p>
<p>ANTLR4 的工作流程如下:</p>
<ul><li>编写语法规则:通常使用 ANTLR4 的语法规则文件(.g4 文件)来定义语法规则。</li><li>生成语法分析器:使用 ANTLR4 工具来生成目标语言的语法分析器。</li><li>使用语法分析器进行语法分析:编写代码来使用生成的语法分析器进行语法分析。分析的结果通常是一个抽象语法树(AST)。</li><li>访问 AST:可以使用访问者模式(Visitor Pattern)或者监听器模式(Listener Pattern)来访问 AST,进行后续的处理,例如解释执行、编译等。</li></ul>
<p class="maodian"><a name="_label2"></a></p><h2>语法分析基本概念</h2>
<p>语法分析的过程分为两个阶段:词法分析(Lexical Analysis)和语法分析(Syntax Analysis)。</p>
<ul><li><p>词法分析:将字符聚集为单词或者符号(token),例如将 <code>1 + 2</code> 分解为 <code>1</code>、<code>+</code>、<code>2</code> 三个 token。</p></li><li><p>语法分析:输入的 token 被组织成一个树形结构,称为抽象语法树(Abstract Syntax Tree,AST),它表示了输入的语法结构。树的每个节点表示一个语法单元,这个单元的构成规则就叫做语法规则。每个节点还可以有子节点。</p></li></ul>
<p>例如,表达式 <code>1 + 2 * 3</code> 的抽象语法树如下:</p>
<div class="jb51code"><pre class="brush:plain;">+
/ \
1 *
/ \
2 3</pre></div>
<p class="maodian"><a name="_label3"></a></p><h2>如何使用 ANTLR4</h2>
<p class="maodian"><a name="_lab2_3_0"></a></p><h3>1. 安装 Antlr4.Runtime.Standard 包</h3>
<p>我们以加减乘除四则运算为例来介绍如何使用 ANTLR4 来构建语法分析器。</p>
<p>新建一个C#项目,在项目中添加 <code>Antlr4.Runtime.Standard</code> 包。</p>
<div class="jb51code"><pre class="brush:csharp;">dotnet add package Antlr4.Runtime.Standard</pre></div>
<p class="maodian"><a name="_lab2_3_1"></a></p><h3>2. 编写 ANTLR4 的语法规则文件</h3>
<p>接着我们需要编写一个 ANTLR4 的语法规则文件,文件的后缀名为 .g4,例如 Arithmetic.g4,文件的内容如下:</p>
<div class="jb51code"><pre class="brush:csharp;">grammar Arithmetic; // grammar name 需要和文件名一致
// 语法规则
// op=('*'|'/') 表示 op 将 ‘*' 或者 ‘/' 标记为一个操作符号
// # MulDiv 将这个规则命名为 MulDiv,访问 AST 时会用到
expr: expr op=('*'|'/') expr # MulDiv
| expr op=('+'|'-') expr # AddSub
| INT # Int
| '(' expr ')' # Parens
;
// 词法规则
INT : + ;
WS: [ \t\r\n]+ -> skip ; // 表示忽略空格</pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202504/202504020917101.jpg" /></p>
<p>g4 文件的内容分为两部分:<strong>词法规则(Lexer Rules)</strong> 和 <strong>语法规则(Parser Rules)</strong>。</p>
<p><strong>词法规则</strong>是用来定义词法单元的,例如数字、运算符、括号等。词法规则通常以大写字母开头。</p>
<p><strong>语法规则</strong>是用来定义语法结构的,例如表达式、语句等。语法规则通常以小写字母开头。</p>
<p>在上面的例子中,我们定义了一个简单的四则运算语法规则,支持加减乘除和括号运算。我们还定义了一个整数类型的词法规则 <code>INT</code>,表示一个或多个数字。</p>
<p>expr 规则表示一个表达式,用 | 分隔的部分表示或的关系,例如 <code>expr op=('*'|'/') expr | expr op=('+'|'-') expr</code> 表示一个表达式可以是乘法或除法,也可以是加法或减法。</p>
<p>而加减乘除的优先级通过定义的顺序来决定,乘除法的规则在加减法之前,所以乘除法的优先级高于加减法。</p>
<p>在语法规则中,我们还可以使用 <code>#</code> 来为规则命名,例如 <code># MulDiv</code>,表示这个规则的名字是 <code>MulDiv</code>。这个名字在访问 AST 时会用到。</p>
<p>规则支持递归定义,例如 <code>expr: expr op=('*'|'/') expr</code> 。</p>
<p>这边因为举的例子比较简单,可以直接在一个 g4 文件中同时定义语法规则和词法规则。对于复杂的语法规则,可以将语法规则和词法规则分开定义。</p>
<p>在 Rider 或 VS Code 中安装 ANTLR4 的插件,可以检查语法规则的正确性。</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202504/202504020917102.jpg" /></p>
<p>在 Rider 中安装 ANTLR4 的插件后,可以在 g4 文件选中 expr 规则,右键选择 <code>Test Rule expr</code> 来测试语法规则是否正确。</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202504/202504020917103.jpg" /></p>
<p>左侧的输入框中输入要测试的表达式,右侧的输出框中会以树形结构的方式显示语法分析的结果。</p>
<p class="maodian"><a name="_lab2_3_2"></a></p><h3>3. 生成语法分析器</h3>
<p>ANTLR4 是基于 Java 开发的,所以我们需要安装 Java 运行环境才能使用 ANTLR4 工具来生成语法分析器。</p>
<p>我们有两种方式来使用 ANTLR4 生成语法分析器,优先推荐使用 <code>Antlr4BuildTasks</code> 项目来自动生成语法分析器。</p>
<p class="maodian"><a name="_lab2_3_3"></a></p><h3>直接使用 ANTLR4 官方提供的工具来生成语法分析器。</h3>
<p>首先,我们需要下载 ANTLR4 工具,可以从 ANTLR4 的官方网站下载:<a href="https://www.antlr.org/download.html" rel="external nofollow" target="_blank">https://www.antlr.org/download.html</a></p>
<p>写本文时,最新的版本是 4.13.2,下载地址为:<br /><a href="https://www.antlr.org/download/antlr-4.13.2-complete.jar" rel="external nofollow" target="_blank">https://www.antlr.org/download/antlr-4.13.2-complete.jar</a></p>
<p>本文为方便演示,将 antlr-4.13.2-complete.jar 下载到 g4 文件所在的目录下。</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202504/202504020917104.jpg" /></p>
<p>接着就可以使用 Java 运行 ANTLR4 工具来生成语法分析器。</p>
<div class="jb51code"><pre class="brush:bash;">java -jar antlr-4.13.2-complete.jar -Dlanguage=CSharp Arithmetic.g4</pre></div>
<p>其中,Arithmetic.g4 是我们编写的语法规则文件,-Dlanguage=CSharp 表示生成 C# 语言的语法分析器。</p>
<p>执行上面的命令后,会生成一些文件,其中包括 <code>ArithmeticLexer.cs</code>、<code>ArithmeticParser.cs</code>。</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202504/202504020917115.jpg" /></p>
<p>后面我们就可以使用生成的语法分析器来进行语法分析了。</p>
<p class="maodian"><a name="_lab2_3_4"></a></p><h3>借助 Antlr4BuildTasks 项目自动生成语法分析器。</h3>
<p>上面的方式需要手动下载 ANTLR4 工具,然后使用 Java 运行 ANTLR4 工具来生成语法分析器,还会生成一些必须需要添加到项目中的文件。这样的方式比较繁琐,我们可以使用 <code>Antlr4BuildTasks</code> 项目来自动生成语法分析器。</p>
<p><code>Antlr4BuildTasks</code> 的 GitHub 地址为:<br /><a href="https://github.com/kaby76/Antlr4BuildTasks" rel="external nofollow" target="_blank">https://github.com/kaby76/Antlr4BuildTasks</a></p>
<p><code>Antlr4BuildTasks</code> 是一个 MSBuild 任务,它可以自动下载 ANTLR4 工具,然后使用 ANTLR4 工具来生成语法分析器,最后将生成的语法分析器添加到项目中。它也会尝试下载 java 运行环境,如果 build 过程中出现错误,可以尝试手动安装全局的 java 运行环境。</p>
<p>除了安装 Antlr4BuildTasks 的包之外,我们还需要在项目文件(.csproj)中添加一些配置,完整 .csproj 文件如下:</p>
<div class="jb51code"><pre class="brush:csharp;"><Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Antlr4.Runtime.Standard" Version="4.13.1"/>
<PackageReference Include="Antlr4BuildTasks" Version="12.8.0" PrivateAssets="all"/>
</ItemGroup>
<ItemGroup>
<Antlr4 Include="**\*.g4"/>
</ItemGroup>
</Project></pre></div>
<p><code><Antlr4 Include="**\*.g4"/></code> 表示将项目中所有的 .g4 文件都添加到 Antlr4 任务中。当然也可以指定具体的 .g4 文件路径。</p>
<p>在 build 项目时,Antlr4BuildTasks 会将 .g4 文件编译成的文件放在 obj 文件夹下,我们可以在 obj 文件夹下找到生成的语法分析器。</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202504/202504020917116.jpg" /></p>
<p>obj 文件夹下的文件是临时文件,会在每次 build 时重新生成,我们不需要将 obj 文件夹下的文件添加到项目中。</p>
<p class="maodian"><a name="_lab2_3_5"></a></p><h3>4. 编写代码来使用语法分析器</h3>
<p>接下来我们就可以编写代码来使用生成的语法分析器了。</p>
<p>访问 AST 的方式有两种:<strong>Visitor</strong>和 <strong>Listener</strong>。我们可以选择其中一种方式来访问 AST。</p>
<p>ANTLR4 会为我们生成一个 <strong>Parser</strong>,<strong>Parser</strong> 在遍历 AST 时会调用 <strong>Visitor</strong> 的 <code>VisitXXX</code> 方法,或者 <strong>Listener</strong> 的 <code>EnterXXX</code> 和 <code>ExitXXX</code> 方法。</p>
<p class="maodian"><a name="_lab2_3_6"></a></p><h3>使用 Visitor 实现</h3>
<p>下面我们以访问者模式为例,编写一个简单的 C# 程序来使用语法分析器。</p>
<p>ANTLR4 会为我们生成一个 <code>ArithmeticBaseVisitor</code> 类,我们可以继承这个类来完成对 AST 的访问。</p>
<p>在前面的 g4 文件中,我们为每个 AST 节点定义了一个名字, <code>MulDiv</code>、<code>AddSub</code>、<code>Int</code>、<code>Parens</code> 这些,对应 <code>ArithmeticBaseVisitor</code> 中的 <code>VisitMulDiv</code>、<code>VisitAddSub</code>、<code>VisitInt</code>、<code>VisitParens</code> 方法。</p>
<p>我们可以通过重写这些方法来实现对 AST 的访问:</p>
<div class="jb51code"><pre class="brush:csharp;">public class ArithmeticVisitor : ArithmeticBaseVisitor<int>
{
// 解析乘除法
public override int VisitMulDiv(ArithmeticParser.MulDivContext context)
{
// context 包含了当前节点的信息
// context.expr(0) 和 context.expr(1) 分别表示乘除法的两个操作数
// 访问子节点,获取操作数的值
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());
}</pre></div>
<p>定义好了 visitor 之后,我们就可以使用它来解析表达式了。</p>
<div class="jb51code"><pre class="brush:csharp;">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);
// 创建语法分析器,传入词法分析器的输出的token流
var parser = new ArithmeticParser(tokens);
// 用 visitor 模式解析表达式
var visitor = new ArithmeticVisitor();
return visitor.Visit(parser.expr());
}</pre></div>
<p class="maodian"><a name="_lab2_3_7"></a></p><h3>使用 Listener 实现</h3>
<p>ANTLR4 的 <strong>Parser</strong> 在遍历 AST 时会调用 <strong>Listener</strong> 的 <code>EnterXXX</code> 和 <code>ExitXXX</code> 方法,我们可以通过重写这些方法来实现对 AST 的访问。</p>
<p><code>EnterXXX</code> 方法在进入节点时调用,<code>ExitXXX</code> 方法在离开节点时调用。<br />我们可以在 <code>ExitXXX</code> 方法里将操作数压入栈中,下次访问时就可以从栈中弹出操作数进行计算。</p>
<div class="jb51code"><pre class="brush:csharp;">public class ArithmeticListener : ArithmeticBaseListener
{
// 使用栈来存储操作数
private readonly Stack<int> _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 ExitParens(ArithmeticParser.ParensContext context)
{
// ExitParens 方法在这里不需要做任何操作,因为我们已经在 MulDiv 和 AddSub 中处理了括号内的表达式
}
public override void ExitInt(ArithmeticParser.IntContext context)
{
int value = int.Parse(context.INT().GetText());
_stack.Push(value);
}
}</pre></div>
<div class="jb51code"><pre class="brush:csharp;">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);
// 创建语法分析器,传入词法分析器的输出的token流
var parser = new ArithmeticParser(tokens);
var listener = new ArithmeticListener();
// 解析表达式
parser.AddParseListener(listener);
parser.expr();
// 获取结果
return listener.Result;
}</pre></div>
<p class="maodian"><a name="_label4"></a></p><h2>构建自定义 AST 以解决复杂问题</h2>
<p>上面的例子中,我们在遍历 AST 时直接计算了表达式的值,这种方式在简单的表达式中是可以的,但如果表达式的处理逻辑比较复杂,更建议将 原始AST 转换成一个我们自定义的 AST,然后在后续的处理逻辑中使用这个自定义的 AST,将解析和处理逻辑分开,可以让代码更清晰,功能也容易实现。</p>
<p>下面我们定义一个比加减乘除法更复杂的需求:指定一个文件夹,用 sql 语句来查询文件夹下的 csv 文件,支持过滤条件、排序等操作。表名是文件名,字段名是 csv 文件的列名。</p>
<p>为简化起见,我们只支持简单的查询语句,支持 <code>SELECT</code>、<code>FROM</code>、<code>WHERE</code>、<code>ORDER BY</code> 等关键字。数据类型仅用字符串类型做示范,支持的过滤方式有 <code>=</code>、<code>!=</code>、<code>LIKE</code>,过滤条件之间只能用 <code>AND</code> 连接,排序方式支持 <code>ASC</code> 和 <code>DESC</code>。</p>
<p>这里我们将词法规则和语法规则分开定义,词法规则定义在 <code>SqlLexer.g4</code> 文件中,语法规则定义在 <code>SqlParser.g4</code> 文件中。</p>
<p><code>SqlLexer.g4</code> 文件的内容如下:</p>
<div class="jb51code"><pre class="brush:csharp;">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
: * // 用于表名、列名等
;
WS : [ \t\r\n]+ -> skip ; // 忽略空格</pre></div>
<p><code>SqlParser.g4</code> 文件的内容如下:</p>
<div class="jb51code"><pre class="brush:csharp;">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)?
;</pre></div>
<p>定义完后可以在 Rider 中使用 ANTLR4 的插件来检查语法规则的正确性,选中 <code>query</code> 规则,右键选择 <code>Test Rule query</code> 来测试语法规则是否正确。</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202504/202504020917117.jpg" /></p>
<p>下面我们先定义一组类型用来表示 SQL 语句的 AST:</p>
<div class="jb51code"><pre class="brush:csharp;">public abstract class Expression;
public class QueryExpression : Expression
{
public required string TableName { get; init; }
public required bool SelectAll { get; init; }
public required IEnumerable<string> SelectList { get; init; }
public required IEnumerable<WhereCondition> WhereConditions { get; init; }
public required IEnumerable<OrderByCondition> 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; }
publicbool IsDescending { get; init; }
}</pre></div>
<p>我们定义一个 <code>SqlAstBuilder</code> 类来实现对 SQL 语句的解析:</p>
<div class="jb51code"><pre class="brush:csharp;">public class SqlAstBuilder : SqlParserBaseVisitor<QueryExpression>
{
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
};
}
}</pre></div>
<p><code>SqlToCsvEngine</code> 类用来执行 SQL 语句并从 CSV 文件中读取数据:</p>
<div class="jb51code"><pre class="brush:csharp;">public class SqlToCsvEngine(DirectoryInfo csvDirectory)
{
public IEnumerable<Dictionary<string, string>> ExecuteQuery(string query)
{
// 创建词法分析器
var lexer = new SqlLexer(new AntlrInputStream(query));
// 创建语法分析器,传入词法分析器的输出的token流
var tokens = new CommonTokenStream(lexer);
var parser = new SqlParser(tokens);
// 将查询语句解析为自定义的 AST
var astBuilder = new SqlAstBuilder();
var expression = astBuilder.Visit(parser.query());
// 处理 AST,执行查询
if (expression is not { } queryExpression)
{
throw new InvalidOperationException("Expected a query expression");
}
// 读取 CSV 文件
var csvData = ReadCsv(queryExpression.TableName);
// 过滤数据
var filteredData = csvData.Where(row =>
{
// 处理 WHERE 条件
// 处理 WHERE 条件
bool isMatch = true;
foreach (var condition in queryExpression.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;
});
// 处理 ORDER BY 条件
foreach (var orderByCondition in queryExpression.OrderByConditions)
{
Func<IEnumerable<Dictionary<string, string>>, Func<Dictionary<string, string>, string>,
IEnumerable<Dictionary<string, string>>> orderByFunc;
if (filteredData is IOrderedEnumerable<Dictionary<string, string>> 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.");
});
}
// 处理 SELECT 条件
if (queryExpression.SelectAll)
{
return filteredData;
}
var selectedData = filteredData.Select(row =>
{
var selectedRow = new Dictionary<string, string>();
foreach (var columnName in queryExpression.SelectList)
{
if (row.TryGetValue(columnName, out var value))
{
selectedRow = value;
}
else
{
throw new InvalidOperationException($"Column {columnName} does not exist in CSV file.");
}
}
return selectedRow;
});
return selectedData;
}
private IEnumerable<Dictionary<string, string>> 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);
}
}
}</pre></div>
<p>接下来我们就可以开始测试了:</p>
<p>测试用的 CSV 文件内容如下:</p>
<div class="jb51code"><pre class="brush:plain;">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
David,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</pre></div>
<div class="jb51code"><pre class="brush:csharp;">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");
}
}</pre></div>
<p>输出结果如下:</p>
<blockquote><p>Name City Occupation Company <br />Alice New York Engineer TechCorp <br />Ben Atlanta Writer Publishing House <br />Bob Los Angeles Designer Creative Inc <br />Charlie Chicago Manager Finance Group <br />David Houston Teacher School District <br />Hannah Boston Lawyer Legal Partners </p></blockquote>
<p class="maodian"><a name="_label5"></a></p><h2>参考资料</h2>
<p><a href="https://github.com/antlr/grammars-v4" rel="external nofollow"rel="external nofollow" target="_blank">https://github.com/antlr/grammars-v4</a></p>
<p><a href="https://wizardforcel.gitbooks.io/antlr4-short-course/content/basic-concept.html" rel="external nofollow" target="_blank">https://wizardforcel.gitbooks.io/antlr4-short-course/content/basic-concept.html</a></p>
<p><a href="https://github.com/antlr/antlr4/blob/master/doc/csharp-target.md" rel="external nofollow" target="_blank">https://github.com/antlr/antlr4/blob/master/doc/csharp-target.md</a></p>
頁:
[1]