Python實現JSON生成器和遞歸降低解釋器

Python實現JSON生成器和遞歸降低解釋器

github地址:https://github.com/EStormLynn/Python-JSON-Parserpython

目標

從零開始寫一個JSON的解析器,特徵以下:git

  • 符合標準的JSON解析器和生成器
  • 手寫遞歸降低的解釋器(recursive descent parser)
  • 使用Python語言(2.7)
  • 解釋器和生成器少於500行
  • 使用cProfile完成性能分析和優化

實現內容

  • [x] 解析字面量(true false null)
  • [x] 解析數字
  • [x] 解析字符串
  • [x] 解析Unicode
  • [x] 解析數組
  • [x] 解析對象
  • [x] 單元測試
  • [x] 生成器
  • [x] cProfile性能優化

詳細介紹

JSON是什麼

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

  • null: 表示爲 null
  • boolean: 表示爲 true 或 false
  • number: 通常的浮點數表示方式,在下一單元詳細說明
  • string: 表示爲 "..."
  • array: 表示爲 [ ... ]
  • object: 表示爲 { ... }

實現解釋器

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"))

es_dumps函數,json生成器

將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性能分析

導入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

1

1
1
相關文章
相關標籤/搜索