发布于 2026-01-06 1 阅读
0

[C++] 从零开始编写一个简单的 JSON 解析器 引言 总体设计和策略 实现 测试 结论

[C++] 从零开始编写一个简单的 JSON 解析器

介绍

总体设计与策略

执行

测试

结论

介绍

在进行我的个人 C++ 练习(打算作为半个作品集展示)时,我需要为程序将要使用的超参数创建一个 fixture 文件。然而,与 Node.js 或 Python 等拥有 JSON 格式专用 STL 的语言不同,C++ 没有,而且向 C++ 添加第三方库也比较棘手。

因此,我决定自己编写一个 C++ JSON 解析器,虽然它比生产代码中常用的解析器要简单一些。我认为这将是我提升 C++ 技能的又一次很好的练习。

总体设计与策略

记住,我不想把这变成另一个大项目,所以我想尽量简化它。具体来说,

  • 我不会使用任何数组。
  • 我将假设该文件包含有效的 JSON 值,因为处理无效值会引入太多特殊情况。
  • 我还假设 JSON 文件以花括号 ( {, })开头和结尾。
  • 我的需求只需要三种数据类型:字符串(我将使用 `strings` 解析std::string)、整数和双精度浮点数。这些数值类型不会包含负值。

那么,我们的解析器策略应该是什么样的呢?

  • 我们使用ParseJson()一个主要的解析函数。该函数必须是递归的,以便处理嵌套的 JSON 值。
  • 我们使用双引号()和分号(:)来识别每个键。值要么以 (嵌套值)开头,要么由纯数字字符和点( )(整数或双精度浮点数){组成。.
  • 只会有两种非负基本类型:doubleint
  • 由于存在嵌套值,定义返回值的类型有些困难。这里,我们采用unionC++ 中的类型定义:


// json_parser.h

union JsonValue {
  int i;
  double d;
  std::map<std::string, JsonValue>* json;  
};


Enter fullscreen mode Exit fullscreen mode

请注意,由于我们使用指针来实现递归,因此类型std::map<std::string, JsonValue>*是联合类型的一部分。此外,由于值是在运行时确定的,因此我们不能在这里使用模板,而只能使用其他方法union

现在,让我们深入了解代码。

执行

主功能的ParseJson()逻辑流程如下,只有两个步骤。

  1. 根据文件路径读取 fixture JSON 文件。
  2. 将JSON文本数据解析为std::map对象。
  3. 我们将使用递归调用的辅助函数来处理嵌套值。
  4. 对于递归的基本情况,我们只需要解析任一int类型double值。

1. 读取文件

首先,我们的解析器需要一个 I/O 系统:我们从 JSON 文件中读取文本作为输入,并将JsonValue数据作为输出返回给调用函数。为此,我们定义了ReadFile()一个读取文本文件并返回std::string对象的函数。



// json_parser.cpp

void JsonParser::ReadFile(const std::string& filepath, std::string& output) {
  std::ifstream file(filepath);
  std::string line;

  while (std::getline(file, line)) {
    output.append(line); // append() copies the argument passed as a reference(std::string&)
  }
}


Enter fullscreen mode Exit fullscreen mode

2. 解析值

接下来,我们定义ParsePrimitive()只解析原始值(doubleint)的值。



// json_parser.cpp

// Remark: test_it is defined in json_parser.h, and it is an
// alias of std::string::iterator

JsonValue JsonParser::ParsePrimitive(const std::string& text, text_it start, text_it end) {
  std::string substr = text.substr(start - text.begin(), end - start);
  size_t float_point_index = substr.find(".");

  if (float_point_index >= (end - start)) { // integer
    return {.i = std::stoi(substr)};
  } else { // float(double)
    return {.d = std::stod(substr) };
  }
}


Enter fullscreen mode Exit fullscreen mode

然后我们实现主要逻辑,ParseJsonHelper()该逻辑负责处理此解析过程的递归性。



// json_parser.cpp

JsonValue JsonParser::ParseJsonHelper(const std::string& text, text_it& it) {
  assert(*it == '{'); // must start with the left curly bracket
  it++;

  std::map<std::string, JsonValue>* json_map = new std::map<std::string, JsonValue>;

  do {
    const auto [key, value] = RetriveKeyValuePair(text, it);
    (*json_map)[key] = value;

    while (*it == ' ' || *it == '\n') {
      it++;
    }
  } while (*it != '}');

  it++; // after '}'

  return { .json = json_map };
}


Enter fullscreen mode Exit fullscreen mode

但是,为了检索内部的键值对ParseJsonHelper(),我们将检索逻辑分离到另一个辅助函数中RetrieveKeyValuePair(),该函数返回一个(键,值)对。



// json_parser.cpp

std::pair<std::string, JsonValue> JsonParser::RetriveKeyValuePair(
  const std::string& text,
  text_it& it
) {
  assert(it != text.end());

  // ignore white spaces & line breaks
  while (*it == ' ' || *it == '\n') {
    it++;
  }

  text_it curr_it;
  std::string key;
  JsonValue value;
  // if hit a double quote for the first time, it is a key
  if (*it == '\"') {
    curr_it = ++it;
    while (*it != '\"') {
      it++;
    }

    key = text.substr(curr_it - text.begin(), it - curr_it);
    assert(*(++it) == ':'); // assert that we are parsing the key string
    it++;
  }

  // now we need to have its corresponding value
  while (*it == ' ' || *it == '\n') {
    it++;
  }

  if (*it == '{') {
    // another json format
    value = ParseJsonHelper(text, it);
  } else {
    // primitive value(double or int)
    curr_it = it;
    while (isdigit(*it) || *it == '.') {
      it++;
    }
    value = ParsePrimitive(text, curr_it, it);
  }

  // after parsing the value, check whether the current iterator points to a comma
  if (*it == ',') {
    it++;
  }

  return std::make_pair(key, value);
}


Enter fullscreen mode Exit fullscreen mode

(请注意,这两个辅助函数ParseJsonHelper()RetrieveKeyValuePair()相互递归调用。)

由于我们的算法本质上是深度优先搜索(DFS)算法,我们将文本值的迭代器(来自原始JSON文本文件)作为引用传递给ParseJsonHelper()和RetrieveKeyValuePair()。这使得每个调用堆栈都可以跟踪给定文本值对应的函数调用位置。

3. 主要解析功能

最后,ParseJson()将目前为止实现的所有功能整合在一起,以解析单个 JSON 文件。



// json_parser.cpp

JsonValue JsonParser::ParseJson(const std::string& filepath) {
  // 1. read the text data from the given file
  std::string text;
  ReadFile(filepath, text);

  // 2. parse the text with the helper function and return
  text_it start = text.begin();
  return ParseJsonHelper(text, start);
}


Enter fullscreen mode Exit fullscreen mode

4. 最终代码

由于完整代码会占用太多空间,我将提供最终代码的链接:

测试

如果能对这个函数进行一些单元测试就太好了。我们这里使用的是GoogleTest

例如,如果我们测试一个简单的 JSON 文本,如下所示:



{
  "one": 1,
  "two": {
    "three": 3,
    "four": {
      "five": 5
    }
  },
  "six": {
    "seven": 7
  }
}


Enter fullscreen mode Exit fullscreen mode

测试代码如下:



TEST(JsonParserTest, TestJsonWithNests) {
  std::string json_text = "{\n \"one\": 1,\n \"two\": {\n\"three\": 3, \n \"four\": { \n\"five\": 5 \n } \n }, \n \"six\": {\n\"seven\": 7\n } \n}";
  text_it start = json_text.begin();
  JsonValue parsed = ParseJsonHelper(json_text, start);

  JsonValue two = (*parsed.json)["two"];
  JsonValue four = (*two.json)["four"];
  JsonValue six = (*parsed.json)["six"];

  EXPECT_EQ((*parsed.json)["one"].i, 1);
  EXPECT_EQ((*two.json)["three"].i, 3);
  EXPECT_EQ((*four.json)["five"].i, 5);
  EXPECT_EQ((*six.json)["seven"].i, 7);
};


Enter fullscreen mode Exit fullscreen mode

现在看看结果:

图片描述

结论

这实际上是我第一次为自己编写工具(之前从未用其他语言(例如我更熟悉的 Python 或 Node.js)做过类似的事情)。尽管如此,我仍然不确定我的设计决策或实现方式是否正确(甚至是否符合 C++ 的最佳实践),但我确实从这个非常简单的项目中学到了很多。

  • 我从零开始编写了一个程序,没有借助任何外部资源,例如教程或任何第三方库和框架(GoogleTest 除外)。
  • 这个程序是供我个人使用的,不是作为家庭作业的一部分。
  • 我使用了一些 C++ STL,并实现了一个相当合理的(至少在我看来是这样!)递归算法。

我必须说,这个项目并不成功。我应该积累更多 C++ 经验,阅读更多书籍和开源代码,并练习更多算法,才能让我的项目尽可能完善。

然而,依我拙见,坐下来写代码才是最重要的。实践经验远比单纯读书和做 LeetCode 题更有价值。

文章来源:https://dev.to/uponthesky/c-making-a-simple-json-parser-from-scratch-250g