如何寫一個 JSON Parser

之前在網上看到某公司新招實習生的第一次作業是寫 JSON Parser,好像之後還要寫 Scheme 的 Parser,就自己也想試試。因爲並不是工作任務,所以也沒去查任何資料,準備自己憋。

但畢竟非科班,也完全沒接觸過語言或編譯的內容,一上來完全摸不到頭腦。於是決定先實現一個 json.dumps 找找規律。dumps 很好寫,只要遞歸判斷類型然後序列化就好了。於是想 loads 是不是也類似呢?

第一版實現使用了 str.split(',') 方法來分隔集合元素,後來發現一旦嵌套就不好使了,比如 "[1, [2, 3]]" 會被處理成 "1""[2""3]"。之後爲了簡化實現,把處理的元素類型限制在整數數組。因爲這是兩種典型的標量和集合類型,其他類型的處理是類似的,只需要橫向擴展判斷邏輯即可。即寫這個 Parser 的主要難題應該還是在於對嵌套集合類型的正確處理。

思考過程如下:

  1. 已知簡單的按逗號分隔行不通,因爲會誤傷子集合類型的元素
  2. 若以 dumps 的逆向過程來理解的話,我們需要首先在頂層分割一個數組,然後遞歸處理其元素
  3. 既然是遞歸處理,在分割頂層元素時就不用考慮反序列化,只是分割成字符串即可
  4. 爲了正確分割頂層元素,我需要能夠區分字符所處的深度

即對於上例的 "[1, [2, 3]]",我需要首先把它分成 "1""[2, 3]",然後遞歸。簡單實現的代碼如下:

#lang python
import re


def loads(s):
    if not s:
        raise ValueError()
    if not isinstance(s, str):
        raise TypeError()
    if is_list(s):
        return list(map(loads, split_list(s[1: -1])))
    else:
        return int(s)


def is_list(s):
    if s[0] == '[':
        assert s[-1] == ']'
        return True
    return False


def split_list(sub):
    elements = []
    depth = 0
    start, offset = 0, 0
    while start < len(sub):
        if sub[start] in (',', ' '):
            start += 1
        elif sub[start].isdigit():
            offset = re.match('\d+(\.\d+)?', sub[start:]).end()
            elements.append(sub[start: start + offset])
            start += offset
            offset = 0
        elif sub[start] == '[':
            for char in sub[start:]:
                offset += 1
                if char == '[':
                    depth += 1
                elif char == ']':
                    depth -= 1
                if depth == 0:
                    break
            if depth == 0:
                elements.append(sub[start: start + offset])
                start += offset
                offset = 0
            else:
                raise ValueError()
    return elements

這個實現能夠工作,但是性能很差,容易發現對於嵌套對象,讀的遍數等於他的深度+1,理想情況下應該是一遍就處理完。且O(n)的遍歷還有一個好處就是能夠儘早的發現錯誤,比如如果是 [1, [2, *]] 這樣的一個串,我只能在第二次(遞歸)調用 loads 時才能發現錯誤。

所以我決定去看一下官方的實現。發現官方的思路是這樣的:

  1. 從前向後遍歷 JSON 串,通過遇到的首字符判斷需要調用的 parse 函數,含
    • parse_string
    • parse_object
    • parse_array
    • parse_float
    • parse_int
    • parse_constant (null, true, false, NaN, Infinity, -Infinity)
  2. 每個 parse 函數接收原始 string 和 當前索引偏移量 end 爲參數,返回處理完的對象和新的偏移量。parse_arrayparse_object 會遞歸調用任何 parse 函數。

這樣就是我之前想要卻沒寫出來的效果——邊讀邊做深度 parse。

添加註釋後的 parse_array 如下:

scan_once 即爲上述第一步裏的判斷函數)

def JSONArray(s_and_end, scan_once, _w=WHITESPACE.match, _ws=WHITESPACE_STR):
    s, end = s_and_end  # s 是原始 JSON 串,end 是當前偏移量
    values = []
    nextchar = s[end:end + 1]

    if nextchar in _ws:  # 處理空白字符,找到有效的 nextchar
        end = _w(s, end + 1).end()
        nextchar = s[end:end + 1]
    # Look-ahead for trivial empty array
    if nextchar == ']':  # 判斷空數組
        return values, end + 1
    _append = values.append  # 不懂爲啥一定要把 append 單拎出來
    while True:  # 一個一個的處理 array 內的元素
        try:
            value, end = scan_once(s, end)  # 判斷並調用 parse 函數的函數,即遞歸入口
        except StopIteration:
            raise ValueError(errmsg("Expecting object", s, end))
        _append(value)  # 處理完畢,添加到 value 裏
        nextchar = s[end:end + 1]
        if nextchar in _ws:  # 重複之前的內容——跳過空白符
            end = _w(s, end + 1).end()
            nextchar = s[end:end + 1]
        end += 1
        if nextchar == ']':  # 判斷結束
            break
        elif nextchar != ',':
            raise ValueError(errmsg("Expecting ',' delimiter", s, end))
        try:  # 不太懂爲啥要判斷這麼多次空白符
            if s[end] in _ws:
                end += 1
                if s[end] in _ws:
                    end = _w(s, end + 1).end()
        except IndexError:
            pass

    return values, end

最後,因爲這是個遞歸過程,所以默認狀態下它的處理深度是有上限的,而且速度不算快。如果有高性能需求的應用,最好去尋找下第三方模塊。或者感興趣的人也可以自己實現一下,思路和官方差不多,改成循環或者開啓尾遞歸優化即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章