自己實現JSON、XML的解析 沒那麼難

lqt0223發表於2018-01-02

本文的目的,不是針對現有的可用於生產環境的JSON、XML解析器原始碼進行剖析,而是介紹文字掃描的基礎方法next(char),並以此為核心武器,根據目標語言的詞法和語法特點,一步步地組織出條例清晰、易維護的解析器程式碼。希望這會是一篇實踐性強,讓您有所收穫的文章。

另外,這裡需要提前說明的是,本文所實現的解析器僅作為coding練習使用。一些目標語言的規範中提到的語法,可能無法正常解析。另外,本文所實現的解析器也缺少大量的例項進行測試。請不要用於生產用途。

前言

作為一個非科班前端程式設計師,我最近特別痴迷於自學《編譯原理》這門課。原因在於,自己大學時代的專業是語言學,其中的理論有頗多相似之處;再加上前端工作中,模版編譯成render function,webpack通過loader載入檔案等都方面涉及到了編譯。我也希望自己能多瞭解一些編譯知識,說不定能在日後的前端工作中能夠發揮奇效。

看了一些youtube上的公開課資源,啃了一點龍書這樣的編譯原理經典作品後,我感覺自己只瞭解了一堆關於詞法法解析、語法解析的理論總結,很難從中獲得“學會了”、“會用了”這樣的成就感。於是在稍稍有了一點知識基礎後,我開始尋找github上關於解析器的原始碼。

JSON的解析

這裡想給大家推薦的是JSON之父,Douglas Crockford的repo: JSON-js中的這個原始碼,它也是本文的靈感源泉:

JSON-js/json_parse.js at master · douglascrockford/JSON-js

據程式碼註釋,這個檔案實現了JSON.parse方法,使用的解析手段是recursive decending(遞迴下降分析)。

在同一個repo裡還有一個json_parse_state.js檔案,也是JSON.parse方法的實現,使用的解析手段是state machine(狀態機)。

其實我個人認為上文連結中的原始碼使用的解析手段也是state machine,因為recursive decending應該是語法分析使用的方法來著= =。

但從程式碼的清晰度上來看,json_parse.js要好不少,所以更推薦閱讀。

快速地過一遍原始碼,我們可以發現一個核心函式:

var next = function (c) {

// If a c parameter is provided, verify that it matches the current character.

    if (c && c !== ch) {
        error("Expected '" + c + "' instead of '" + ch + "'");
    }

// Get the next character. When there are no more characters,
// return the empty string.

    ch = text.charAt(at);
    at += 1;
    return ch;
};
複製程式碼

這個方法相當於一個字元掃描器,其中使用的全域性變數at是當前掃描游標所處位置的索引,ch是當前掃描游標所處位置的字元。呼叫next方法時,如果傳入了引數c(也是一個字元),則會比較此字元與當前掃描器所在的字元,如果不相同就會報錯,並且掃描游標不會向前移動;如果未傳引數,掃描游標的位置和所指的字元都會向前更新一個位置。

這份程式碼中的其他函式,充斥著對next的呼叫,讓我們來看幾個例子感受一下next的用法。

var word = function () {

    // true, false, or null.

    switch (ch) {
    case "t":
        next("t");
        next("r");
        next("u");
        next("e");
        return true;
    case "f":
        next("f");
        next("a");
        next("l");
        next("s");
        next("e");
        return false;
    case "n":
        next("n");
        next("u");
        next("l");
        next("l");
        return null;
    }
    error("Unexpected '" + ch + "'");
};
複製程式碼

word函式用來處理JSON中的三個常量token,即true, falsenull。整個函式根據首字母,分別接收t->r->u->e,f->a->l->s->e,n->u->l->l這樣的字元輸入。如果其中出現了其他順序的字元輸入,都會丟擲Error。word方法還會在匹配token的同時,返回所匹配到的token的值。3個return語句所出現的位置,表示word函式已經接受了這段字元輸入,併成功解析出了一個值。

再來看另一個不傳參呼叫next()的例子:

var white = function () {

// Skip whitespace.

    while (ch && ch <= " ") {
        next();
    }
};
複製程式碼

white函式的作用僅在於跳過空白,只要當前字元是屬於空白的,就不停地呼叫next()作無條件後跳。

原始碼中還有number和string函式,其用途和上面的word, white一樣,只不過邏輯更為複雜,可以解析出不定長度、不定字元組合的數字和字串。

一步一步地寫出這些“零件”的解析函式後,我們就可以進一步寫出一些複合結構的解析函式了,也就是原始碼中的array和object函式。

最後,原始碼中實現了可以解析任意一個JSON元素的value函式。從語法的角度講,這裡所定義的value,可以是任何一個string, number, array或object,至此,我們就完成了解析所有JSON元素需要的函式。

以上就是解析的核心程式碼了,個人認為十分地易於理解並且有明確的分層,易於維護以及以後增加功能。我也在這裡用同樣的next函式的手法,嘗試重寫了這個JSON解析器原始碼。作為練習,我沒有實現escape或revive等功能,但把各個解析函式拆分得更加精細(例如為每個單字元token都寫了解析函式,將array拆解為[ + elements + ]等),使得程式碼更易讀。地址是:

18 JSON parser

XML的解析

有了上面的JSON解析器實現的“手感”,我又嘗試著用同樣的next函式手法,部分地實現了XML的解析。和JSON相比,個人在實現過程中發現的坑點主要在於:

  • JSON物件基本上就是JavaScript中Object物件的字面化表示,所以每次解析出來一小段之後,直接以JavaScript數列或物件的形式儲存即可。XML節點需要為其定義類似下面的資料結構,所以程式碼的複雜度略有增加:
Node {
  tagName //節點標籤名
  attrs //節點上的屬性,為key/value的陣列
  children //節點的子節點,為Node的陣列
}
複製程式碼
  • XML物件必須作語法分析,也就是close tag有沒有匹配的問題。諸如<a><b></a></b>這樣的XML需要提示解析錯誤。不過實現這個也很簡單,使用一個nodeStack棧,在opentag時推入節點;在closetag時檢查當前節點是否和棧尾的tag相匹配,匹配則推出末尾的節點;在comment節點或text節點時不作處理即可。
  • comment節點的結束判斷。comment節點的格式是<!--content-->,因此在解析content部分時,每輸入一個字元,需要作3個字元的提前判斷。即,如果當前所讀到的字元的接下來三個字元分別是-->時,停止解析。

我所實現的XML解析器的程式碼如下(沒有實現self-closing tag的解析功能,例如<br>, <input>等。所有tag必須成對出現):

20 XML parser

相關文章