luckyscript / blog_archive

My blog
www.luckyscript.me
5 stars 0 forks source link

JSON词法分析思路浅谈 #12

Closed luckyscript closed 6 years ago

luckyscript commented 6 years ago

写在前面:已经许久不写博客,这些天觉得前一段时间过得有点浪了,是时候得把书本拿起来,把键盘敲起来了。

前段时间,写了一个带注释的JSON的词法分析。从刚开始的信誓旦旦,到后来的迷茫,再到代码的重构。整个过程像是在坐过山车一样起伏,加上自己对npm包不熟悉,也顺便发布了一个包到npm上。也算是自己的第一个包,准备会一直维护。

npm install json_with_comments_parser;

第一版思路浅析:

export default {
  parse(content) {
    if (typeof content != "string") content = JSON.stringify(content);
    if (!content) return;
    let result = [];
    let contentArray = content.split("");
    if (contentArray[0] == "{") {
      // object like json
      result = objectJsonParser(content.split(""));
    } else if (contentArray.shift() == "[") {
      // array like json
      let arrayLike = JSON.parse(content);
      if (arrayLike.length == 0) return result;
      arrayLike.forEach((v, i) => {
        result.push(objectJsonParser(JSON.stringify(v)));
      });
    } else {
        console.error("not a valid json");
    }
    return result;
  }
};

主函数如此,第一版我的操作是,把传入的json字符串 转成一个数组,然后开始遍历这个数组,通过对遍历到的值,得到一些判断。主函数这里主要判断是object类型的json还是数组类型的json。然而这里有个问题是,我没有对字符串trim,所以当json前面有空格或者tab的时候,都会认为是非法的。这点当时也是忽略了。

接着,主函数中主要任务就是把内容传入objectJsonParser中。

...
for (char of contentArray) {
    // if {, push
    if (char == "{" && !isStringValue) {
      //// console.log("caaa",char);
      if (counter > 0) {
        stack.push(char);
      }
      counter += 1;
    } else if (char == "}" && !isStringValue) {
      if (counter > 1) {
        stack.push(char);
      }
      counter -= 1;
    }
...

这里主要是 做了字符的判断,通过counter来判断处于object的层级。同时还要注意,当前是否在字符串内。所以这个判断条件也是很恶心了。

if (char == ":" && counter == 1 && !isArrayValue && !isStringValue) {
        //// console.log("caaa",char);
        object.key = stack.join("").trim();
        valueEnd = false;
        isSemiPush = false;
        isPushedValue = false;
        stack = [];
      } else if (
        char == "," &&
        counter == 1 &&
        !isArrayValue &&
        !isStringValue
      ) {
        object.children =
          typeof objectJsonParser(
            stack
              .join("")
              .trim()
              .split("")
          ) == "string"
            ? []
            : objectJsonParser(
                stack
                  .join("")
                  .trim()
                  .split("")
              );

        object.value = delEmpty(stack)
          .join("")
          .trim();
        object.type = judgeType(delEmpty(stack));
        valueEnd = true;
        // result.push({...object});
        result.push(JSON.parse(JSON.stringify(object)));
        stack = [];
        isSemiPush = true;
      }

当然 更恶心的还在这个地方,比如':'的判断,这里我之前就没考虑到 字符串中包含冒号和数组中包含冒号的情况,导致判断条件写的越来越臃肿。随意感受一个这个代码,不但“抽象”,而且丝毫没有维护性,定位bug也是需要定位半天。所以,在此基础上,我决定重构代码。

第二版思路浅析

第二版主要使用typescript来写,因为我觉得 写这些包不涉及node,不涉及dom,用ts的强类型还是一个比较好的选择,刚好自己不怎么会ts,借此机会来学习一下。

import parser from './parser'
export function parse (json:string) {
    let jsonText = json.trim();
    console.log("【jsontext】=> ", jsonText);
    return parser(jsonText);
}

入口函数是这样的,其实就是一段废话,当然这里做了trim的操作,也是有了前车之鉴。

function parse (json:string):any {
    if(!check_valid(json))
        throw new Error("Not valid JSON");
    if(json[0] === '{') {
        return {
            value: parse_object(json).value, 
            type: 'Object'
        };
    } else if(json[0] == '[') {
        // array like json
        return {
            value: parse_value_array(json).children||parse_value_array(json).value,
            type: 'Array'
        };
    } else {
        // if comment is before json, throw error
        throw new Error(`unsupport input: ${json}`)
    }
}

主函数 parse,思路还是和之前的很像,这里也不赘述。

let parse_object = (value:string):Array<Tree>|any => {
    let len:number = value.length;
    let pointer:number = 1;
    let tree:Tree = {
        key: '',
        value: '',
        comment: '',
        children: [],
        type: 'Object'
    }
    let result: Array<Tree>|any = [];
    let inKey:boolean = false, inValue: boolean = false, inComment:boolean = false;
    let nextType = 'key';
    let stack:Array<string> = [];
    let commentFlag = false;

    // skip whitespace
    pointer = skip_whitespace(value, pointer);
    if(value[pointer] == '}') {
        result = [];
        return {value: result};
    }
    for(let depth = 1;pointer < len && depth !== 0;pointer++) {
        // key start
        let char:string = value[pointer];
        if(inKey) {
            if(char == '"') {
                // key end
                inKey = false;
                nextType = 'value';
                tree.key = stack.join("");
                stack = [];
                pointer = skip_whitespace(value, pointer);
                commentFlag = true;
            } else {
                stack.push(char);
            }
        }
        if(char == '"' && nextType == 'key') {
            // stack.push(char)
            inKey = true;
            // result.push(JSON.parse(JSON.stringify(tree))
            tree = {
                key: '',
                value: '',
                children: [],
                type: 'Object',
                comment: ''
            };
        }
        if(inValue) {
            let val = parse_value(value.substr(pointer));
            pointer += val.len;
            if(val.type == 'Object') {
                tree.children = val.value
            } else {
                tree.value = val.value;
            }
            tree.type = val.type;
            result.push(JSON.parse(JSON.stringify(tree)));
            inValue = false;
            nextType = 'key';
        }
        if(char == ':' && !inValue && !inComment) {
            pointer = skip_whitespace(value, pointer);
            inValue = true;
        }
        if(char == '/' && value[pointer - 1] == '/' && !inValue && !inKey && commentFlag) {
            // sigle line comment start
            let comment = parse_single_comment(value.substr(pointer));

            pointer += comment.len;
            tree.comment = comment.comment;
            result.pop();
            result.push(JSON.parse(JSON.stringify(tree)));
        }
        if(char == '*' && value[pointer - 1] == '/' && !inValue && !inKey) {
             // sigle line comment start
             let comment = parse_multi_comment(value.substr(pointer));

             pointer += comment.len;
             tree.comment = comment.comment;
             result.pop();
             result.push(JSON.parse(JSON.stringify(tree)));
        }
        if(char == '{') depth++;
        if(char == '}') depth--;
    }
    return {
        value: result,
        len: pointer + 1,
        type: 'Object'
    };

}

object类型 的parse函数,这里我将 值为object和最外层的object统一了,用递归的方法去调用。在最后用depth来标志object的层级。用pointer,来获取char的同时,也能知道当前的位置。在获取值的时候,直接调用parse_value。在匹配到注释的时候,直接调用parse_single_comment或者parse_multi_comment。唯一需要判断的就是inkey还是invalue。思路还是比较清晰。

let parse_value = function (value: string) {
    value = value.trim();
    switch(value[0]) {
        // bool
        case 't': 
            return parse_value_literal(value, 'true')
        case 'f': 
            return parse_value_literal(value, 'false');
        // null
        case 'n': 
            return parse_value_literal(value, 'null');
        // string
        case '"':
            return parse_value_string(value);
        // array
        case '[':
            return parse_value_array(value);
        // object
        case '{':
            return parse_object(value);
        default:
            return parse_number(value);
    }
}

parsevalue中,主要就是一个switch case语句,通过特征来匹配到对应的值的解析中。

比如number就会进入 parse_number中。

let parse_number = (value:string) => {
    let p = 0;
    if(value[p] == '-')
        p++;
    if(value[p] == '0')
        p++;
    else {
        if(!ISDIGIT1TO9(value[p]))
            throw new Error('not a valid number');
        for(p++; ISDIGIT(value[p]); p++);
    }
    if (value[p] == '.') {
        p++;
        if (!ISDIGIT(value[p])) 
            throw new Error('not a valid number');
        for (p++; ISDIGIT(value[p]); p++);
    }
    if (value[p] == 'e' || value[p] == 'E') {
        p++;
        if (value[p] == '+' || value[p] == '-') p++;
        if (!ISDIGIT(value[p])) 
            throw new Error('not a valid number');
        for (p++; ISDIGIT(value[p]); p++);
    }
    return {
        value: value.substr(0, p),
        type: 'Number',
        len: p
    }
}

虽然输入的值 可能会多余,但是 只匹配到 对应的值,然后返回。

源码在:https://github.com/luckyscript/json_parser