javascript
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即可。
@Overridepublic void exitAtom(JSONParser.AtomContext ctx) {setXml(ctx, ctx.getText());}而對于string,我們需要做一個額外處理——剔除首位雙引號。
@Overridepublic 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());}@Overridepublic void exitString(JSONParser.StringContext ctx) {setXml(ctx, stripQuotes(ctx.getText()));}而對于value規則的ObjectValue和ArrayValue備選分支,其實只需要去調用object和array規則方法就行。
@Overridepublic void exitObjectValue(JSONParser.ObjectValueContext ctx) {// 類比 String value() { return object(); }setXml(ctx,getXml(ctx.object()));}@Overridepublic void exitArrayValue(JSONParser.ArrayValueContext ctx) {setXml(ctx,getXml(ctx.array()));}在完成對value規則所有元素的翻譯后,我們需要處理鍵值對,將它們轉換成標簽和文本。對于STRING ':' value,分別對應XML中標簽名和標簽值。因此,它們的翻譯結果如下:
@Overridepublic 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追加到語法分析樹存儲的結果中。
@Overridepublic 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());}@Overridepublic void exitEmptyObject(JSONParser.EmptyObjectContext ctx) {setXml(ctx,"");}同理,對于array規則,我們采用同樣的處理方式,唯一不同的是,我們需要為子節點添加標簽<element>
@Overridepublic 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());}@Overridepublic void exitEmptyArray(JSONParser.EmptyArrayContext ctx) {setXml(ctx,"");}最后,我們將最終結果存入根節點中。
@Overridepublic 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);}@Overridepublic void exitJson(JSONParser.JsonContext ctx) {setXml(ctx,getXml(ctx.getChild(0)));}@Overridepublic 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());}@Overridepublic void exitEmptyObject(JSONParser.EmptyObjectContext ctx) {setXml(ctx,"");}@Overridepublic 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);}@Overridepublic 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());}@Overridepublic void exitEmptyArray(JSONParser.EmptyArrayContext ctx) {setXml(ctx,"");}@Overridepublic void exitString(JSONParser.StringContext ctx) {setXml(ctx, stripQuotes(ctx.getText()));}@Overridepublic void exitAtom(JSONParser.AtomContext ctx) {setXml(ctx, ctx.getText());}@Overridepublic void exitObjectValue(JSONParser.ObjectValueContext ctx) {// 類比 String value() { return object(); }setXml(ctx,getXml(ctx.object()));}@Overridepublic 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的翻譯器。可以看到,我們翻譯的過程并不是一蹴而就的,而是采用分而治之的思想,是從最簡單的開始翻譯,然后將局部結果合并的。
?
總結
以上是生活随笔為你收集整理的Antlr4入门(六)实战之JSON的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 阿里巴巴常用的12个开发工具
- 下一篇: 原生JS面试题