自制c++ json解释器 jsonExplorer

项目源码 https://github.com/hao297531173/jsonExplorer

说在前面的话

之前我读过一本叫做《自制编程语言》 的书,给我的启发真的很大,这本书的源码在https://github.com/hao297531173/DIYProgramLanguage

我这次项目借鉴了书中的很多编码技巧,下面我就详细介绍一下核心代码编写思路

PS:对外接口的写的不多,如果你觉得这个项目很有用的话,可以自己写接口

简单用法实例

#include <cstdio>
#include <cstring>
#include <iostream>
#include "jsonExplorer.h"
using namespace std;

void get(){
    explorer e("test.json");
    e.dump();
    char a[2][100] = {"Tony", "course"};
    char b[2][100] = {"Mary", "age"};
    Value val;
    val = e.findElement(a, 2);
    if(val.type != ERROR){
        e.traverse(val);
    }
    val = e.findElement(b, 2);
    if(val.type != ERROR){
        e.traverse(val);
    }
}

int main(){
    get();
    return 0;
}

只需要引入头文件"jsonExplorer.h"就行了,然后实例化一个explorer类,构造函数中的参数就是你要解析的json文件。

之后调用接口dump()将json文件内容存入数据结构中,使用findElement(char a[][], int length);即可找到你要找的值,

其中二维数组就是你要找的值所对应的键,由外向内寻找。

输出结果

object analysis
object analysis
list analysis
object analysis
list analysis
[
        "calculus" ,"linear algebra"
    ](NUMBER):19

 

两个主要类的定义

parser

class parser{
    const char* file;   //文件路径
    FILE *fileWrite;  //写出的文件指针
    FILE *fp;   //文件指针
    Token result;
    int line;   //标识行
    char ch;
    int tokenCount = 0;
    

    //判断token是否是关键字
    TokenType isKeyword(char a[], int length);
    //跳过空格,运行完后fp指向不为空格或者回车的字符
    void skipBlanks();
    //获取下一个token
    void getNextToken();
    //显示token
    void showToken();
    //初始化result
    void initResult();
public:
    //解析主函数(用于测试)
    void parse();
    //发送token给别的程序(给别的程序调用)
    Token pullToken();
    //构造函数(一个无参数,一个有参数)
    parser(){}
    parser(char *file){
        this->file = file;
        this->fp = fopen(this->file, "r");  //打开文件
        this->fileWrite = fopen("output.txt", "w");
        this->line = 1;
        initResult();
        ch = fgetc(fp); //先读取第一个字符
        //printf("construct : %c\n", ch);
    }
    void initParser(char *file){
        this->file = file;
        this->fp = fopen(this->file, "r");  //打开文件
        this->fileWrite = fopen("output.txt", "w");
        this->line = 1;
        initResult();
        ch = fgetc(fp); //先读取第一个字符
    }
    //析构函数,主要用来关闭文件
    ~parser(){
        fclose(this->fileWrite);
        fclose(this->fp);
    }
};

类中有一个result属性,记录当前的token(有关token的定义会在后面介绍),提供两个构造函数,一个无参数的,一个有参数的,无参数的构造函数需要使用initParser()来初始化parser对象,参数就是待解析的json文件,有参数的构造函数直接将文件名传入就行了。

parse()是测试用的接口,一次将所有的token都输出到文件中(我默认是写入output.txt中,可以在构造函数中修改)

pullToken()时候提供给别的程序的接口,一次解析一个token,返回值为Token类型,返回自身属性result

explorer

class explorer{
    Value value;    //用于存储json数据
    Value error;    //用于解析错误时返回
    Token token;    //用于存储当前token
    int num;        //用于控制每行缩进个数
    int depth;      //用于记录查询深度
    parser p;
    char *file;
    //解析花括号
    Value analysisBrace();
    //解析方括号
    Value analysisSquare();
    //递归遍历对象
    void traverseObject(Value val);
    //递归遍历列表
    void traverseList(Value val);
    /*将遍历结果输出到文件*/
    void outputObject(FILE *fp, Value val);
    void outputList(FILE *fp, Value val);
    /*通过key值递归查询*/
    Value findValue(string key,Value val);
    Value findValue(Value val, char a[][MAXSIZE], int length);
    /*修改value的值*/
    Value changeValue(string key, Value ch, Value &val);
public:
    //构造函数(a是文件名字符串)
    explorer(char *a){
        this->file = a;
        p.initParser(file);
        token = p.pullToken();  //获取第一个token
        error.type = ERROR;
        this->depth = 0;    //从0开始计数
    }
    ~explorer(){}
    //提供给外部的接口
    void dump();
    //写一个遍历函数
    void traverse();
    void traverse(Value val);
    //输出到文件
    void output(char *file,Value val);
    void output(char *file);
    //根据key值查value并且返回一个value变量
    Value findElement(string key);
    Value findElement(char key[][MAXSIZE], int length);
    //根据二维数组找Value值
    //Value findElement(char a[][]);
    //修改某个key中的value值
    //参数分别是key值,要改变的value,从那个value开始查找
    //第三个参数缺省的话就是改变类自己的value
    Value changeValueOfKey(string key, Value ch, Value &val);
    Value changeValueOfKey(string key, Value ch);
    
};

构造函数的参数是待解析的文件名。

dump()是提供给用户的接口,将json数据存入内部数据结构中,也就是类中value属性中

traverse()是遍历Value数据(关于Value的定义会在后面介绍),无参数的就是遍历类本身的value,带参数的就是遍历传入的value。

output()将Value类型的数据以json格式存入json文件中,文件名就是给定的第一个参数,同样提供写入自身value值和传入value值两个版本。

findElement()就是搜索的接口,二维数组就是要找的值所对应的键(由外向里),length就是键的个数。

主要的数据结构

在介绍算法之前,我们先来看一下我采用的数据结构

首先是Token结构体

typedef enum{
    TOKEN_UNKNOWN,  //未知类型
    
    TOKEN_LEFT_BRACE,   //左花括号
    TOKEN_RIGHT_BRACE,  //右花括号

    TOKEN_LEFT_SQUARE,  //左方括号
    TOKEN_RIGHT_SQUARE, //右方括号

    TOKEN_COMMA,        //逗号

    TOKEN_QUOTATION,   //双引号

    TOKEN_NULL,         //NULL

    TOKEN_FLAG, //斜杠
    TOKEN_RE_FLAG,   //反斜杠
    TOKEN_COLON,    //冒号
    TOKEN_EOF
}TokenType;
struct Token{
    TokenType type; //token类型
    int length; //长度
    char token[MAXSIZE];
    int lineNo; //行号
};

TokenType是一个枚举,用来表示token的类型,需要解析其他的token的时候在这里添加入其他类型即可。

length记录了token的长度

token[]记录了token的字面量

lineNo记录了token在文件中的行数(这个实现很简单,在解析的时候每当遇到一个换行的时候行数加一即可)

接着看一下Value结构体

为了解决无限嵌套问题,我采用了广义表,并将所有数据都封装到Value结构体中(这也是从《自制编程语言》中学的)

//将数据类型封装
/*
    type 0表示null
         1表示数字量(整型和浮点型)
         2表示字符串
         3表示list列表
         4表示嵌套的json数据对象
         5表示保留字
         6表示bool值
*/

typedef enum{
    null,
    NUMBER,
    STRING,
    LIST,
    OBJECT,
    BOOL,
    ERROR
} ValueType;



struct Value{
    ValueType type = null;   //初始化为null   
    string str=""; //用来记录字符量,整型常数和浮点型常数
    vector<Value> list; //用来记录列表
    map<string, Value> object;  //用来记录嵌套的json对象
    int count = 0;  //记录列表或者json对象数量
};

Value类型中第一个参数是ValueType的枚举,用来记录当前value是什么类型的数据,我们一共定义了7中类型,分别是null,数字类型,字符串类型,列表类型,json对象(键值对)类型和BOOL类型,最后的ERROR类型用来异常返回(比如查询失败)。

str用来记录字面量,null,数字,字符串和bool都算字面量,关于他们的区分很简单,如果有双引号的话就是字符串,没有双引号的话如果是null那么就是null类型的,如果是false或者true就是bool类型的。

list容器用来记录列表值

object用来记录json对象(键值对)

因为json对象的key都是字符串,所以我使用map<string, Value>来记录json对象.

count是用来记录列表或者json键值对数量的,但是后面都没怎么用到

前端部分

前端包括词法分析和语法分析,主要就是吃token然后吐给语义分析程序,输出的形式是三元式 (字符串,长度,token类型)

下面是一个输出样例

line 8 : (isMonitor, 9, TOKEN_UNKNOWN)
line 8 : (", 1, TOKEN_QUOTATION)
line 8 : (:, 1, TOKEN_COLON)
line 8 : (false, 5, TOKEN_UNKNOWN)
line 9 : (}, 1, TOKEN_RIGHT_BRACE)
line 9 : (,, 1, TOKEN_COMMA)
line 10 : (", 1, TOKEN_QUOTATION)
line 10 : (Mary, 4, TOKEN_UNKNOWN)
line 10 : (", 1, TOKEN_QUOTATION)
line 10 : (:, 1, TOKEN_COLON)
line 10 : ({, 1, TOKEN_LEFT_BRACE)
line 11 : (", 1, TOKEN_QUOTATION)
line 11 : (age, 3, TOKEN_UNKNOWN)
line 11 : (", 1, TOKEN_QUOTATION)
line 11 : (:, 1, TOKEN_COLON)
line 11 : (19, 2, TOKEN_UNKNOWN)
line 11 : (,, 1, TOKEN_COMMA)
line 12 : (", 1, TOKEN_QUOTATION)
line 12 : (major, 5, TOKEN_UNKNOWN)
line 12 : (", 1, TOKEN_QUOTATION)
line 12 : (:, 1, TOKEN_COLON)
line 12 : (", 1, TOKEN_QUOTATION)
line 12 : (physics, 7, TOKEN_UNKNOWN)
line 12 : (", 1, TOKEN_QUOTATION)
line 12 : (,, 1, TOKEN_COMMA)
line 13 : (", 1, TOKEN_QUOTATION)
line 13 : (course, 6, TOKEN_UNKNOWN)
line 13 : (", 1, TOKEN_QUOTATION)
line 13 : (:, 1, TOKEN_COLON)
line 13 : ([, 1, TOKEN_LEFT_SQUARE)
line 14 : (", 1, TOKEN_QUOTATION)
line 14 : (machanics, 9, TOKEN_UNKNOWN)
line 14 : (", 1, TOKEN_QUOTATION)
line 14 : (,, 1, TOKEN_COMMA)
line 14 : (", 1, TOKEN_QUOTATION)
line 14 : (electromagnetism, 16, TOKEN_UNKNOWN)
line 14 : (", 1, TOKEN_QUOTATION)
line 15 : (], 1, TOKEN_RIGHT_SQUARE)
line 15 : (,, 1, TOKEN_COMMA)
line 16 : (", 1, TOKEN_QUOTATION)
line 16 : (isMonitor, 9, TOKEN_UNKNOWN)
line 16 : (", 1, TOKEN_QUOTATION)
line 16 : (:, 1, TOKEN_COLON)
line 16 : (true, 4, TOKEN_UNKNOWN)
line 17 : (}, 1, TOKEN_RIGHT_BRACE)
line 18 : (}, 1, TOKEN_RIGHT_BRACE)
line 18 : (EOF, 1, TOKEN_EOF)

接下来我们详细看一下parser代码

首先是keywordToken结构体,这个结构体保存关键字信息,也就是说每当吃一个token就和这个结构体数组中信息比较,如果有匹配的话那么就是关键字,否则就是字面量,数字,bool型等等其他数据类型。

typedef enum{
    TOKEN_UNKNOWN,  //未知类型
    
    TOKEN_LEFT_BRACE,   //左花括号
    TOKEN_RIGHT_BRACE,  //右花括号

    TOKEN_LEFT_SQUARE,  //左方括号
    TOKEN_RIGHT_SQUARE, //右方括号

    TOKEN_COMMA,        //逗号

    TOKEN_QUOTATION,   //双引号

    TOKEN_NULL,         //NULL

    TOKEN_FLAG, //斜杠
    TOKEN_RE_FLAG,   //反斜杠
    TOKEN_COLON,    //冒号
    TOKEN_EOF
}TokenType;

//保留字结构体
struct keywordToken{
    char* keyword;
    int length;
    TokenType token;
};

其实写这种程序的前端部分都是大同小异的,如果你要写编程语言的编译器的话,那么把关键字扩充就行了,比如你定义变量的时候要写成 var 变量名, 那么var就应该成为你的保留字,你在解析token的时候应该能要识别出来。

//关键字查找表
struct keywordToken keywords[] = {
    {"{", 1, TOKEN_LEFT_BRACE},
    {"}", 1, TOKEN_RIGHT_BRACE},
    {"[", 1, TOKEN_LEFT_SQUARE},
    {"]", 1, TOKEN_RIGHT_SQUARE},
    {",", 1, TOKEN_COMMA},
    {"\"", 1, TOKEN_QUOTATION},
    {"null", 4, TOKEN_NULL},
    {"\\", 1, TOKEN_FLAG},
    {"/", 1, TOKEN_RE_FLAG},
    {":", 1, TOKEN_COLON},
    {"EOF", 1, TOKEN_EOF},
    {NULL, 0, TOKEN_UNKNOWN}
};

在parser类中我顶一个char类型的ch属性,用来接收文件中的下一个字符,之前我是让每个函数单独有一个ch解析,后来我发现这样不利于同于,于是将ch作为类的一个属性,这样也给词法分析带来了很多方便,这算是一个小经验吧,我感觉在用面向对象编程的时候,如果能把变量写成类的属性还是写成属性比较好

pullToken()是提供给别的程序的接口,用来返回下一个token,工作过程是先跳过空格或者换行,然后解析下一个token。

Token parser::pullToken(){
    skipBlanks();
    getNextToken();
    return this->result;
}

其中skipBlanks()是跳过空格和换行,如果跳过的是换行的话,那么行数要加一,这样可以精确定位到token的位置。

PS:如果你需要提供对于注释的支持的话,你要能识别出注释符号,比如 // ,然后略去之后的注释,不过我没见过json有写注释的,所以也就没有提供对注释的支持了。

//这个函数运行完后ch为第一个不是空格或者换行的字符
void parser::skipBlanks(){
    while(ch == ' ' || ch == '\n'){
        if(ch == '\n'){
            this->line++;
        }
        ch = fgetc(fp);
        //printf("skip : %c\n", ch);
    }
}

getNextToken()获取下一个token

先把一些关键符号 比如 {} [] , 等识别出来,如果都不是的话再看看是不是别的关键字,如果都不是的话就是TOKEN_UNKNOWN,这就是json文件中我们需要的数据

在解析TOKEN_UNKNOWN的时候是一直读文件,知道遇到第一个换行或者空格位置,然后和关键字表进行比较,看看是不是null类型的token(我们这里只识别一个关键字,bool是我当时拉下的,所以就放在语义分析中解析了),实际上调用isKeyword()之后就会返回TOKEN_UNKNOWN或者TOKEN_NULL,不需要自己做其他动作了。


void parser::getNextToken(){
    //这个时候ch已经指向了第一个字符
    initResult();
    //先判断是不是特殊字符
    if(ch == EOF){
        this->result.type = TOKEN_EOF;
        this->result.length = 1;
        strcpy(this->result.token, "EOF");
         this->result.lineNo = this->line;
        return;
    }
    if(ch == ','){
        this->result.type = TOKEN_COMMA;
        this->result.length = 1;
        strcpy(this->result.token, ",");
         this->result.lineNo = this->line;
        ch = fgetc(fp);
        return;
    }
    if(ch == '"'){
        this->result.type = TOKEN_QUOTATION;
        this->result.length = 1;
        strcpy(this->result.token, "\"");
         this->result.lineNo = this->line;
        ch = fgetc(fp);
        return;
    }
    if(ch == '{'){
        this->result.type = TOKEN_LEFT_BRACE;
        this->result.length = 1;
        strcpy(this->result.token, "{");
         this->result.lineNo = this->line;
        ch = fgetc(fp);
        return;
    }
    if(ch == '}'){
        this->result.type = TOKEN_RIGHT_BRACE;
        this->result.length = 1;
        strcpy(this->result.token, "}");
        this->result.lineNo = this->line;
        ch = fgetc(fp);
        return;
    }
    if(ch == '['){
        this->result.type = TOKEN_LEFT_SQUARE;
        this->result.length = 1;
        strcpy(this->result.token, "[");
         this->result.lineNo = this->line;
        ch = fgetc(fp);
        return;
    }
    if(ch == ']'){
        this->result.type = TOKEN_RIGHT_SQUARE;
        this->result.length = 1;
        strcpy(this->result.token, "]");
         this->result.lineNo = this->line;
        ch = fgetc(fp);
        return;
    }
    if(ch == '/'){
        this->result.type = TOKEN_RE_FLAG;
        this->result.length = 1;
        strcpy(this->result.token, "/");
         this->result.lineNo = this->line;
        ch = fgetc(fp);
        return;
    }
    if(ch == '\\'){
        this->result.type = TOKEN_FLAG;
        this->result.length = 1;
        strcpy(this->result.token, "\\");
         this->result.lineNo = this->line;
        ch = fgetc(fp);
        return;
    }
    if(ch == ':'){
        this->result.type = TOKEN_COLON;
        this->result.length = 1;
        strcpy(this->result.token, ":");
         this->result.lineNo = this->line;
        ch = fgetc(fp);
        return;
    }
    //到这里就不会是上面几种符号了
    while(ch != ' ' && ch != '\n'){
        if(ch!=',' && ch!='\"' && ch!='/' && ch!='\\' && ch!=':' && ch!='{' &&
            ch!='}' && ch!='[' && ch!=']'){
        this->result.token[this->result.length] = ch;
        this->result.length++;
        ch = fgetc(fp);
        //printf("gettoken : %c\n", ch);
        } else {    //等于其中任意一种
            //this->result.token[this->result.length] = '\0';
            this->result.type = isKeyword(this->result.token, this->result.length);
            this->result.lineNo = this->line;
            return;
        }
    }
}

isKeyword()关键字比较函数用于匹配我们先前定义好的关键字

这个函数我几乎是完全照搬《自制编程语言》的,不得不说,作者写的很漂亮

主要的算法就是用传入的字符串和关键字表进行比较,如果和表中某一项是同一个字符串的话,那么就认定token是关键字,返回TokenType枚举

关键字表中最后一项是NULL,用来结束搜索,也就是说如果比较到NULL的话就说明比较结束,当前token类型应该是TOKEN_UNKNOWN,即不是关键字

TokenType parser::isKeyword(char a[], int length){
    int idx = 0;
        while(keywords[idx].keyword != NULL){
            if(length == keywords[idx].length &&
                memcmp(keywords[idx].keyword, a, length) == 0){
                    return keywords[idx].token;
                }
            idx++;
        }
        return TOKEN_UNKNOWN;   //不是关键字
}

到这里差不多就把parser介绍完了

后端部分

这个部分的功能就是根据吃的token来判定要表达的意思,然后将数据存入数据结构中

我们先来观察一下json文件格式

{
    "Tony":{
        "age" : 18,
        "major" : "computer science",
        "course" : [
            "calculus", "linear algebra"
        ],
        "isMonitor" : false
    },
    "Mary":{
        "age" : 19,
        "major" : "physics",
        "course" : [
            "machanics", "electromagnetism"
        ],
        "isMonitor" : true
    }
}

最外层是一个花括号(代表键值对),其中值可以是嵌套的花括号或者列表或者是普通的string,数字等值。

理论上来说应该是可以无限嵌套的,列表里面也能嵌套一对花括号,这个项目的主要难点就在这里。

之前的我也是觉得这里无从下手,后来在课上想到了一个方法可以解决嵌套问题,机智的我,嘿嘿((/ω\))

 

解析的算法

我们可以观察到,json的数据是以括号为单位的,比如遇到{ 那么就意味着有一个OBJECT类型的对象待解析,遇到[就意味着有一个LIST类型的对象待解析,我们先来看一下入口函数

void explorer::dump(){
    if(token.type == TOKEN_LEFT_BRACE){
        this->value = analysisBrace();
    } else {
        formatError_runner(this->token, '{');
    }
}

 

先判断token的类型,如果是左花括号那么就继续解析(因为json文件都是以左花括号开始的),调用解析花括号的函数

formatError_runner()是错误抛出函数,我都放到error.h中了

花括号解析函数

Value explorer::analysisBrace(){
    printf("object analysis\n");
    //只要不是左括号就继续解析
    Value v;
    v.type = OBJECT;    //json对象
    while(1){
        //key值必须是字符串
        this->token = this->p.pullToken();
        if(token.type != TOKEN_QUOTATION){
            formatError_runner(token, '"');
            return error;
        }
        this->token = this->p.pullToken();
        if(token.type != TOKEN_UNKNOWN){
            stringError(this->token);
            return this->error;
        }
        //到这里,就需要申请一个Value做为key值了
        string str;
        str += this->token.token;
        //还是解析双引号
        this->token = this->p.pullToken();
        if(this->token.type != TOKEN_QUOTATION){
            formatError_runner(token, '"');
           return this->error;
        }
        //解析冒号
       this->token = this->p.pullToken();
        if(this->token.type != TOKEN_COLON){
            formatError_runner(token, ':');
           return this->error;
        }
        //继续解析值
        Value  val;    //作为值
        this->token = this->p.pullToken();
        if(this->token.type == TOKEN_LEFT_BRACE){
            //如果是左括号就递归调用解析对象
            val = analysisBrace();  //递归调用
        } else if(this->token.type == TOKEN_LEFT_SQUARE){
            val = analysisSquare(); //解析列表值
        } else if(this->token.type == TOKEN_NULL){
            //是null值
            val.type = null;
            val.str += "null";
        } else if(this->token.type == TOKEN_UNKNOWN){
            //看看是不是布尔值
            if(strcmp(token.token, "true")==0 ||
                strcmp(token.token, "false")==0){
                    val.type = BOOL;
            }else {
                //认为是数字量
                val.type = NUMBER;
            }
            val.str += token.token;
        } else if(this->token.type == TOKEN_QUOTATION){
            //认为是字符量,注意,字符量可以有空格的
            this->token = this->p.pullToken();
            val.type = STRING;
            val.str += token.token;
            this->token = this->p.pullToken();
            while(this->token.type == TOKEN_UNKNOWN){
                val.str += " "; //两个字符量之间加一个空格
                val.str += token.token;
                this->token = this->p.pullToken();
            }
            //当不是字面量的时候需要一个双引号,否则报错
            if(token.type != TOKEN_QUOTATION){
                formatError_runner(token, '"');
               return this->error;
            }
        }
        //解析完后要将object insert
        v.object[str] = val;
        v.count++;

        //看看是不是逗号
        this->token = this->p.pullToken();
        
        if(this->token.type == TOKEN_COMMA){
            continue;   //如果是逗号就进入下一轮解析
        } else if(this->token.type == TOKEN_RIGHT_BRACE){
            break;//这一层解析结束
        } else {
            //报错,期望逗号或者左花括号
            formatError_runner(this->token, ',', '}');
            return this->error;
        }
    }
    return v;
}

返回值是Value类型的数据,我们观察json文件会发现,json数据是周期性出现的:

1. 吃 ' " ', 吃 STRING 字面量, 吃' " '

2. 吃 ':'

3.吃下一个token,如果是 ' " ' 的话那么就是STRING类型,如果是TOKEN_UNKNOWN的话就是数字或者bool值或者null,这中间需要做一个区分,如果是 '{',那么应该递归调用自己,去解析深一层的OBJECT类型的Value, 如果是'[' ,那么应该调用列表解析函数取解析列表

4,吃下一个token,如果是逗号的话继续执行1 ,如果是右花括号那么就结束解析

我原本想用一个符号栈来进行括号匹配,后来发现不需要,因为每一层都只会有一对花括号,否则就是json写错了,所以不需要符号站了,直接遇到有括号结束解析就行了。

方括号解析函数


Value explorer::analysisSquare(){
    printf("list analysis\n");
    Value v;    //用来当做返回值
    v.type = LIST;
    while(1){
        this->token = this->p.pullToken();
        if(this->token.type == TOKEN_QUOTATION){
            //解析STRING
            this->token = this->p.pullToken();
            Value val;
            val.type = STRING;
            val.str += this->token.token;
            val.count++;
            this->token = this->p.pullToken();
            while(this->token.type == TOKEN_UNKNOWN){
                val.str += " ";
                val.str += this->token.token;
                this->token = this->p.pullToken();
            }
            v.list.push_back(val);
            v.count++;
            //找到双引号
            if(this->token.type != TOKEN_QUOTATION){
                formatError_runner(this->token, '"');
                return this->error;
            }
        } else if(this->token.type == TOKEN_UNKNOWN){
            //解析bool值或者数字
            if(strcmp(this->token.token, "true") == 0||
               strcmp(this->token.token, "false") == 0){
                   Value val;
                   val.type = BOOL;
                   val.str += this->token.token;
                   v.list.push_back(val);
                   v.count++;
            } else {    //是数字
                Value val;
                val.type = NUMBER;
                val.str += this->token.token;
                v.list.push_back(val);
                v.count++;
            }
        } else if(this->token.type == TOKEN_LEFT_BRACE){
            Value val = analysisBrace();
            v.list.push_back(val);
            v.count++;
        } else if(this->token.type == TOKEN_LEFT_SQUARE){
            Value val = analysisSquare();
            v.list.push_back(val);
            v.count++;
        }
        this->token = this->p.pullToken();
        if(this->token.type == TOKEN_COMMA){
            continue;   //遇到逗号就继续解析
        } else if(this->token.type == TOKEN_RIGHT_SQUARE){
            break;  //解析结束
        } else {
            formatError_runner(this->token, ',', ']');
        }
    }
    return v;
}

解析思路和上面大体相同:

1. 吃token,如果是 ' " '就继续吃token作为STRING类型的Value添加入列表中, 然后吃' " '

                     如果是TOKEN__UNKNOWN 就看看是 NUMBER, BOOL 还是NULL类型的

                     如果是花括号就调用花括号解析,如果是方括号就调用方括号解析

2.吃下一个token,如果是逗号的话就继续执行1,否则解析结束,返回当前解析的Value值

数据的遍历

遍历入口函数

void explorer::traverse(){
    //Value只有两种对象 json对象和列表
    this->num = 0; //每行缩进个数
    traverseObject(value);
}

遍历的时候也是先调用object类型的遍历,如果是列表或者其他类型的话,我们在traverseObject()会有相应的策略。

遍历OBJECT类型的Value


void explorer::traverseObject(Value val){
    this->num++;
    if(val.type != OBJECT){
        if(val.type == null){
            printf("null\n");
        } else if(val.type == NUMBER){
            printf("(NUMBER):%s\n", val.str.c_str());
        } else if(val.type == STRING){
            printf("(STRING):\"%s\"\n", val.str.c_str());
        } else if(val.type == BOOL){
            printf("(BOOL):%s\n", val.str.c_str());
        } else if(val.type == ERROR){
            traverseError();
        } else if(val.type == LIST){
            traverseList(val);
        }
        return;
    }
    printf("{\n");
    map<string, Value>::iterator it = val.object.begin();
    for(; it!= val.object.end(); ){
        for(int i=0; i<this->num; i++){
            printf("    ");
        }
        //注意,it-first 是string类型的变量
        printf("\"%s\" : ", it->first.c_str());
        if(it->second.type == null){
            printf("null");
        } else if(it->second.type == NUMBER){
            printf("%s", it->second.str.c_str());
        } else if(it->second.type == STRING){
            printf("\"%s\"", it->second.str.c_str());
        } else if(it->second.type == LIST){
            traverseList(it->second);
        } else if(it->second.type == OBJECT){
            traverseObject(it->second);
        } else if(it->second.type == BOOL){
            printf("%s", it->second.str.c_str());
        }
        it++;
        if(it != val.object.end()) {
            printf(",\n");
        } else {
            printf("\n");
        }
    }
    this->num--;
    for(int i=0; i<this->num; i++){
        printf("    ");
    }
    printf("}");
    
}

算法如下:

最外层是map<string, Value>的遍历

对于每一个键值对中的value, 如果当前value是null,数字,字符串或者bool的话,直接输出即可,如果是LIST类型的话调用下面的traverseList()进行遍历,如果是OBJECT类型的话调用自身遍历

遍历LIST类型的Value


void explorer::traverseList(Value val){
    if(val.type != LIST){
        printf("ERROR: the type of val is not LIST\n");
        return;
    }
    this->num++;
    printf("[\n");
    vector<Value>::iterator it = val.list.begin();
    for(int i=0; i<this->num; i++){
            printf("    ");
    }
    for(; it != val.list.end(); ){
        
        if(it->type == null){
            printf("null\n");
        } else if (it->type == NUMBER){
            printf("%s ", it->str.c_str());
        } else if(it->type == STRING){
            printf("\"%s\" ", it->str.c_str());
        } else if(it->type == LIST){
            traverseList(*it);
        } else if(it->type == OBJECT){
            traverseObject(*it);
        } else if(it->type == BOOL){
            printf("\"%s\" ", it->str.c_str());
        } 
        it++;
        if(it !=val.list.end()) {
            printf(",");
        }
    }
    this->num--;
    printf("\n");
    for(int i=0; i<this->num; i++){
        printf("    ");
    }
    printf("]");
    
}

思路和上面很相似,最外层是vector<Value>的遍历,对于每一个元素,如果是null,字符串,数字或者bool的话就直接输出,如果是OBJECT类型的话就调用traverseObject(),如果是LIST类型的话就调用自身解析

最后再说一下输出时候的缩进问题,我在explorer类中定义了一个num属性,用于记录缩进的个数,遍历开始的时候初始化为1,然后每进入一层遍历函数traverseObject()或者traverseList()的时候num++,每退出一层遍历函数的时候num--,然后再输出的时候根据num值打印缩进就行啦。

好啦,到这里差不多就介绍完啦,感谢各位大佬耐心看我的文章,Thanks♪(・ω・)ノ

发布了130 篇原创文章 · 获赞 151 · 访问量 19万+

猜你喜欢

转载自blog.csdn.net/haohulala/article/details/89784831