在前面的章節中,我們已經學習瞭如何編寫語法文件和使用監聽器和訪問器來實現具體的動作。現在,是時候使用這些知識來構造真實世界的語法了。在本章中,我們將從最簡單的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工具來測試它。
最後,使用ANTLR工具去生成語法分析器等代碼。
二、監聽器——加載CSV數據
這裏,我們的目標是編寫一個自定義的監聽器,將CSV中的數據加載到一種精心設計的數據結構(List<Map<String,String>>)中。這跟其他數據讀取器或者配置文件讀取器是相類似的。
我們會爲每一行建立一個Map,其中包含從列名到字段的映射。因此,對於如下輸入文件:
我們預期的處理結果是:
完整的監聽器代碼如下:
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方法結果如下: