使用ANTLR解析CSV和JSON

再續 ANTLR專題 ,有了前面的基礎,下面開始用ANTLR寫一些有趣且實用的程序。

CSVJSON這兩種數據格式對軟件開發人員來說最熟悉不過了,一般讀寫CSVJSON格式的數據都會藉助現成的、比較成熟工具庫,非常方便。

試想一下,如果解析的是自定義格式的數據或者不依賴現有的CSVJSON解析庫,還有更通用的實現思路與解決方案嗎?

ANTLR作爲一個專業且成熟的語言識別工具,就能提供一套通用的解決方案。

解析CSV

完整源碼見: https://github.com/bytesfly/antlr-demo/tree/main/csv-loader/

輸入CSV格式數據:

Details,Month,Amount
Mid Bonus,June,"$2,000"
,January,"""zippo"""
Total Bonuses,"","$5,000"

解析後加載到內存中的數據結構是List<Map<String, String>>,打印出來如下:

[{Month=June, Details=Mid Bonus, Amount="$2,000"}, {Month=January, Details=, Amount="""zippo"""}, {Month="", Details=Total Bonuses, Amount="$5,000"}]

該程序實現了對常見CSV格式數據的解析。

語法規則爲CSV.g4,如下:

grammar CSV;

@header {package com.github.bytesfly.csvloader.antlr;}

file : header row+ ;

header : row ;

row : field (',' field)* NEWLINE ;

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

TEXT : ~[,\n\r"]+ ;
STRING : '"' ('""'|~'"')* '"' ; // 兩個雙引號是對雙引號的轉義
NEWLINE : '\r'? '\n' ;

上面的語法規則中,能明白爲什麼把headerrow分開嗎?

是爲了解析時更簡單方便,也更有助於理解。

我們自定義CsvLoaderListener.java,如下:

public class CsvLoaderListener extends CSVBaseListener {

    /**
     * 存儲表頭字段
     */
    private List<String> header;

    /**
     * 這個列表中的每個Map對應csv文件一行數據 ;
     * Map是從字段名到字段值的映射
     */
    private final List<Map<String, String>> rows = new ArrayList<>();

    /**
     * 存儲正在讀取的當前行的字段值
     */
    private List<String> row;

    @Override
    public void exitHeader(CSVParser.HeaderContext ctx) {
        header = row;
    }

    @Override
    public void enterRow(CSVParser.RowContext ctx) {
        row = new ArrayList<>();
    }

    @Override
    public void exitRow(CSVParser.RowContext ctx) {
        if (header != null) {
            rows.add(CollUtil.zip(header, row));
        }
    }

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

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

    @Override
    public void exitEmpty(CSVParser.EmptyContext ctx) {
        row.add("");
    }

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

最終完整的加載CSV格式數據的程序爲CsvLoader.java,如下:

public class CsvLoader {

    public static void main(String[] args) {
        // 讀取resources目錄下example.csv文件
        String s = FileUtil.readUtf8String("example.csv");

        // 從字符串讀取輸入數據
        CharStream input = CharStreams.fromString(s);

        // 新建一個詞法分析器
        CSVLexer lexer = new CSVLexer(input);

        // 新建一個詞法符號的緩衝區,用於存儲詞法分析器將生成的詞法符號
        CommonTokenStream tokens = new CommonTokenStream(lexer);

        // 新建一個語法分析器,處理詞法符號緩衝區中的內容
        CSVParser parser = new CSVParser(tokens);

        // 針對file規則,開始語法分析
        ParseTree tree = parser.file();

        // 新建一個通用的、能夠觸發回調函數的語法分析樹遍歷器
        ParseTreeWalker walker = new ParseTreeWalker();

        // 創建我們自定義的監聽器
        CsvLoaderListener listener = new CsvLoaderListener();

        // 遍歷語法分析過程中生成的語法分析樹,觸發回調
        walker.walk(listener, tree);

        // 打印從csv文件加載的數據
        System.out.println(listener.getRows());
    }
}

解析JSON

完整源碼見: https://github.com/bytesfly/antlr-demo/tree/main/json2xml/

輸入JSON格式的數據:

{
  "description" : "An imaginary server config file",
  "logs" : {"level":"verbose", "dir":"/var/log"},
  "host" : "antlr.org",
  "bool": true,
  "null": null,
  "pi": 3.14,
  "admin": ["parrt", "tombu"],
  "aliases": []
}

解析後並轉成XML格式數據如下:

<description>An imaginary server config file</description>
<logs>
<level>verbose</level>
<dir>/var/log</dir>
</logs>
<host>antlr.org</host>
<bool>true</bool>
<null>null</null>
<pi>3.14</pi>
<admin>
<element>parrt</element>
<element>tombu</element>
</admin>
<aliases>
</aliases>

該程序實現了對常見JSON格式數據的解析並將其轉成我們想要的XML格式。

語法規則爲JSON.g4,如下:

// Derived from http://json.org
grammar JSON;

@header {package com.github.bytesfly.jx.antlr;}

json:   object
    |   array
    ;

object
    :   '{' pair (',' pair)* '}'    # AnObject
    |   '{' '}'                     # EmptyObject
    ;

array
    :   '[' value (',' value)* ']'  # ArrayOfValues
    |   '[' ']'                     # EmptyArray
    ;

pair:   STRING ':' value ;

value
    :   STRING		# String
    |   NUMBER		# Atom
    |   object  	# ObjectValue
    |   array  		# ArrayValue
    |   'true'		# Atom
    |   'false'		# Atom
    |   'null'		# Atom
    ;

LCURLY : '{' ;
LBRACK : '[' ;
STRING :  '"' (ESC | ~["\\])* '"' ;

fragment ESC :   '\\' (["\\/bfnrt] | UNICODE) ;
fragment UNICODE : 'u' HEX HEX HEX HEX ;
fragment HEX : [0-9a-fA-F] ;

NUMBER
    :   '-'? INT '.' INT EXP?   // 1.35, 1.35E-9, 0.3, -4.5
    |   '-'? INT EXP            // 1e10 -3e4
    |   '-'? INT                // -3, 45
    ;
fragment INT :   '0' | '1'..'9' '0'..'9'* ; // no leading zeros
fragment EXP :   [Ee] [+\-]? INT ; // \- since - means "range" inside [...]

WS  :   [ \t\n\r]+ -> skip ;

我們自定義Json2XmlListener.java,如下:

public class Json2XmlListener extends JSONBaseListener {

    private final StringBuilder builder = new StringBuilder();

    @Override
    public void enterPair(JSONParser.PairContext ctx) {
        // <key>
        builder.append("<")
                .append(stripQuotes(ctx.STRING().getText()))
                .append(">");
    }

    @Override
    public void exitPair(JSONParser.PairContext ctx) {
        // </key>
        builder.append("</")
                .append(stripQuotes(ctx.STRING().getText()))
                .append(">\n");
    }

    @Override
    public void enterString(JSONParser.StringContext ctx) {
        ifEnterArray(ctx);
        builder.append(stripQuotes(ctx.STRING().getText()));
    }

    @Override
    public void exitString(JSONParser.StringContext ctx) {
        ifExitArray(ctx);
    }

    @Override
    public void enterAtom(JSONParser.AtomContext ctx) {
        ifEnterArray(ctx);
        builder.append(ctx.getText());
    }

    @Override
    public void exitAtom(JSONParser.AtomContext ctx) {
        ifExitArray(ctx);
    }

    @Override
    public void enterObjectValue(JSONParser.ObjectValueContext ctx) {
        ifEnterArray(ctx);
        builder.append("\n");
    }

    @Override
    public void exitObjectValue(JSONParser.ObjectValueContext ctx) {
        ifExitArray(ctx);
    }

    @Override
    public void enterArrayValue(JSONParser.ArrayValueContext ctx) {
        ifEnterArray(ctx);
        builder.append("\n");
    }

    @Override
    public void exitArrayValue(JSONParser.ArrayValueContext ctx) {
        ifExitArray(ctx);
    }

    /**
     * 去除字符串包裹着的雙引號
     */
    private static String stripQuotes(String s) {
        if (s == null || s.charAt(0) != CharPool.DOUBLE_QUOTES) {
            return s;
        }
        return s.substring(1, s.length() - 1);
    }

    /**
     * 是否進入數組元素的訪問
     */
    private void ifEnterArray(JSONParser.ValueContext ctx) {
        // 如果上級是數組的話
        if (ctx.getParent().getRuleIndex() == JSONParser.RULE_array) {
            builder.append("<element>");
        }
    }

    /**
     * 是否退出數組元素的訪問
     */
    private void ifExitArray(JSONParser.ValueContext ctx) {
        // 如果上級是數組的話
        if (ctx.getParent().getRuleIndex() == JSONParser.RULE_array) {
            builder.append("</element>\n");
        }
    }

    /**
     * 獲取JSON轉XML的結果
     */
    public String getResult() {
        return builder.toString();
    }
}

最終完整的解析JSON並將其轉成想要的XML格式程序爲Json2Xml.java,如下:

public class Json2Xml {

    public static void main(String[] args) {
        // 讀取resources目錄下example.json文件
        String s = FileUtil.readUtf8String("example.json");

        // 從字符串讀取輸入數據
        CharStream input = CharStreams.fromString(s);

        // 新建一個詞法分析器
        JSONLexer lexer = new JSONLexer(input);

        // 新建一個詞法符號的緩衝區,用於存儲詞法分析器將生成的詞法符號
        CommonTokenStream tokens = new CommonTokenStream(lexer);

        // 新建一個語法分析器,處理詞法符號緩衝區中的內容
        JSONParser parser = new JSONParser(tokens);

        // 針對json規則,開始語法分析
        ParseTree tree = parser.json();

        // 新建一個通用的、能夠觸發回調函數的語法分析樹遍歷器
        ParseTreeWalker walker = new ParseTreeWalker();

        // 創建我們自定義的監聽器
        Json2XmlListener listener = new Json2XmlListener();

        // 遍歷語法分析過程中生成的語法分析樹,觸發回調
        walker.walk(listener, tree);

        // 打印JSON轉XML的結果
        System.out.println(listener.getResult());
    }
}

通過上面兩個實戰案例,能感受到ANTLR的威力嘛?

當然,別看自己寫的代碼不多,但是需要思考的地方並不少,不理解的地方還是建議自己下載源碼本地打斷點等方式琢磨琢磨,動手之後其實也不是太難。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章