项目源码 https://github.com/hao297531173/jsonExplorer
之前我读过一本叫做《自制编程语言》 的书,给我的启发真的很大,这本书的源码在https://github.com/hao297531173/DIYProgramLanguage
我这次项目借鉴了书中的很多编码技巧,下面我就详细介绍一下核心代码编写思路
PS:对外接口的写的不多,如果你觉得这个项目很有用的话,可以自己写接口
#include
#include
#include
#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
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
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 list; //用来记录列表
map 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
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来判定要表达的意思,然后将数据存入数据结构中
{
"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::iterator it = val.object.begin();
for(; it!= val.object.end(); ){
for(int i=0; inum; 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; inum; i++){
printf(" ");
}
printf("}");
}
算法如下:
最外层是map
对于每一个键值对中的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::iterator it = val.list.begin();
for(int i=0; inum; 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; inum; i++){
printf(" ");
}
printf("]");
}
思路和上面很相似,最外层是vector
最后再说一下输出时候的缩进问题,我在explorer类中定义了一个num属性,用于记录缩进的个数,遍历开始的时候初始化为1,然后每进入一层遍历函数traverseObject()或者traverseList()的时候num++,每退出一层遍历函数的时候num--,然后再输出的时候根据num值打印缩进就行啦。
好啦,到这里差不多就介绍完啦,感谢各位大佬耐心看我的文章,Thanks♪(・ω・)ノ