之前在網上看到某公司新招實習生的第一次作業是寫 JSON Parser,好像之後還要寫 Scheme 的 Parser,就自己也想試試。因爲並不是工作任務,所以也沒去查任何資料,準備自己憋。
但畢竟非科班,也完全沒接觸過語言或編譯的內容,一上來完全摸不到頭腦。於是決定先實現一個 json.dumps
找找規律。dumps 很好寫,只要遞歸判斷類型然後序列化就好了。於是想 loads
是不是也類似呢?
第一版實現使用了 str.split(',')
方法來分隔集合元素,後來發現一旦嵌套就不好使了,比如 "[1, [2, 3]]"
會被處理成 "1"
、"[2"
和 "3]"
。之後爲了簡化實現,把處理的元素類型限制在整數和數組。因爲這是兩種典型的標量和集合類型,其他類型的處理是類似的,只需要橫向擴展判斷邏輯即可。即寫這個 Parser 的主要難題應該還是在於對嵌套集合類型的正確處理。
思考過程如下:
- 已知簡單的按逗號分隔行不通,因爲會誤傷子集合類型的元素
- 若以
dumps
的逆向過程來理解的話,我們需要首先在頂層分割一個數組,然後遞歸處理其元素 - 既然是遞歸處理,在分割頂層元素時就不用考慮反序列化,只是分割成字符串即可
- 爲了正確分割頂層元素,我需要能夠區分字符所處的深度
即對於上例的 "[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
時才能發現錯誤。
所以我決定去看一下官方的實現。發現官方的思路是這樣的:
- 從前向後遍歷 JSON 串,通過遇到的首字符判斷需要調用的 parse 函數,含
parse_string
parse_object
parse_array
parse_float
parse_int
parse_constant
(null, true, false, NaN, Infinity, -Infinity)
- 每個 parse 函數接收原始 string 和 當前索引偏移量 end 爲參數,返回處理完的對象和新的偏移量。
parse_array
和parse_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
最後,因爲這是個遞歸過程,所以默認狀態下它的處理深度是有上限的,而且速度不算快。如果有高性能需求的應用,最好去尋找下第三方模塊。或者感興趣的人也可以自己實現一下,思路和官方差不多,改成循環或者開啓尾遞歸優化即可。