Antlr4入門(六)實戰之JSON

本章中,我們將學習編寫JSON語法文件,即如何通過閱讀參考手冊、樣例代碼和已有的非ANTLR語法來構造完整的語法。接着我們將使用監聽器或訪問器來將JSON格式轉成XML。

注:JSON是一種存儲鍵值對的數據結構,由於值本身也可以作爲鍵值對的容器,所以JSON中可以包含嵌套結構。

一、自頂向下的設計——編寫JSON語法

在本章中,我們的目標是通過閱讀JSON參考手冊、查看它的語法描述圖和現有的語法來構造一個能夠解析JSON的ANTLR語法。下面,我們將從JSON參考手冊中提取關鍵詞彙,然後一步步將它們編寫成ANTLR規則。

一個JSON文件可以是一個對象,或者是由若干個值組成的數組

從語法上看,這不過是一個選擇模式,因此,我們可以使用下列規則來表達:

// 一個JSON文件可以是一個對象,或者是由若干個值組成的數組
json : object
     | array
     ;

下一步是將json規則引用的各個子規則進行分解。對於對象,JSON語法是這樣定義的:

一個對象是一組無序的鍵值對集合。一個對象以一個左花括號{開始,且以右花括號}結束。每個鍵後跟一個冒號:,鍵值對之間由逗號,分隔。

JSON官網上的語法圖強調對象中的鍵必須是字符串。爲將上面這段自然語言的表述轉換成語法結構,我們試着將它分解,從中提取關鍵的、能夠指示採用何種模式的詞組。第一句話中的“一個對象是”明確地告訴我們創建一個名爲“object”的規則。接着,“一組無序的鍵值對集合”實際上是若干個“鍵值對”組成的序列。而“無序的集合”指明瞭對象的鍵的語義,即鍵的順序沒有意義。第二個句子中引入了一個詞法符號依賴,一個對象是以左右花括號作爲開始和結束的。最後一個句子進一步指明瞭鍵值對序列的細節:由逗號分隔。至此,我們可以得到以下ANTLR標記編寫的語法:

// 一個對象是一組無序的鍵值對集合。一個對象以一個左花括號{開始,且以右花括號}結束。
// 每個鍵後跟一個冒號:,鍵值對之間由逗號,分隔
object : '{' pair (',' pair)* '}' 
       | '{' '}' 
       ;
pair : STRING ':' value;

下面,我們接着來看JSON中另外一種高級結構——數組。數組的語法描述如下:

數組是一組值的有序集合。一個數組由一個左方括號[開始,並以一個右方括號]結束。其中的值由逗號,分隔

和object規則一樣,array包含一個由逗號分隔的序列模式和一個左右方括號間的詞法符號依賴。

// 數組是一組值的有序集合。一個數組由一個左方括號[開始,並以一個右方括號]結束。
// 其中的值由逗號,分隔
array : '[' value (',' value)* ']'
      | '[' ']'
      ;

在上訴規則的基礎上進一步細分,我們就需要編寫規則value。通過查看JSON參考手冊,我們可以知道value的語法描述如下:

一個值可以是一個雙引號包圍的字符串、一個數字、true\false、null、一個對象、或者一個數組。

顯而易見,這是一個很簡單的選擇模式。

// 一個值可以是一個雙引號包圍的字符串、一個數字、true\false、null、一個對象、或者一個數組。
value : STRING
      | NUMBER
      | 'true'
      | 'false'
      | 'null'
      | object
      | array
      ;

這裏,由於value規則引用了object和array,它成爲(間接)遞歸規則。以上就是解析JSON的所有語法規則,下面我們來看下詞法規則。

根據JSON語法參考,字符串定義如下:

一個字符串就是一個由零個或多個Unicode字符組成的序列,它由雙引號包圍,其中的Unicode字符使用反斜槓轉義。單個字符由長度爲1的字符串表示。

JSON的字符串定義和C/Java中的字符串非常相似。其實在前文中,我們已經編寫了字符串的ANTLR詞法規則,而這裏的JSON字符串定義只是比我們之前編寫的字符串增加了對Unicode字符的轉義。我們接着查看JSON參考手冊,可以得到以下需要被轉義的字符。

因此,我們的string規則定義如下:

// 一個字符串就是一個由零個或多個Unicode字符組成的序列,它由雙引號包圍,其中的字符使用反斜槓轉義。
// 單個字符由長度爲1的字符串表示
STRING : '"' (ESC | ~["\\])* '"';
fragment ESC : '\\' (["\\/bfnrt] | UNICODE);
fragment UNICODE : 'u' HEX HEX HEX HEX;
fragment HEX : [0-9a-fA-F];

其中ESC片段規則匹配一個Unicode序列或者預定義的轉義字符。而在UNICODE片段規則中,我們又定義了一個HEX片段規則來替代需要多次重複的編寫的十六進制數字。

最後一個需要編寫的詞法符號是NUMBER。

// 一個數字和C/Java中的數字非常相似,除了一點之外:不允許使用八進制和十六進制
NUMBER
    :   '-'? INT '.' [0-9]+ EXP? // 1.35, 1.35E-9, 0.3, -4.5
    |   '-'? INT EXP             // 1e10 -3e4
    |   '-'? INT                 // -3, 45
    ;
fragment INT :   '0' | [1-9] [0-9]* ; // 除零外的數字不允許以0開始
fragment EXP :   [Ee] [+\-]? INT ; // \- 是對-的轉義,因爲[...]中的-用於表示“範圍”

和上一章CSV語法中不同的是,JSON需要額外處理空白字符。

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

至此,完整的JSON語法文件已經編寫完畢。下面是完整的JSON語法文件併爲備選分支添加標籤後的結果:

grammar JSON;

// 一個JSON文件可以是一個對象,或者是由若干個值組成的數組
json : object
     | array
     ;

// 一個對象是一組無序的鍵值對集合。一個對象以一個左花括號{開始,且以右花括號}結束。
// 每個鍵後跟一個冒號:,鍵值對之間由逗號,分隔
object : '{' pair (',' pair)* '}'   #AnObject
       | '{' '}'                    #EmptyObject //空對象
       ;
pair : STRING ':' value;

// 數組是一組值的有序集合。一個數組由一個左方括號[開始,並以一個右方括號]結束。
// 其中的值由逗號,分隔
array : '[' value (',' value)* ']'  #ArrayOfValues
      | '[' ']'                     #EmptyArray     //空數組
      ;

// 一個值可以是一個雙引號包圍的字符串、一個數字、true\false、null、一個對象、或者一個數組。
value : STRING  #String
      | NUMBER  #Atom
      | 'true'  #Atom
      | 'false' #Atom
      | 'null'  #Atom
      | object  #ObjectValue
      | array   #ArrayValue
      ;

// 一個字符串就是一個由零個或多個Unicode字符組成的序列,它由雙引號包圍,其中的字符使用反斜槓轉義。
// 單個字符由長度爲1的字符串表示
STRING : '"' (ESC | ~["\\])* '"';
fragment ESC : '\\' (["\\/bfnrt] | UNICODE);
fragment UNICODE : 'u' HEX HEX HEX HEX;
fragment HEX : [0-9a-fA-F];

// 一個數字和C/Java中的數字非常相似,除了一點之外:不允許使用八進制和十六進制
NUMBER
    :   '-'? INT '.' [0-9]+ 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 ;

讓我們使用ANTLR工具來測試下吧。

二、將JSON轉成XML

在本小節中我們將構建一個從JSON到XML的翻譯器。對於以下JSON輸入,我們期待的輸出是:

其中,<element>元素是一個我們需要在翻譯過程中生成的標籤。

由於監聽器無法存儲值(返回類型是void),所以我們需要ParseTreeProperty來存放中間結果。

接着我們從最簡單規則的開始翻譯。value規則中的Atom備選分支用於匹配詞法符號中的文本內容,對於它,我們只需要將值存入ParseTreeProperty即可。

    @Override
    public void exitAtom(JSONParser.AtomContext ctx) {
        setXml(ctx, ctx.getText());
    }

而對於string,我們需要做一個額外處理——剔除首位雙引號。

    @Override
    public void exitArrayOfValues(JSONParser.ArrayOfValuesContext ctx) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("\n");
        for (JSONParser.ValueContext valueContext : ctx.value()){
            stringBuilder.append("<element>");
            stringBuilder.append(getXml(valueContext));
            stringBuilder.append("<element>");
            stringBuilder.append("\n");
        }
        setXml(ctx,stringBuilder.toString());
    }

    @Override
    public void exitString(JSONParser.StringContext ctx) {
        setXml(ctx, stripQuotes(ctx.getText()));
    }

而對於value規則的ObjectValue和ArrayValue備選分支,其實只需要去調用object和array規則方法就行。

    @Override
    public void exitObjectValue(JSONParser.ObjectValueContext ctx) {
        // 類比 String value() { return object(); }
        setXml(ctx,getXml(ctx.object()));
    }

    @Override
    public void exitArrayValue(JSONParser.ArrayValueContext ctx) {
        setXml(ctx,getXml(ctx.array()));
    }

在完成對value規則所有元素的翻譯後,我們需要處理鍵值對,將它們轉換成標籤和文本。對於STRING ':' value,分別對應XML中標籤名和標籤值。因此,它們的翻譯結果如下:

    @Override
    public void exitPair(JSONParser.PairContext ctx) {
        String tag = stripQuotes(ctx.STRING().getText());
        String value = String.format("<%s>%s<%s>\n",tag,getXml(ctx.value()),tag);
        setXml(ctx,value);
    }

而對於object規則,我們知道它是由一系列的鍵值對組成,也就是說,我們需要循環遍歷其中的鍵值對,將其對應的XML追加到語法分析樹存儲的結果中。

@Override
    public void exitAnObject(JSONParser.AnObjectContext ctx) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("\n");
        for (JSONParser.PairContext pairContext : ctx.pair()){
            stringBuilder.append(getXml(pairContext));
        }
        setXml(ctx,stringBuilder.toString());
    }

    @Override
    public void exitEmptyObject(JSONParser.EmptyObjectContext ctx) {
        setXml(ctx,"");
    }

同理,對於array規則,我們採用同樣的處理方式,唯一不同的是,我們需要爲子節點添加標籤<element>

    @Override
    public void exitArrayOfValues(JSONParser.ArrayOfValuesContext ctx) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("\n");
        for (JSONParser.ValueContext valueContext : ctx.value()){
            stringBuilder.append("<element>");
            stringBuilder.append(getXml(valueContext));
            stringBuilder.append("<element>");
            stringBuilder.append("\n");
        }
        setXml(ctx,stringBuilder.toString());
    }

    @Override
    public void exitEmptyArray(JSONParser.EmptyArrayContext ctx) {
        setXml(ctx,"");
    }

最後,我們將最終結果存入根節點中。

    @Override
    public void exitJson(JSONParser.JsonContext ctx) {
        setXml(ctx,getXml(ctx.getChild(0)));
    }

完整的翻譯器代碼如下:

package json;

import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeProperty;

public class JSONToXMLListener extends JSONBaseListener {
    // 將每棵子樹翻譯完的字符串存儲在該子樹的根節點中
    private ParseTreeProperty<String> xml = new ParseTreeProperty<String>();

    public void setXml(ParseTree node, String value){
        xml.put(node, value);
    }

    public String getXml(ParseTree node){
        return xml.get(node);
    }

    /**
     * 去掉字符串首尾的雙引號""
     * @param s
     * @return
     */
    public String stripQuotes(String s) {
        if ( s==null || s.charAt(0)!='"' ) return s;
        return s.substring(1, s.length() - 1);
    }


    @Override
    public void exitJson(JSONParser.JsonContext ctx) {
        setXml(ctx,getXml(ctx.getChild(0)));
    }

    @Override
    public void exitAnObject(JSONParser.AnObjectContext ctx) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("\n");
        for (JSONParser.PairContext pairContext : ctx.pair()){
            stringBuilder.append(getXml(pairContext));
        }
        setXml(ctx,stringBuilder.toString());
    }

    @Override
    public void exitEmptyObject(JSONParser.EmptyObjectContext ctx) {
        setXml(ctx,"");
    }

    @Override
    public void exitPair(JSONParser.PairContext ctx) {
        String tag = stripQuotes(ctx.STRING().getText());
        String value = String.format("<%s>%s<%s>\n",tag,getXml(ctx.value()),tag);
        setXml(ctx,value);
    }

    @Override
    public void exitArrayOfValues(JSONParser.ArrayOfValuesContext ctx) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("\n");
        for (JSONParser.ValueContext valueContext : ctx.value()){
            stringBuilder.append("<element>");
            stringBuilder.append(getXml(valueContext));
            stringBuilder.append("<element>");
            stringBuilder.append("\n");
        }
        setXml(ctx,stringBuilder.toString());
    }

    @Override
    public void exitEmptyArray(JSONParser.EmptyArrayContext ctx) {
        setXml(ctx,"");
    }

    @Override
    public void exitString(JSONParser.StringContext ctx) {
        setXml(ctx, stripQuotes(ctx.getText()));
    }

    @Override
    public void exitAtom(JSONParser.AtomContext ctx) {
        setXml(ctx, ctx.getText());
    }

    @Override
    public void exitObjectValue(JSONParser.ObjectValueContext ctx) {
        // 類比 String value() { return object(); }
        setXml(ctx,getXml(ctx.object()));
    }

    @Override
    public void exitArrayValue(JSONParser.ArrayValueContext ctx) {
        setXml(ctx,getXml(ctx.array()));
    }
}

編寫main方法調用測試

import json.JSONLexer;
import json.JSONParser;
import json.JSONToXMLListener;
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;

public class JSONMain {
    public static void main(String[] args) throws Exception{
        BufferedReader reader = new BufferedReader(new FileReader("xxx\\json.txt"));
        ANTLRInputStream inputStream = new ANTLRInputStream(reader);
        JSONLexer lexer = new JSONLexer(inputStream);
        CommonTokenStream tokenStream = new CommonTokenStream(lexer);
        JSONParser parser = new JSONParser(tokenStream);
        ParseTree parseTree = parser.json();
        System.out.println(parseTree.toStringTree());

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

        String xml = listener.getXml(parseTree);
        System.out.println(xml);
    }
}

json.txt內容如下:

{
    "id" : 1,
    "name" : "Li",
    "scores" : {
        "Chinese" : "95",
        "English" : "85"
    },
    "array" : [1.2, 2.0e1, -3] 
}

運行結果如下:

後記

本章我們學習瞭如何通過閱讀參考手冊、採用自頂向下設計來編寫JSON語法文件。還學習了使用監聽器來實現從JSON到XML的翻譯器。可以看到,我們翻譯的過程並不是一蹴而就的,而是採用分而治之的思想,是從最簡單的開始翻譯,然後將局部結果合併的。

 

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