一種基于JSON語(yǔ)法的JSON數(shù)據(jù)轉(zhuǎn)換器

1. 項(xiàng)目背景

最近產(chǎn)品研發(fā)中我們?cè)O(shè)計(jì)了一個(gè)算法集成規(guī)范,定義了一個(gè)統(tǒng)一的算法服務(wù)API接口,并通過(guò)產(chǎn)品的“模型管理”模塊進(jìn)行算法服務(wù)的配置,從而實(shí)現(xiàn)外部算法服務(wù)的靈活集成與擴(kuò)展。

這個(gè)模式對(duì)于新開發(fā)的算法是沒(méi)有問(wèn)題的,按照定義的接口規(guī)范實(shí)現(xiàn)就可以輕松地集成。但是對(duì)于已有的算法,或者遇到客戶比較強(qiáng)勢(shì)不愿意改自己接口的時(shí)候,就比較尷尬了。有沒(méi)有什么比較好的方式能夠解決這個(gè)問(wèn)題呢?

另外,網(wǎng)上有很多開放的API服務(wù),可以非常方便地進(jìn)行調(diào)用。但是盡管大部分API都采用JSON格式作為返回?cái)?shù)據(jù)格式但卻格式不同,如何能夠方便進(jìn)行利用呢?

2. 需求提煉

根據(jù)場(chǎng)景問(wèn)題,我把需求進(jìn)行了提煉,問(wèn)題轉(zhuǎn)化為:設(shè)計(jì)一個(gè)通用的工具,以JSON數(shù)據(jù)作為輸入,根據(jù)指定的規(guī)則進(jìn)行數(shù)據(jù)格式轉(zhuǎn)換、數(shù)據(jù)提取,從而解決上述問(wèn)題。

目前僅考慮支持JSON,主要是大部分線上API服務(wù)(我司全部后臺(tái)服務(wù))使用JSON作為返回值格式,甚至采用標(biāo)準(zhǔn)restful 形式。

那么,對(duì)于規(guī)則應(yīng)該如何提煉、設(shè)計(jì)呢?初步考慮也采用JSON語(yǔ)法,JSON本身相對(duì)簡(jiǎn)單、容易理解,對(duì)于不太了解編程開發(fā)的人來(lái)說(shuō)會(huì)比較友好。關(guān)于JSON語(yǔ)法規(guī)范,建議大家直接看JSON官網(wǎng),非常好理解。

下一步就是對(duì)JSON規(guī)則進(jìn)行設(shè)計(jì)

3. 規(guī)則設(shè)計(jì)

我目前先考慮幾種簡(jiǎn)單情況。
(1)樣例數(shù)據(jù)設(shè)計(jì)

{
  code: 20000,
  msg: "abcde",
  data: {
    ner: {
      name: "aaa",
      type: "person",
      start: 5,
      end: 7,
      info: ['abc']
    },
    events: [
      {type, subtype, trigger}
    ],
    ent_name: [
      "a", "b", "c"
    ],
    ent_type: [
      "person", "loc", "org"
    ]
  }
}

(2)單值規(guī)則

1. $ 代表頂層,即輸入本身(當(dāng)前處理的頂層)
2. x.field 表示x為JSON對(duì)象,獲取x的field字段;支持特殊field:$len 表示字符串或數(shù)組長(zhǎng)度;$keys 表示獲取字典的key組成的數(shù)組
3. x[y] 表示x為JSON數(shù)組,獲取x數(shù)組的第y項(xiàng)元素
4. 上述規(guī)則可以嵌套,從而支持任意層次的單值信息獲?。ǔ谦@取元素本身為數(shù)組)

示例如下:

- $.code -> 20000
- $.data -> {ner, events }
- $.data.ner.name -> 'aaa'
- $.data.ner.info[0] -> 'abc'
- $.data.ner.$keys -> ['name', 'type', 'start', 'end', 'info']
- $.data.events.$length -> 1
- $.data.events

(3)對(duì)象規(guī)則

5. 對(duì)象規(guī)則,返回對(duì)象  { "a": "aaa" } 沒(méi)有變量,返回規(guī)則本身;{ "a": "$.a[2].e"} 根據(jù)指定規(guī)則獲得a的值
6. key為$root 用于指定當(dāng)前訪問(wèn)的頂層節(jié)點(diǎn)
7. key為$val 用于將本規(guī)則返回值作為當(dāng)前層級(jí)的值
8. key為$fun 用于提供一段腳本函數(shù)(例如js腳本,待定)進(jìn)行執(zhí)行,以函數(shù)返回值作為該字段的值,用于應(yīng)對(duì)復(fù)雜情況

示例如下:

{
  "$root": "$"
  "data": {
    "$root": "$.data.events",
    "$val": "$"
  }
}
->
{
  "data": [
      {type, subtype, trigger}
  ]
}

(4)數(shù)組規(guī)則

9. 數(shù)據(jù)規(guī)則返回?cái)?shù)組,數(shù)組中每個(gè)元素為一個(gè)新的規(guī)則

示例如下:

["$.data.ent_name[0]", "$.data.ent_type[0]"] 
->
["a", "person"]

(5)其他待考慮情況
基于目前了解到的API形式,還有以下需求上述規(guī)則未能很好覆蓋:

  1. 對(duì)于對(duì)稱的子樹,如果自動(dòng)進(jìn)行匹配返回?例如示例數(shù)據(jù)中,需要對(duì)ent_name和ent_type進(jìn)行配對(duì)返回:
 [["a", "person"], ["b", "loc"], ["c", "org"] ] 

這里的問(wèn)題是數(shù)組長(zhǎng)度是不定的,且橫向的數(shù)量也是不定的(請(qǐng)大家想象一棵樹,水平的多個(gè)子樹)。另外對(duì)于對(duì)稱的JSON對(duì)象節(jié)點(diǎn)又應(yīng)該如何處理?

  1. 如何實(shí)現(xiàn)對(duì)獲取值進(jìn)行映射?比如上述NLP結(jié)果中返回的person(人物),需要轉(zhuǎn)換成統(tǒng)一標(biāo)識(shí)PER
  2. 如何實(shí)現(xiàn)根據(jù)業(yè)務(wù)邏輯進(jìn)行獲?。勘热缟鲜鰧?shí)例數(shù)據(jù)中如果code不等于20000,就應(yīng)該返回錯(cuò)誤
    這留待后續(xù)進(jìn)行完善。

4. 程序設(shè)計(jì)

把規(guī)則描述清楚了,其實(shí)主要工作就結(jié)束了,因?yàn)檫@個(gè)問(wèn)題就是樹遍歷+文本處理問(wèn)題,類似SQL解析、模板消解或著編程課里面小兒科的“手寫計(jì)算器”之類的問(wèn)題。

說(shuō)一下主要思想:

  1. 首先JSON值是一棵樹,輸入規(guī)則是JSON,所以也是一棵樹,帶著規(guī)則對(duì)樹進(jìn)行遍歷,直到獲取所有數(shù)據(jù)。由于是多叉樹,所以一開始別想著用循環(huán)進(jìn)行樹遍歷,那樣太累了,直接上遞歸。在限定數(shù)據(jù)規(guī)劃和棧層次的情況下,不會(huì)發(fā)生溢出,這一點(diǎn)我可以保證,信不信由你 ^^
  2. 根據(jù)當(dāng)前規(guī)則的類型進(jìn)行判斷,如果是字符串,并且以$開頭,那么肯定是簡(jiǎn)單規(guī)則,一擼到底
  3. 再看是否是對(duì)象規(guī)則,區(qū)分幾種特殊key的情況:
    3.1 $root 這是相當(dāng)于上下文躍遷,直接把當(dāng)前數(shù)據(jù)節(jié)點(diǎn)定位到了指定的數(shù)據(jù)節(jié)點(diǎn)上了,甚至可以新建一個(gè)上下文 與原來(lái)的節(jié)點(diǎn)可以毫無(wú)干系

3.2 $val 是指定獲取的值直接作為當(dāng)前對(duì)象規(guī)則的返回值,所以要直接返回

3.3 $fun 是指定用函數(shù)進(jìn)行評(píng)估,這個(gè)函數(shù)具體用什么語(yǔ)言(JS還是python),稍微有點(diǎn)復(fù)雜(例如跨語(yǔ)言)后面再考慮,但是基本可以確定需要傳遞給函數(shù)的是當(dāng)前節(jié)點(diǎn)和當(dāng)前規(guī)則

3.4 其他特殊key考慮,可能涉及調(diào)整優(yōu)先級(jí),目前暫無(wú)

3.5 進(jìn)行對(duì)象遍歷,相當(dāng)于以輸入為模板對(duì)象,進(jìn)入下一層

  1. 再看是否是數(shù)組規(guī)則,目前比較簡(jiǎn)單粗暴,直接遍歷數(shù)組,遞歸進(jìn)入下一層,最后返回?cái)?shù)組值

  2. 其他情況,直接返回當(dāng)前數(shù)據(jù)節(jié)點(diǎn)

5. 簡(jiǎn)單規(guī)則解析

在上述思想框架下,剩下一個(gè)葉子節(jié)點(diǎn)問(wèn)題,那就是對(duì)簡(jiǎn)單規(guī)則進(jìn)行解析。直接點(diǎn),上狀態(tài)機(jī):


image.png

狀態(tài)轉(zhuǎn)移矩陣:


image.png

在此狀態(tài)圖指引下,很容易寫出相關(guān)程序,這里不再贅述。

初步效果如下:


image.png

6. 錯(cuò)誤處理

如果規(guī)則不符合約定規(guī)范怎么辦?這個(gè)時(shí)候肯定需要進(jìn)行錯(cuò)誤處理。程序里面唯一沒(méi)有問(wèn)題的就是總是有問(wèn)題!

目前簡(jiǎn)單的想法就是在遇到錯(cuò)誤的時(shí)候返回錯(cuò)誤狀態(tài),但是解析過(guò)程會(huì)盡可能繼續(xù)。

主要考慮的幾種錯(cuò)誤包括(再看一下狀態(tài)圖):

  1. 期待 .[ ,但遇到了其他字符,Invalid Start
  2. 期待其他字符,卻遇到了.[,Invalid Token
  3. [開始的token,沒(méi)有遇到],Dangling [
  4. " 開始的token,沒(méi)有遇到結(jié)束的引號(hào),Dangling "
  5. ]結(jié)束的token,發(fā)現(xiàn)不是數(shù)字 Invalid Index

7. 設(shè)計(jì)細(xì)節(jié)

  1. 把簡(jiǎn)單規(guī)則歸納為以$開頭,包含0個(gè)或多個(gè)<操作, 參數(shù)>的列表,解析過(guò)程就是獲取每一個(gè)操作及其參數(shù),操作分為 .操作和index操作,分別對(duì)應(yīng)對(duì)象和數(shù)組的訪問(wèn)
  2. 我利用了Python的生成器機(jī)制,符合條件的時(shí)候就yield返回,從而解析函數(shù)可以被當(dāng)做迭代器來(lái)遍歷
  3. 為了支持特殊符號(hào)的key,引入引號(hào),凡是引號(hào)括起來(lái)的內(nèi)容都是一個(gè)token
  4. 添加一個(gè)簡(jiǎn)單的校驗(yàn)函數(shù),通過(guò)調(diào)用解析函數(shù),發(fā)現(xiàn)錯(cuò)誤即失敗返回

8. 工程化設(shè)計(jì)應(yīng)用

目前為止只是實(shí)現(xiàn)了一個(gè)工具,可以單獨(dú)運(yùn)行,但是不能很好地提供給別人使用。那么需要考慮幾個(gè)方面:

  1. 服務(wù)化,基于Web框架,實(shí)現(xiàn)Web服務(wù),支持接收數(shù)據(jù)和規(guī)則,返回解析結(jié)果。常見的Python Flask和Java SpringBoot都可以很方便地實(shí)現(xiàn)。
  2. 持久化,規(guī)則多了之后就需要管理,由于規(guī)則相對(duì)來(lái)說(shuō)不變,優(yōu)先考慮把規(guī)則存儲(chǔ)起來(lái),并分配一個(gè)ID,使用時(shí),只需要傳遞數(shù)據(jù)和規(guī)則ID即可。常用的MySQL和MongoDB都可以推薦。使用MongoDB的時(shí)候,注意MongoDB的查詢語(yǔ)法也是JSON,因此建議對(duì)規(guī)則進(jìn)行JSON序列化,保存為一個(gè)String字段
  3. 服務(wù)集成,不僅要保存規(guī)則,還要保存服務(wù)相關(guān)的信息,基本信息包括API地址、請(qǐng)求方法、請(qǐng)求頭固定參數(shù)、請(qǐng)求體固定參數(shù)等。請(qǐng)求服務(wù)時(shí),由客戶端指定服務(wù)ID、規(guī)則ID,以及請(qǐng)求頭和請(qǐng)求體中變化的參數(shù),基于服務(wù)基本信息封裝成最后的請(qǐng)求后執(zhí)行,對(duì)返回結(jié)果應(yīng)用規(guī)則獲取輸出并返回給客戶端。這里主要涉及HTTP的知識(shí)。常用的庫(kù)requests、urllib、Apache HttpClient等
  4. 規(guī)?;{(diào)用,系統(tǒng)如果涉及大量的服務(wù)集成調(diào)用或大量規(guī)則需要分別執(zhí)行,可以采用多線程、多進(jìn)程、消息中間件等技術(shù),實(shí)現(xiàn)任務(wù)調(diào)度。推薦使用消息中間件(如Kafka),將任務(wù)寫入隊(duì)列中,并啟用多個(gè)消費(fèi)者或流處理引擎(Spark 或 Flink)進(jìn)行消費(fèi)執(zhí)行
  5. 自動(dòng)化,基于觸發(fā)或定時(shí)進(jìn)行自動(dòng)化調(diào)度。crontab就很好用。

9. 下一步考慮

  1. 完善規(guī)則設(shè)計(jì)和解析引擎,使得規(guī)則更加強(qiáng)大
  2. 算法服務(wù)集成的demo系統(tǒng),提供簡(jiǎn)單前端界面進(jìn)行配置
  3. 線上數(shù)據(jù)服務(wù)持續(xù)集成,實(shí)現(xiàn)特定領(lǐng)域數(shù)據(jù)快速獲取和持續(xù)積累
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容