前言
在之前實現的 JSON
解析器中當時只實現了將一個 JSON 字串轉換為一個 JSONObject
,並沒有將其對映為一個具體的 struct
;如果想要獲取值就需要先做斷言將其轉換為 map
或者是切片再來獲,會比較麻煩。
decode, err := gjson.Decode(`{"glossary":{"title":"example glossary","age":1}}`)
assert.Nil(t, err)
glossary := v["glossary"].(map[string]interface{})
assert.Equal(t, glossary["title"], "example glossary")
assert.Equal(t, glossary["age"], 1)
但其實轉念一想,部分場景我們甚至我們只需要拿到 JSON
中的某個欄位的值,這樣還需要先宣告一個 struct
會略顯麻煩。
經過查詢發現已經有了一個類似的庫來解決該問題,https://github.com/tidwall/gjson 並且 star 數還很多(甚至名字都是一樣的?),說明這樣的需求大家還是很強烈的。
於是我也打算增加類似的功能,使用方式如下:
最後還加上了一個四則運算的功能。
物件導向的方式操作 JSON
因為功能類似,所以我參考了 tidwall
的 API
但去掉一些我覺得暫時用不上的特性,並調整了一點語法。
當前這個版本只能通過確定的 key
加上 .
點符號訪問資料,如果是陣列則用 [index]
的方式訪問下標。
[]
符號訪問陣列我覺得要更符合直覺一些。
以下是一個包含多重巢狀 JSON
的訪問示例:
str := `
{
"name": "bob",
"age": 20,
"skill": {
"lang": [
{
"go": {
"feature": [
"goroutine",
"channel",
"simple",
true
]
}
}
]
}
}`
name := gjson.Get(str, "name")
assert.Equal(t, name.String(), "bob")
age := gjson.Get(str, "age")
assert.Equal(t, age.Int(), 20)
assert.Equal(t, gjson.Get(str,"skill.lang[0].go.feature[0]").String(), "goroutine")
assert.Equal(t, gjson.Get(str,"skill.lang[0].go.feature[1]").String(), "channel")
assert.Equal(t, gjson.Get(str,"skill.lang[0].go.feature[2]").String(), "simple")
assert.Equal(t, gjson.Get(str,"skill.lang[0].go.feature[3]").Bool(), true)
這樣的語法使用個人覺得還是滿符合直覺的,相信對使用者來說也比較簡單。
返回值參考了 tidwall
使用了一個 Result
物件,它提供了多種方法可以方便的獲取各種型別的資料
func (r Result) String() string
func (r Result) Bool() bool
func (r Result) Int() int
func (r Result) Float() float64
func (r Result) Map() map[string]interface{}
func (r Result) Array() *[]interface{}
func (r Result) Exists() bool
比如使用 Map()/Array()
這兩個函式可以將 JSON
資料對映到 map
和切片中,當然前提是傳入的語法返回的是一個合法 JSONObject
或陣列。
實現原理
在實現之前需要先定義一個基本語法,主要支援以下四種用法:
- 單個
key
的查詢:Get(json,"name")
- 巢狀查詢:
Get(json,"obj1.obj2.obj3.name")
- 陣列查詢:
Get(json,"obj.array[0]")
- 陣列巢狀查詢:
Get(json,"obj.array[0].obj2.obj3[1].name")
語法很簡單,符合我們日常接觸到語法規則,這樣便可以訪問到 JSON
資料中的任何一個值。
其實實現過程也不復雜,我們已經在上一文中實現將 JSON
字串轉換為一個 JSONObject
了。
這次只是額外再解析剛才定義的語法為 token
,然後解析該 token
的同時再從生成好的 JSONObject
中獲取資料。
最後在解析完 token
時拿到的 JSONObject
資料返回即可。
我們以這段查詢程式碼為例:
首先第一步是對查詢語法做詞法分析,最終得到下圖的 token
。
在詞法分析過程中也可以做簡單的語法校驗;比如如果包含陣列查詢,並不是以 ]
符號結尾時就丟擲語法錯誤。
接著我們遍歷語法的 token。如下圖所示:
每當遍歷到 token
型別為 Key
時便從當前的 JSONObject 物件中獲取資料,並用獲取到的值替覆蓋為當前的 JSONObject。
其中每當遇到 .
[
]
這樣的 token 時便消耗掉,直到我們將 token 遍歷完畢,這時將當前 JSONObject
返回即可。
在遍歷過程中當遇到非法格式時,比如 obj_list[1.]
便會返回一個空的 JSONObject
。
語法校驗這點其實也很容易辦到,因為根據我們的語法規則,Array
中的 index
後一定緊接的是一個 EndArray
,只要不是一個 EndArray
便能知道語法不合法了。
有興趣的可以看下解析過程的原始碼:
https://github.com/crossoverJie/gjson/blob/cfbca51cc9bc0c77e6cb9c9ad3f964b2054b3826/json.go#L46
對 JSON 做四則運算
str := `{"name":"bob", "age":10,"magic":10.1, "score":{"math":[1,2]}}`
result := GetWithArithmetic(str, "(age+age)*age+magic")
assert.Equal(t, result.Float(), 210.1)
result = GetWithArithmetic(str, "(age+age)*age")
assert.Equal(t, result.Int(), 200)
result = GetWithArithmetic(str, "(age+age) * age + score.math[0]")
assert.Equal(t, result.Int(), 201)
result = GetWithArithmetic(str, "(age+age) * age - score.math[0]")
assert.Equal(t, result.Int(), 199)
result = GetWithArithmetic(str, "score.math[1] / score.math[0]")
assert.Equal(t, result.Int(), 2)
最後我還擴充套件了一下語法,可以支援對 JSON
資料中的整形(int、float)
做四則運算,雖然這是一個小眾需求,但做完我覺得還挺有意思的,目前在市面上我還沒發現有類似功能的庫,可能和小眾需求有關?。
其中核心的四則運算邏輯是由之前寫的指令碼直譯器提供的:
https://github.com/crossoverJie/gscript
單獨提供了一個函式,傳入一個四則運算表示式返回計算結果。
由於上一版本還不支援 float,所以這次專門適配了一下。
限於篇幅,更多關於這個四則運算的實現邏輯會在後面繼續分享。
總結
至此算是我第一次利用編譯原理的知識解決了一點特定領域問題,在大學以及工作這些年一直覺得編譯原理比較高深,所以內心一直是抗拒的,但經過這段時間的學習和實踐慢慢的也掌握到了一點門道。
不過目前也只是冰山一角,後面的編譯原理後端更是要涉及到計算機底層知識,所以依然任重而道遠。
已上都是題外話,針對於這個庫我也會長期維護;為了能達到生產的使用要求,儘量提高了單測覆蓋率,目前是98%。
也歡迎大家使用,提 bug?。
後面會繼續優化,比如支援轉義字元、提高效能等。
感興趣的朋友請持續關注:
https://github.com/crossoverJie/gjson