這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
項目地址:json2xml
什麼是antlr
antlr(ANother Tool for Language Recognition)是一個強大的文法分析器產生工具,它可用於讀取,處理,執行和翻譯結構化的文本和二進位檔案。目前,該工具廣泛應用於學術和工業生產領域,同時也是眾多語言,工具和架構的基礎。
今天我們就用這個工具實現一個go語言版的json2xml轉換器;
antlr的作用
關於一門語言的文法描述叫做grammar, 該工具能夠為該語言產生文法解析器,並自動建立文法分析數AST,同時antlr也能自動產生數的遍曆器,極大地降低手工coding 文法解析器的成本;
實踐開始
言歸正傳,拿json2xml為例,實現一個工具;
安裝
以macOS為例
brew install antlr
編輯json語言解析文法
// Derived from http://json.orggrammar JSON;json: object | array ;object : '{' pair (',' pair)* '}' # AnObject | '{' '}' # NullObject ;array : '[' value (',' value)* ']' # ArrayOfValues | '[' ']' # NullArray ;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 zerosfragment EXP : [Ee] [+\-]? INT ; // \- since - means "range" inside [...]WS : [ \t\n\r]+ -> skip ;
上面是依照antlr4的文法格式編輯的檔案
antlr4檔案文法也比較簡單:
- 以grammar關鍵字開頭,名字與檔案名稱相匹配
- 文法分析器的規則必須以小寫字母開頭
- 詞法分析器的規則必須用大寫開頭
- | 管道符號分割同一語言規則的若干備選分支,使用圓括弧把一些符號組成子規則。
涉及到的幾個專有名詞:
- 語言: 語言即一個有效語句的集合,語句由片語組成,片語由子片語組成,一次迴圈類推;
- 文法: 文法定義語言的語意規則, 文法中每條規則定義一種片語結構;
- 文法分析樹: 以樹狀的形式代表的文法的階層;根結點對應文法規則的名字,葉子節點代表語句中的符號或者詞法符號。
- 詞法分析器: 將輸入的字元序列分解成一系列的詞法符號。一個詞法分析器負責分析詞法;
- 文法分析器: 檢查語句結構是否符合文法規範或者是否合法。分析的過程類似走迷宮,一般都是通過對比匹配完成。
- 自頂向下文法分析器: 是文法分析器的一種實現,每條規則都對應文法分析器中的一個函數;
- 前向預測: 文法分析器使用前向預測來進行決策判斷,具體指將輸入的符號於每個備選分支的起始字元進行比較;
產生解析基礎代碼
# antlr4 -Dlanguage=Go -package json2xml JSON.g4
使用antlr產生目標語言為Go, package名為json2xml的基礎代碼
產生的檔案如下:
$ tree├── JSON.g4├── JSON.interp # 文法解析中間檔案├── JSON.tokens # 文法分析tokens流檔案├── JSONLexer.interp # 詞法分析中間檔案├── JSONLexer.tokens # 詞法分析tokens流檔案├── json_base_listener.go # 預設是listener模式檔案├── json_lexer.go # 詞法分析器├── json_listener.go # 抽象listener介面檔案├── json_parser.go # parser解析器檔案
實現解析器(listener例子)
package mainimport ( "fmt" "io/ioutil" "log" "os" "strings" "testing" "c2j/parser/json2xml" "github.com/antlr/antlr4/runtime/Go/antlr")func init() { log.SetFlags(log.LstdFlags | log.Lshortfile)}type j2xConvert struct { *json2xml.BaseJSONListener xml map[antlr.Tree]string}func NewJ2xConvert() *j2xConvert { return &j2xConvert{ &json2xml.BaseJSONListener{}, make(map[antlr.Tree]string), }}func (j *j2xConvert) setXML(ctx antlr.Tree, s string) { j.xml[ctx] = s}func (j *j2xConvert) getXML(ctx antlr.Tree) string { return j.xml[ctx]}// j2xConvert methodsfunc (j *j2xConvert) ExitJson(ctx *json2xml.JsonContext) { j.setXML(ctx, j.getXML(ctx.GetChild(0)));}func (j *j2xConvert) stripQuotes(s string) string { if s == "" || ! strings.Contains(s, "\"") { return s } return s[1 : len(s)-1]}func (j *j2xConvert) ExitAnObject(ctx *json2xml.AnObjectContext) { sb := strings.Builder{} sb.WriteString("\n") for _, p := range ctx.AllPair() { sb.WriteString(j.getXML(p)) } j.setXML(ctx, sb.String())}func (j *j2xConvert) ExitNullObject(ctx *json2xml.NullObjectContext) { j.setXML(ctx, "")}func (j *j2xConvert) ExitArrayOfValues(ctx *json2xml.ArrayOfValuesContext) { sb := strings.Builder{} sb.WriteString("\n") for _, p := range ctx.AllValue() { sb.WriteString("<element>") sb.WriteString(j.getXML(p)) sb.WriteString("</element>") sb.WriteString("\n") } j.setXML(ctx, sb.String())}func (j *j2xConvert) ExitNullArray(ctx *json2xml.NullArrayContext) { j.setXML(ctx, "")}func (j *j2xConvert) ExitPair(ctx *json2xml.PairContext) { tag := j.stripQuotes(ctx.STRING().GetText()) v := ctx.Value() r := fmt.Sprintf("<%s>%s</%s>\n", tag, j.getXML(v), tag) j.setXML(ctx, r)}func (j *j2xConvert) ExitObjectValue(ctx *json2xml.ObjectValueContext) { j.setXML(ctx, j.getXML(ctx.Object()))}func (j *j2xConvert) ExitArrayValue(ctx *json2xml.ArrayValueContext) { j.setXML(ctx, j.getXML(ctx.Array()))}func (j *j2xConvert) ExitAtom(ctx *json2xml.AtomContext) { j.setXML(ctx, ctx.GetText())}func (j *j2xConvert) ExitString(ctx *json2xml.StringContext) { j.setXML(ctx, j.stripQuotes(ctx.GetText()))}func TestJSON2XMLVisitor(t *testing.T) { f, err := os.Open("testdata/json2xml/t.json") if err != nil { panic(err) } defer f.Close() content, err := ioutil.ReadAll(f) if err != nil { panic(err) } // Setup the input is := antlr.NewInputStream(string(content)) // Create lexter lexer := json2xml.NewJSONLexer(is) stream := antlr.NewCommonTokenStream(lexer, antlr.LexerDefaultTokenChannel) // Create parser and tree p := json2xml.NewJSONParser(stream) p.BuildParseTrees = true tree := p.Json() // Finally AST tree j2x := NewJ2xConvert() antlr.ParseTreeWalkerDefault.Walk(j2x, tree) log.Println(j2x.getXML(tree))}
上面代碼比較簡單,看注釋就好;
一般流程如下:
- 建立輸入資料流
- 建立詞法分析器
- 產生token流,儲存詞法分析器產生的詞法符號tokens
- 建立文法分析器parser,處理tokens
- 然後針對文法規則,開始文法分析
- 最後通過預設提供的Walker,進行AST的遍曆
其中針對中間產生的參數和結果如何存放?好辦,直接定義個map,map鍵以Tree存放;
xml map[antlr.Tree]string
Listener和Visitor
antlr產生的程式碼有兩種預設,預設是listener實現,要產生visitor,需要另加參數-visitor。
這兩種機制的區別在於,監聽器的方法會被antlr提供的遍曆器對象自動調用,而visitor模式的方法中,必須顯示調用visit方法來訪問子節點。如果忘記調用的話,對應的子樹就不會被訪問。
總結
antlr是一個強大的工具,能讓常見的文法解析工作事半功倍,效率極高。同時,該工具使文法分析過程和程式本身高度分離,提供足夠的靈活性和可操控性。