欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Antlr4入门(五)实战之CSV

程序员文章站 2022-04-13 14:10:02
...

在前面的章节中,我们已经学习了如何编写语法文件和使用监听器和访问器来实现具体的动作。现在,是时候使用这些知识来构造真实世界的语法了。在本章中,我们将从最简单的CSV(comma-separated-value)格式开始,学习如何通过阅读参考手册、样例代码和已有的非ANTLR语法来构造完整的语法,并使用监听器或访问器来将CSV转成Map存储。

一、自顶向下的设计——编写CSV语法

设计良好的语法反应了编程世界中的功能分解或者自顶向下的设计。这意味着我们对语言结构的辨识是从最粗的粒度开始,一直进行到最详细的层次,并把它们编写成为语法规则。所以,我们的第一个任务是找到最粗粒度的语言结构,将它作为我们的起始规则。

现在让我们查看CSV的参考手册,我们可以知道 一个CSV文件就是一系列以换行符为终止的行(a comma-separated-value[CSV] file is a sequence of rows terminated by newlines),根据上诉定义,我们可以写出以下伪代码

file : <<sequence of rows terminated by newlines>>;

接着,我们降低一个层级,描述起始规则右侧所指定的那些元素。它右侧的名称通常是词法符号或者尚未定义的子规则。其中,词法符号是那些我们大脑能够轻易识别出的单词、标点符号或者运算符。正如英语语句中的单词是最基本元素一样,词法符号是语法的基本元素。起始规则引用了其他的、需要进一步细分的语言结构,如上面例子中的行(row)。

一个行就是一系列由逗号分隔的字段(a row is a sequence of fields separated by commas),一个字段就是一个数字或者字符串(a field is a number or string)。按照定义,我们的伪代码更新如下:

file : <<sequence of rows terminated by newlines>>;
row : <<sequence of fields separated by commas>>;
field : <<number or string>>;

当我们完成对规则的定义后,我们的CSV语法草稿就成形了。然后我们对它进行一些增强,使它能够识别标题行,并且允许空列存在。接着按照前面所学的知识,我们可以写出如下CSV语法文件。

grammar CSV;
// 一个CSV由标题行和一个或多个的常规行组成
file : header row+;
// 标题行与常规行并没有区别
header : row;
// 常规行由一系列由逗号分隔的字段组成,并以换行符结束
row : field (',' field)* '\r'? '\n';

field : TEXT        # text
      | STRING      # string
      |             # empty
      ;

// 除,\r\n"之外的任意字符
TEXT : ~[,\r\n"]+;
// 两个双引号是对双引号的转义
STRING : '"' ('""'|~'"')* '"';   

至此,一个CSV语法文件已经编写完毕,下面,我们使用ANTLR工具来测试它。

Antlr4入门(五)实战之CSV

最后,使用ANTLR工具去生成语法分析器等代码。

二、监听器——加载CSV数据

这里,我们的目标是编写一个自定义的监听器,将CSV中的数据加载到一种精心设计的数据结构(List<Map<String,String>>)中。这跟其他数据读取器或者配置文件读取器是相类似的。

我们会为每一行建立一个Map,其中包含从列名到字段的映射。因此,对于如下输入文件:

Antlr4入门(五)实战之CSV

我们预期的处理结果是:

Antlr4入门(五)实战之CSV

完整的监听器代码如下:

package csv;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class CSVToMapListener extends CSVBaseListener{
    private static final String EMPTY = "";
    private List<Map<String,String>> rows = new ArrayList<Map<String, String>>(16);
    List<String> header = new ArrayList<String>(16);
    List<String> currentRow = new ArrayList<String>(16);

    public List<Map<String, String>> getRows() {
        return rows;
    }

    @Override
    public void exitFile(CSVParser.FileContext ctx) {
        super.exitFile(ctx);
    }

    @Override
    public void exitHeader(CSVParser.HeaderContext ctx) {
        header.addAll(currentRow);
    }

    @Override
    public void enterRow(CSVParser.RowContext ctx) {
        currentRow.clear();
    }


    @Override
    public void exitRow(CSVParser.RowContext ctx) {
        // 判断当前RowContext的父节点是否是HeaderContext
        if (ctx.getParent() instanceof CSVParser.HeaderContext){
            return;
        }
        Map<String, String> line = new HashMap<String, String>(16);
        for(int i = 0; i < header.size(); i++){
            line.put(header.get(i), currentRow.get(i));
        }
        rows.add(line);
    }

    @Override
    public void exitText(CSVParser.TextContext ctx) {
        currentRow.add(ctx.getText());
    }

    @Override
    public void exitString(CSVParser.StringContext ctx) {
        currentRow.add(ctx.getText());
    }

    @Override
    public void exitEmpty(CSVParser.EmptyContext ctx) {
        currentRow.add(EMPTY);
    }
}

对于词汇符号(Text、String、Empty)的处理方式是:提取合适的字符串,并将其放入currentRow中。而对于常规行,我们需要将其与标题行区分开来,然后将所需数据存放Map中。

import csv.CSVLexer;
import csv.CSVParser;
import csv.CSVToMapListener;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeWalker;

import java.io.BufferedReader;
import java.io.FileReader;
import java.util.List;
import java.util.Map;

public class CSVMain {
    public static void main(String[] args) throws Exception{
        BufferedReader reader = new BufferedReader(new FileReader("xxx\\t.csv"));

        ANTLRInputStream inputStream = new ANTLRInputStream(reader);
        CSVLexer lexer = new CSVLexer(inputStream);
        CommonTokenStream tokenStream = new CommonTokenStream(lexer);
        CSVParser parser = new CSVParser(tokenStream);
        ParseTree parseTree = parser.file();
        System.out.println(parseTree.toStringTree(parser));

        CSVToMapListener listener = new CSVToMapListener();
        ParseTreeWalker walker = new ParseTreeWalker();
        walker.walk(listener,parseTree);

        List<Map<String, String>> rows = listener.getRows();
        System.out.println(rows);
    }
}

t.csv内容如上截图所示。运行main方法结果如下:

Antlr4入门(五)实战之CSV

 

相关标签: antlr