本身動手,寫一個json2xml小工具

項目地址:json2xmlgit

什麼是antlr

antlr(ANother Tool for Language Recognition)是一個強大的語法分析器生成工具,它可用於讀取,處理,執行和翻譯結構化的文本和二進制文件。目前,該工具普遍應用於學術和工業生產領域,同時也是衆多語言,工具和框架的基礎。
今天咱們就用這個工具實現一個go語言版的json2xml轉換器;github

antlr的做用

關於一門語言的語法描述叫作grammar, 該工具可以爲該語言生成語法解析器,並自動創建語法分析數AST,同時antlr也能自動生成數的遍歷器,極大地下降手工coding 語法解析器的成本;json

實踐開始

言歸正傳,拿json2xml爲例,實現一個工具;bash

安裝

以macOS爲例框架

brew install antlr

編輯json語言解析語法

// Derived from http://json.org
grammar 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 zeros
fragment EXP :   [Ee] [+\-]? INT ; // \- since - means "range" inside [...]
WS  :   [ \t\n\r]+ -> skip ;

上面是依照antlr4的語法格式編輯的文件ide

  • antlr4文件語法也比較簡單:函數

    • 以grammar關鍵字開頭,名字與文件名相匹配
    • 語法分析器的規則必須以小寫的字母開頭
    • 詞法分析器的規則必須用大寫開頭
    • | 管道符號分割同一語言規則的若干備選分支,使用圓括號把一些符號組成子規則。
  • 涉及到的幾個專有名詞:工具

    • 語言: 語言即一個有效語句的集合,語句由詞組組成,詞組由子詞組組成,一次循環類推;
    • 語法: 語法定義語言的語意規則, 語法中每條規則定義一種詞組結構;
    • 語法分析樹: 以樹狀的形式表明的語法的層次結構;根結點對應語法規則的名字,葉子節點表明語句中的符號或者詞法符號。
    • 詞法分析器: 將輸入的字符序列分解成一系列的詞法符號。一個詞法分析器負責分析詞法;
    • 語法分析器: 檢查語句結構是否符合語法規範或者是否合法。分析的過程相似走迷宮,通常都是經過對比匹配完成。
    • 自頂向下語法分析器: 是語法分析器的一種實現,每條規則都對應語法分析器中的一個函數;
    • 前向預測: 語法分析器使用前向預測來進行決策判斷,具體指將輸入的符號於每一個備選分支的起始字符進行比較;

生成解析基礎代碼

# antlr4 -Dlanguage=Go -package json2xml JSON.g4
使用antlr生成目標語言爲Go, package名爲json2xml的基礎代碼

生成的文件以下:ui

$ 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 main

import (
    "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 methods
func (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是一個強大的工具,能讓常見的語法解析工做事半功倍,效率極高。同時,該工具使語法分析過程和程序自己高度分離,提供足夠的靈活性和可操控性。

相關文章
相關標籤/搜索