github地址:https://github.com/EStormLynn/Python-JSON-Parserpython
從零開始寫一個JSON的解析器,特徵以下:git
JSON(JavaScript Object Notation)是一個用於數據交換的文本格式,參考ecma標準,JSON Data Interchange Format,先看一段JSON的數據格式:github
{ "title": "Design Patterns", "subtitle": "Elements of Reusable Object-Oriented Software", "author": [ "Erich Gamma", "Richard Helm", "Ralph Johnson", "John Vlissides" ], "year": 2009, "weight": 1.8, "hardcover": true, "publisher": { "Company": "Pearson Education", "Country": "India" }, "website": null }
在json的樹狀結構中web
es_parser 是一個手寫的遞歸降低解析器(recursive descent parser)。因爲 JSON 語法特別簡單,能夠將分詞器(tokenizer)省略,直接檢測下一個字符,即可以知道它是哪一種類型的值,而後調用相關的分析函數。對於完整的 JSON 語法,跳過空白後,只需檢測當前字符:json
n ➔ literal t ➔ true f ➔ false " ➔ string 0-9/- ➔ number [ ➔ array { ➔ object
對於json的typevalue和json string編寫了這樣2個類數組
class EsValue(object): __slots__ = ('type', 'num', 'str', 'array', 'obj') def __init__(self): self.type = JTYPE_UNKNOW class context(object): def __init__(self, jstr): self.json = list(jstr) self.pos = 0
以解析多餘的空格,製表位,換行爲例:性能優化
def es_parse_whitespace(context): if not context.json: return pos = 0 while re.compile('[\s]+').match(context.json[pos]): pos += 1 context.json = context.json[pos:]
字面量包括了false,true,null三種。ide
def es_parse_literal(context, literal, mytype): e_value = EsValue() if ''.join(context.json[context.pos:context.pos + len(literal)]) != literal: raise MyException("PARSE_STATE_INVALID_VALUE, literal error") e_value.type = mytype context.json = context.json[context.pos + len(literal):] return PARSE_STATE_OK, e_value def es_parse_value(context, typevalue): if context.json[context.pos] == 't': return es_parse_literal(context, "true", JTYPE_TRUE) if context.json[context.pos] == 'f': return es_parse_literal(context, "false", JTYPE_FALSE) if context.json[context.pos] == 'n': return es_parse_literal(context, "null", JTYPE_NULL)
JSON number類型,number 是以十進制表示,它主要由 4 部分順序組成:負號、整數、小數、指數。只有整數是必需部分。函數
JSON 可以使用科學記數法,指數部分由大寫 E 或小寫 e 開始,而後可有正負號,以後是一或多個數字(0-9)。性能
JSON 標準 ECMA-404 採用圖的形式表示語法,能夠更直觀地看到解析時可能通過的路徑:
python是一種動態語言,因此es_value中num能夠是整數也能夠是小數,
class es_value(): def __init__(self, type): self.type = type self.num = 0
python對於string類型,能夠強制轉換成float和int,可是int(string)沒法處理科學記數法的狀況,因此統一先轉成float在轉成int
typevalue.num = float(numstr) if isint: typevalue.num = int(typevalue.num)
實現的單元測試包含:
def testnum(self): print("\n------------test number-----------") self.assertEqual(type(self.parse("24")), type(1)) self.assertEqual(type(self.parse("1e4")), type(10000)) self.assertEqual(type(self.parse("-1.5")), type(-1.5)) self.assertEqual(type(self.parse("1.5e3")), type(1.500))
對於字符串中存在轉義字符,在load的時候需要處理轉義字符,\u的狀況,進行編碼成unicode
def es_parse_string(context): charlist = { '\\"': '\"', "\\'": "\'", "\\b": "\b", "\\f": "\f", "\\r": "\r", "\\n": "\n", "\\t": "\t", "\\u": "u", "\\\\": "\\", "\\/": "/", "\\a": "\a", "\\v": "\v" } while context.json[pos] != '"': # 處理轉意字符 if context.json[pos] == '\\': c = context.json[pos:pos + 2] if c in charlist: e_value.str += charlist[c] else: e_value.str += ''.join(context.json[pos]) pos += 1 continue pos += 2 else: e_value.str += ''.join(context.json[pos]) pos += 1 e_value.type = JTYPE_STRING context.json = context.json[pos + 1:] context.pos = 1 if '\u' in e_value.str: e_value.str = e_value.str.encode('latin-1').decode('unicode_escape') return PARSE_STATE_OK, e_value
單元測試:
def teststring(self): print("\n------------test string----------") self.assertEqual(type(self.parse("\" \\\\line1\\nline2 \"")), type("string")) # input \\ is \ self.assertEqual(type(self.parse("\" abc\\def\"")), type("string")) self.assertEqual(type(self.parse("\" null\"")), type("string")) self.assertEqual(type(self.parse("\"hello world!\"")), type("string")) self.assertEqual(type(self.parse("\" \u751F\u5316\u5371\u673A \"")), type("string"))
將python dict結構dumps成json串
def es_dumps(obj): obj_str = "" if isinstance(obj, bool): if obj is True: obj_str += "True" else: obj_str += "False" elif obj is None: obj_str += "null" elif isinstance(obj, basestring): for ch in obj.decode('utf-8'): if u'\u4e00' <= ch <= u'\u9fff': obj_str += "\"" + repr(obj.decode('UTF-8')) + "\"" break else: obj_str += "\"" + obj + "\"" elif isinstance(obj, list): obj_str += '[' if len(obj): for i in obj: obj_str += es_dumps(i) + ", " obj_str = obj_str[:-2] obj_str += ']' elif isinstance(obj, int) or isinstance(obj, float): # number obj_str += str(obj) elif isinstance(obj, dict): obj_str += '{' if len(obj): for (k, v) in obj.items(): obj_str += es_dumps(k) + ": " obj_str += es_dumps(v) + ", " obj_str = obj_str[:-2] obj_str += '}' return obj_str
導入cProfile模塊進行性能分析,load中國34個省份地區人口發佈,
import cProfile from jsonparser import * import json cProfile.run("print(es_load(\"china.json\"))")
修改部分代碼使用python build-in,優化context結構,string在copy的時候比list性能顯著提升。消耗時間從20s降到1s