JSON字符串解析

JSON格式介绍

JSON(JavaScript Object Notation)
,是一种序列化的格式,最大的优点在于可读性极强,以及可直接嵌入到js代码中,所以广泛运用于web数据的收发。

JSON格式有以下基本类型:

  • null类型:值为null,表示为空
  • bool类型:值为true和false
  • number类型:值为int、double(即整数或小数
  • string类型:形如 “abc”

以及以下复合类型:

  • list类型(也称array类型)
  • dict类型(也称object类型)

解析json字符串

整套解析流程如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WwkHLTjs-1660653072769)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/231baf4270a64bf6aa38ca5961507611~tplv-k3u1fbpfcp-watermark.image?)]

创建JObject类

我们需要把json的类型对应到计算机语言的类型。
由于json的数据在我们看来都是字符串,那么有如下对应关系:

  • "null"对应我们构造的null类型
  • “true","false"对应内部的bool类型即可
  • number类型数据对应int、double类型
  • string类型数据对应string即可
  • list类型对应C++中的vector
  • dict类型对应C++中的map或unordered_map

我们在计算机语言中,需要构造一个对象类型,用于将以上类型全部涵盖。

在C++中通过std::variant来进行,还需要一个枚举tag来表示当前对象内存储的数据类型。

enum TYPE
{
T_NULL,
T_BOOL,
T_INT,
T_DOUBLE,
T_STR,
T_LIST,
T_DICT
};

using null_t = string;
using int_t = int32_t;
using bool_t = bool;
using double_t = double;
using str_t = string;
using list_t = vector<JObject>;
using dict_t = map<string, JObject>;

class JObject
{
public:
using value_t = variant<bool_t, int_t, double_t, str_t, list_t, dict_t>;
...
private:
TYPE m_type;
value_t m_value;
};

创建Parser类

我们有了JObject,可以把所有的JSON数据接收起来,现在要做的就是扫描JSON字符串,对其中的数据进行读取处理,然后转化为JObject。
关键代码如下:

JObject Parser::parse()
{
    char token = get_next_token();
    if (token == 'n')
    {
        return parse_null();
    }
    if (token == 't' || token == 'f')
    {
        return parse_bool();
    }
    if (token == '-' || std::isdigit(token))
    {
        return parse_number();
    }
    if (token == '\"')
    {
        return parse_string();
    }
    if (token == '[')
    {
        return parse_list();
    }
    if (token == '{')
    {
        return parse_dict();
    }

    throw std::logic_error("unexpected character in parse json");
}

以上就是整个字符串的解析过程,每次通过get_next_token这个方法得到整个字符串的下一个token,根据token决定解析对应的数据类型。

get_next_token方法

跳过空白符号,以及跳过注释(标准的JSON格式不支持注释,我这里硬加的,为了vscode的JSON格式配置文件解析

char Parser::get_next_token()
{
    while (std::isspace(m_str[m_idx])) m_idx++;
    if (m_idx >= m_str.size())
        throw std::logic_error("unexpected character in parse json");
    //如果是注释,记得跳过
    skip_comment();
    return m_str[m_idx];
}
parse_null和parse_bool

由于这两个很简单,就放在一起了。

  • parse_null
JObject Parser::parse_null()
{
    if (m_str.compare(m_idx, 4, "null") == 0)
    {
        m_idx += 4;
        return {};
    }
    throw std::logic_error("parse null error");
}
  • parse_bool
bool Parser::parse_bool()
{
    if (m_str.compare(m_idx, 4, "true") == 0)
    {
        m_idx += 4;
        return "true";
    }
    if (m_str.compare(m_idx, 5, "false") == 0)
    {
        m_idx += 5;
        return "false";
    }
    throw std::logic_error("parse bool error");
}
parse_number
JObject Parser::parse_number()
{
    auto pos = m_idx;
    //integer part
    if (m_str[m_idx] == '-')
    {
        m_idx++;
    }
    if (isdigit(m_str[m_idx]))
        while (isdigit(m_str[m_idx]))
            m_idx++;
    else
    {
        throw std::logic_error("invalid character in number");
    }

    if (m_str[m_idx] != '.')
    {
        return (int) strtol(m_str.c_str() + pos, nullptr, 10);
    }

    //decimal part
    if (m_str[m_idx] == '.')
    {
        m_idx++;
        if (!std::isdigit(m_str[m_idx]))
        {
            throw std::logic_error("at least one digit required in parse float part!");
        }
        while (std::isdigit(m_str[m_idx]))
            m_idx++;
    }
    return strtof64(m_str.c_str() + pos, nullptr);
}
parse_list
JObject Parser::parse_list()
{
    JObject arr((list_t()));//得到list类型的JObject
    m_idx++;
    char ch = get_next_token();
    if (ch == ']')
    {
        m_idx++;
        return arr;
    }

    while (true)
    {
        arr.push_back(parse());
        ch = get_next_token();
        if (ch == ']')
        {
            m_idx++;
            break;
        }

        if (ch != ',') //如果不是逗号
        {
            throw std::logic_error("expected ',' in parse list");
        }
        //跳过逗号
        m_idx++;
    }
    return arr;
}
parse_dict
JObject Parser::parse_dict()
{
    JObject dict((dict_t()));//得到dict类型的JObject
    m_idx++;
    char ch = get_next_token();
    if (ch == '}')
    {
        m_idx++;
        return dict;
    }
    while (true)
    {
        //解析key
        string key = std::move(parse().Value<string>());
        ch = get_next_token();
        if (ch != ':')
        {
            throw std::logic_error("expected ':' in parse dict");
        }
        m_idx++;

        //解析value
        dict[key] = parse();
        ch = get_next_token();
        if (ch == '}')
        {
            m_idx++;
            break; //解析完毕
        }
        if (ch != ',')//没有结束,此时必须为逗号
        {
            throw std::logic_error("expected ',' in parse dict");
        }
        //跳过逗号
        m_idx++;
    }
    return dict;
}

完善JObject类

很明显,我们需要为JObject类提供一个方法,此方法可以让调用者直接访问到std::variant里面对应的数据,并且我们也需要提供一个方法能让JObject快速初始化为对应的类型。

初始化接口

添加好下面这些方法后,外界可通过调用方法把JObject的内部状态改变。

void Null()
{
    m_type = T_NULL;
    m_value = "null";
}

void Int(int_t value)
{
    m_value = value;
    m_type = T_INT;
}

void Bool(bool_t value)
{
    m_value = value;
    m_type = T_BOOL;
}

void Double(double_t value)
{
    m_type = T_DOUBLE;
    m_value = value;
}

void Str(string_view value)
{
    m_value = string(value);
    m_type = T_STR;
}

void List(list_t value)
{
    m_value = std::move(value);
    m_type = T_LIST;
}

void Dict(dict_t value)
{
    m_value = std::move(value);
    m_type = T_DICT;
}

Value方法:

#define THROW_GET_ERROR(erron) throw std::logic_error("type error in get "#erron" value!")

template<class V>
    V &Value()
{
    //添加安全检查
    if constexpr(IS_TYPE(V, str_t))
    {
        if (m_type != T_STR)
            THROW_GET_ERROR(string);
    } else if constexpr(IS_TYPE(V, bool_t))
    {
        if (m_type != T_BOOL)
            THROW_GET_ERROR(BOOL);
    } else if constexpr(IS_TYPE(V, int_t))
    {
        if (m_type != T_INT)
            THROW_GET_ERROR(INT);
    } else if constexpr(IS_TYPE(V, double_t))
    {
        if (m_type != T_DOUBLE)
            THROW_GET_ERROR(DOUBLE);
    } else if constexpr(IS_TYPE(V, list_t))
    {
        if (m_type != T_LIST)
            THROW_GET_ERROR(LIST);
    } else if constexpr(IS_TYPE(V, dict_t))
    {
        if (m_type != T_DICT)
            THROW_GET_ERROR(DICT);
    }

    void *v = value();
    if (v == nullptr)
        throw std::logic_error("unknown type in JObject::Value()");
    return *((V *) v); //强转即可
}

重载方法让对象更好用

当JObject为dict类型时,我们可以直接用下标运算符进行key-value的赋值(得益于隐式转化和运算符重载

JObject &operator[](string const &key)
{
    if (m_type == T_DICT)
    {
        auto &dict = Value<dict_t>();
        return dict[key];
    }
    throw std::logic_error("not dict type! JObject::opertor[]()");
}

同样如果为list对象,我们也准备了push_back等方法

void push_back(JObject item)
{
    if (m_type == T_LIST)
    {
        auto &list = Value<list_t>();
        list.push_back(std::move(item));
        return;
    }
    throw std::logic_error("not a list type! JObjcct::push_back()");
}

当然,为了让类更好用,你也可以重载很多其他方法,但是注意别忘了类型的安全检查!

完善Parser类

我们之前是完成了整个字符串到JObject的解析过程,但是每次都需要创建一个Parser类,然后调用方法,这样的过程未免有些繁琐,我们可以对外提供FromString的静态方法,然后充分利用一个对象便可完成整个解析过程。

如下:

JObject Parser::FromString(string_view content)
{
    static Parser instance;
    instance.init(content);
    return instance.parse();
}

完成序列化和反序列化过程

我们前面所做的工作其实已经把这个过程相当于完成了,当然还差一个把JObject变为JSON字符串的方法,现在添加上就是,也不是很难,按照相似的逻辑反推一遍就行。如下给JObject添加一个to_string方法:

string JObject::to_string()
{
    void *value = this->value();
    std::ostringstream os;
    switch (m_type)
    {
        case T_NULL:
            os << "null";
            break;
        case T_BOOL:
            if (GET_VALUE(bool))
                os << "true";
            else os << "false";
            break;
        case T_INT:
            os << GET_VALUE(int);
            break;
        case T_DOUBLE:
            os << GET_VALUE(double);
            break;
        case T_STR:
            os << '\"' << GET_VALUE(string) << '\"';
            break;
        case T_LIST:
        {
            list_t &list = GET_VALUE(list_t);
            os << '[';
            for (auto i = 0; i < list.size(); i++)
            {
                if (i != list.size() - 1)
                {
                    os << ((list[i]).to_string());
                    os << ',';
                } else os << ((list[i]).to_string());
            }
            os << ']';
            break;
        }
        case T_DICT:
        {
            dict_t &dict = GET_VALUE(dict_t);
            os << '{';
            for (auto it = dict.begin(); it != dict.end(); ++it)
            {
                if (it != dict.begin()) //为了保证最后的json格式正确
                    os << ',';
                os << '\"' << it->first << "\":" << it->second.to_string();
            }
            os << '}';
            break;
        }
        default:
            return "";
    }
    return os.str();
}

有关JObject的方法也都补充的差不多了,那么我们现在要考虑的是如何通过JObject这个中间对象将我们自定义的任何一个类给序列化和反序列化?

如图:

JSON字符串解析过程

所有的序列化和反序列化的过程都依托于JObject进行。而Parser这个类在中间作为一个方便使用的对外接口。

序列化接口设计

在Parser类中添加一个ToJSON的静态方法,用来对任意类型进行序列化,这个方法使用模板。

代码如下:

template<class T>
    static string ToJSON(T const &src)
{
    //如果是基本类型
    if constexpr(IS_TYPE(T, int_t))
    {
        JObject object(src);
        return object.to_string();
    } else if constexpr(IS_TYPE(T, bool_t))
    {
        JObject object(src);
        return object.to_string();
    } else if constexpr(IS_TYPE(T, double_t))
    {
        JObject object(src);
        return object.to_string();
    } else if constexpr(IS_TYPE(T, str_t))
    {
        JObject object(src);
        return object.to_string();
    }
    //如果是自定义类型调用方法完成dict的赋值,然后to_string即可
    json::JObject obj((json::dict_t()));
    src.FUNC_TO_NAME(obj);
    return obj.to_string();
}

如上代码如果是基本类型则直接初始化JObject调用to_string方法,如果是自定义类型,则需要在该类型中嵌入一个方法,这个方法名字我们用FUNC_TO_NAME这个宏代替。

也就是说,如果是自定义的类型,那么肯定就是对应JSON数据的dict类型,所以只需要你对该类型定义对应的方法,该方法需要将值传递给JObject,为了简化这个过程我们用宏来替代。

宏定义如下:

#define FUNC_TO_NAME _to_json
#define FUNC_FROM_NAME _from_json

#define START_TO_JSON void FUNC_TO_NAME(json::JObject & obj) const{
#define to(key) obj[key]
    //push一个自定义类型的成员
#define to_struct(key, struct_member) json::JObject tmp((json::dict_t())); struct_member.FUNC_TO_NAME(tmp); obj[key] = tmp
#define END_TO_JSON  }

#define START_FROM_JSON void FUNC_FROM_NAME(json::JObject& obj) {
#define from(key, type) obj[key].Value<type>()
#define from_struct(key, struct_member) struct_member.FUNC_FROM_NAME(obj[key])
#define END_FROM_JSON }

基本使用如下:

struct Base
{
    int pp;
    string qq;

    START_FROM_JSON //生成反序列化相关的方法
        pp = from("pp", int);
        qq = from("qq", string);
    END_FROM_JSON

    START_TO_JSON //生成序列化相关的代码
        to("pp") = pp;
        to("qq") = qq;
    END_TO_JSON
};

struct Mytest
{
    int id;
    std::string name;
    Base q;

    START_TO_JSON	//序列化相关代码
        to_struct("base", q);
        to("id") = id;
        to("name") = name;
    END_TO_JSON

    START_FROM_JSON //反序列化相关代码
        id = from("id", int);
        name = from("name", string);
        from_struct("base", q);
    END_FROM_JSON
};

实际上上面代码等效于下面的代码,以Base类为例:

struct Base
{
    int pp;
    string qq;

    void _from_json(json::JObject& obj){ //反序列化
        pp = obj.Value<int>();
        qq = obj.Value<string>();
    }
    
    void _to_json(json::JObject& obj){//序列化代码
        obj["pp"] = pp;
        obj["qq"] = qq;
    }
};

欢迎使用Markdown编辑器

你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。

新的改变

我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:

  1. 全新的界面设计 ,将会带来全新的写作体验;
  2. 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
  3. 增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
  4. 全新的 KaTeX数学公式 语法;
  5. 增加了支持甘特图的mermaid语法1 功能;
  6. 增加了 多屏幕编辑 Markdown文章功能;
  7. 增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
  8. 增加了 检查列表 功能。

功能快捷键

撤销:Ctrl/Command + Z
重做:Ctrl/Command + Y
加粗:Ctrl/Command + B
斜体:Ctrl/Command + I
标题:Ctrl/Command + Shift + H
无序列表:Ctrl/Command + Shift + U
有序列表:Ctrl/Command + Shift + O
检查列表:Ctrl/Command + Shift + C
插入代码:Ctrl/Command + Shift + K
插入链接:Ctrl/Command + Shift + L
插入图片:Ctrl/Command + Shift + G
查找:Ctrl/Command + F
替换:Ctrl/Command + G

合理的创建标题,有助于目录的生成

直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC语法后生成一个完美的目录。

如何改变文本的样式

强调文本 强调文本

加粗文本 加粗文本

标记文本

删除文本

引用文本

H2O is是液体。

210 运算结果是 1024.

插入链接与图片

链接: link.

图片: Alt

带尺寸的图片: Alt

居中的图片: Alt

居中并且带尺寸的图片: Alt

当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。

如何插入一段漂亮的代码片

博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片.

// An highlighted block
var foo = 'bar';

生成一个适合你的列表

  • 项目
    • 项目
      • 项目
  1. 项目1
  2. 项目2
  3. 项目3
  • 计划任务
  • 完成任务

创建一个表格

一个简单的表格是这么创建的:

项目Value
电脑$1600
手机$12
导管$1

设定内容居中、居左、居右

使用:---------:居中
使用:----------居左
使用----------:居右

第一列第二列第三列
第一列文本居中第二列文本居右第三列文本居左

SmartyPants

SmartyPants将ASCII标点字符转换为“智能”印刷标点HTML实体。例如:

TYPEASCIIHTML
Single backticks'Isn't this fun?'‘Isn’t this fun?’
Quotes"Isn't this fun?"“Isn’t this fun?”
Dashes-- is en-dash, --- is em-dash– is en-dash, — is em-dash

创建一个自定义列表

Markdown
Text-to- HTML conversion tool
Authors
John
Luke

如何创建一个注脚

一个具有注脚的文本。2

注释也是必不可少的

Markdown将文本转换为 HTML

KaTeX数学公式

您可以使用渲染LaTeX数学表达式 KaTeX:

Gamma公式展示 Γ ( n ) = ( n − 1 ) ! ∀ n ∈ N \Gamma(n) = (n-1)!\quad\forall n\in\mathbb N Γ(n)=(n1)!nN 是通过欧拉积分

Γ ( z ) = ∫ 0 ∞ t z − 1 e − t d t   . \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. Γ(z)=0tz1etdt.

你可以找到更多关于的信息 LaTeX 数学表达式here.

新的甘特图功能,丰富你的文章

Mon 06 Mon 13 Mon 20 已完成 进行中 计划一 计划二 现有任务 Adding GANTT diagram functionality to mermaid
  • 关于 甘特图 语法,参考 这儿,

UML 图表

可以使用UML图表进行渲染。 Mermaid. 例如下面产生的一个序列图:

张三 李四 王五 你好!李四, 最近怎么样? 你最近怎么样,王五? 我很好,谢谢! 我很好,谢谢! 李四想了很长时间, 文字太长了 不适合放在一行. 打量着王五... 很好... 王五, 你怎么样? 张三 李四 王五

这将产生一个流程图。:

链接
长方形
圆角长方形
菱形
  • 关于 Mermaid 语法,参考 这儿,

FLowchart流程图

我们依旧会支持flowchart的流程图:

Created with Raphaël 2.3.0 开始 我的操作 确认? 结束 yes no
  • 关于 Flowchart流程图 语法,参考 这儿.

导出与导入

导出

如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。

导入

如果你想加载一篇你写过的.md文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。


  1. mermaid语法说明 ↩︎

  2. 注脚的解释 ↩︎

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值