JSON 详解与解析方法对比报告

基于 Clock 项目中 weather.c 的实际代码,从零开始讲清楚 JSON 是什么、为什么用它、怎么解析它,以及项目用的”手搓解析”和专业库的对比。


第一部分:JSON 是什么?为什么要用它?

1.1 JSON 的本质——一种”文字格式的数据包”

想象你要从网络上请求天气数据,服务器需要把”城市名、天气状况、温度、天气代码”这几个数据发给你。服务器和你的单片机是两台完全不同的机器,如何才能让双方都看懂这些数据?

最简单的方式是:约定一种文本格式,双方都按照这个格式来读写数据。JSON(JavaScript Object Notation)就是目前互联网上最流行的这种”通用数据格式”。

1.2 JSON 长什么样

本项目中,向心知天气 API 发请求后,服务器会返回这样一段文字:

{
  "results": [
    {
      "location": {
        "id": "WTEMH46Z5N09",
        "name": "Hefei",
        "country": "CN",
        "path": "Hefei,Hefei,Anhui,China",
        "timezone": "Asia/Shanghai",
        "timezone_offset": "+08:00"
      },
      "now": {
        "text": "Cloudy",
        "code": "4",
        "temperature": "32"
      },
      "last_update": "2025-07-26T16:30:00+08:00"
    }
  ]
}

这就是服务器发过来的原始字符串,里面包含了我们需要的:城市名(name)、天气描述(text)、天气代码(code)、温度(temperature)。

1.3 JSON 的语法规则(非常简单)

语法元素写法含义
对象{ "key": value }用花括号包裹,键值对集合
数组[ item1, item2 ]用方括号包裹,有顺序的列表
字符串"Hello"必须用双引号
数字323.14不加引号
布尔true / false小写
嵌套对象里面可以再套对象或数组形成树状结构

1.4 为什么选 JSON,而不是其他格式?

格式优点缺点
JSON人类可读、格式简洁、各语言都支持有冗余的引号和括号
XML有标准、自描述性强标签太啰嗦,体积大
纯文本(CSV)极简没有层级结构,复杂数据无法表示
二进制协议体积小、速度快人类不可读,调试困难

结论: JSON 是”易读性”和”简洁性”的最佳平衡点,成为互联网 API 的默认标准。


第二部分:解析 JSON 的两种方法对比

方法 A:使用专业 JSON 库(cJSON)——标准做法

在 PC 或手机开发中,几乎都会使用专门的 JSON 解析库。嵌入式领域最流行的是 cJSON(轻量级,专为 MCU 设计)。

cJSON 解析同一个天气 JSON 的代码:

#include "cJSON.h"
 
bool parse_weather_with_cjson(const char *response, weather_info_t *info)
{
    // 第一步:把 JSON 字符串解析成一棵"树"(内存中的结构)
    cJSON *root = cJSON_Parse(response);
    if (root == NULL) return false;  // 解析失败(格式不对)
 
    // 第二步:像查字典一样,按键名逐级找到要的数据
    // 找 results 数组
    cJSON *results = cJSON_GetObjectItem(root, "results");
    if (!cJSON_IsArray(results)) goto cleanup;
 
    // 取数组第0个元素
    cJSON *first = cJSON_GetArrayItem(results, 0);
    if (first == NULL) goto cleanup;
 
    // 进入 location 对象
    cJSON *location = cJSON_GetObjectItem(first, "location");
    cJSON *name = cJSON_GetObjectItem(location, "name");
    if (cJSON_IsString(name)) {
        strncpy(info->city, name->valuestring, 31);
    }
 
    // 进入 now 对象
    cJSON *now = cJSON_GetObjectItem(first, "now");
    cJSON *text = cJSON_GetObjectItem(now, "text");
    cJSON *code = cJSON_GetObjectItem(now, "code");
    cJSON *temp = cJSON_GetObjectItem(now, "temperature");
 
    if (cJSON_IsString(text)) strncpy(info->weather, text->valuestring, 15);
    if (cJSON_IsString(code)) info->weather_code = atoi(code->valuestring);
    if (cJSON_IsString(temp)) info->temperature = atof(temp->valuestring);
 
    // 第三步:释放内存(cJSON 在解析时动态分配了很多内存)
cleanup:
    cJSON_Delete(root);  // 必须手动释放,否则内存泄漏!
    return true;
}

cJSON 工作原理:

原始字符串 → cJSON_Parse() → 内存中的树形结构

                  root (对象)
                    │
               "results" (数组)
                    │
               [0] (对象)
              ┌────┴────┐
         "location"   "now"
          (对象)      (对象)
         ┌──┘         ┌──┤──┐
       "name"      "text" "code" "temperature"
      "Hefei"    "Cloudy"  "4"       "32"

cJSON 把整个 JSON 字符串变成一棵树,然后你用键名在树上查找节点,取出值。


方法 B:项目中的”手搓解析”——strstr + sscanf

项目中没有用 cJSON,而是用两个标准 C 函数自己解析:

实际代码(weather.c):

bool parse_seniverse_response(const char *response, weather_info_t *info)
{
    // strstr(大字符串, 要找的子串) → 返回子串在大字符串中的位置(指针)
    response = strstr(response, "\"results\":");
    if (response == NULL) return false;
 
    const char *location_response = strstr(response, "\"location\":");
    if (location_response == NULL) return false;
 
    // 找到 "name": 出现的位置,然后用 sscanf 提取引号里的内容
    const char *loaction_name_response = strstr(location_response, "\"name\":");
    if (loaction_name_response) {
        //  格式字符串解读:
        //  "name": "%31[^\"]"
        //   ↑找到"name":后,跳过空格,匹配一个最多31字符且不包含"的字符串
        sscanf(loaction_name_response, "\"name\": \"%31[^\"]\"", info->city);
    }
 
    const char *now_response = strstr(response, "\"now\":");
    if (now_response == NULL) return false;
 
    const char *now_text_response = strstr(now_response, "\"text\":");
    if (now_text_response) {
        sscanf(now_text_response, "\"text\": \"%15[^\"]\"", info->weather);
    }
 
    const char *now_code_response = strstr(now_response, "\"code\":");
    if (now_code_response) {
        sscanf(now_code_response, "\"code\": \"%d\"", &info->weather_code);
    }
 
    char temperature_str[16] = { 0 };
    const char *now_temperature_response = strstr(now_response, "\"temperature\":");
    if (now_temperature_response) {
        if (sscanf(now_temperature_response, "\"temperature\": \"%15[^\"]\"", temperature_str) == 1)
            info->temperature = atof(temperature_str);
    }
 
    return true;
}

第三部分:核心函数逐行解释

3.1 strstr ——在字符串中”搜索”

const char *strstr(const char *str, const char *target);
  • 功能:在 str 里搜索 target 第一次出现的位置
  • 返回:指向找到位置的指针;找不到返回 NULL

图解:

原始字符串:
{ "results": [{ "location": { "name": "Hefei" ...
                              ↑
                              strstr(response, "\"name\":") 返回这里的指针

此时指针 loaction_name_response 指向:
"name": "Hefei", "country": "CN" ...
↑ 从这里开始

为什么要逐层往里找? 因为 JSON 中同一个键名可能出现多次。 比如 “name” 在整个响应里可能出现很多次, 先定位到 “location”: 的位置,再在 location 这一段里找 “name”, 确保找到的是城市名,而不是其他地方的 “name”。

3.2 sscanf ——从字符串中”提取数据”

int sscanf(const char *str, const char *format, ...);
  • 功能:按照格式模板,从字符串中提取数据(和 printf 相反)
  • 返回:成功提取的参数个数

格式模板详解(最难懂的部分):

sscanf(loaction_name_response, "\"name\": \"%31[^\"]\"", info->city);

把格式字符串拆解来看(去掉转义符后):

模板:   "name": "%31[^\"]"
匹配:   "name": "Hefei"
                  ↑↑↑↑↑
                  提取这里

%31[^\"] 的含义:

  • %:开始一个格式说明符
  • 31:最多读取 31 个字符(防止缓冲区溢出)
  • [^]:字符集合,^ 表示”取反/排除”
  • \":要排除的字符 = 双引号 "
  • 完整含义:读取最多31个字符,遇到双引号就停下

所以这个模式的效果:从 "name": " 开始匹配,跳过固定文本,把引号里的内容(直到下一个引号)提取出来。

更多 sscanf 格式示例:

// 提取整数
sscanf("\"code\": \"4\"", "\"code\": \"%d\"", &weather_code);
// → weather_code = 4
 
// 提取多个值(Wi-Fi解析 esp_at.c中)
sscanf("+CWJAP:\"WiFi名\",\"aa:bb:cc\",6,-50",
       "+CWJAP:\"%63[^\"]\",\"%17[^\"]\",%d,%d",
       ssid, bssid, &channel, &rssi);
// → ssid="WiFi名", bssid="aa:bb:cc", channel=6, rssi=-50

第四部分:两种方法全面对比

对比维度手搓法(strstr+sscanf)专业库(cJSON)
代码量少,weather.c 只有 60 行需要引入 cJSON.c/cJSON.h(约2000行)
内存占用极低,不动态分配内存,直接在原字符串上操作需要大量动态内存(malloc),在内存中建树
执行速度快,线性扫描一遍较慢,先完整解析再查询
健壮性弱,JSON 格式稍有变化(多一个空格)就可能失败强,严格按照 JSON 标准解析,格式有一定容错
可维护性差,每个字段都要写一段查找代码,容易出错好,代码清晰,增加新字段很方便
适用场景嵌入式、内存极小的 MCU、已知API不会变PC、Linux、资源充足的环境
调试难度难,格式字符串一旦写错很难发现易,解析出错有明确的错误信息

第五部分:为什么这个项目选择手搓法?

原因 1:内存极度受限

STM32F4 的 RAM 约 192KB,FreeRTOSConfig.h 中配置了堆大小仅 92KB

#define configTOTAL_HEAP_SIZE   (92 * 1024)

cJSON 解析一个 JSON 时会大量调用 malloc,在内存里建立整棵树。 这个天气响应大约 300 字节,cJSON 解析后需要的内存远超原始大小(每个节点都要分配结构体)。

手搓法 直接在原始字符串上操作,不分配任何额外内存,零内存开销。

原因 2:API 格式固定不变

心知天气的响应格式由官方文档定义,项目作者知道它不会随意变化。 既然格式固定,用 strstr 精确定位就够了,完全不需要通用的完整 JSON 解析器。

原因 3:只需要少数几个字段

这个 JSON 里有十几个字段,但我们只关心 4 个(name, text, code, temperature)。 没有必要把整个 JSON 解析成树再去查,直接”点对点”找到目标字段即可。


第六部分:手搓法的隐患与改进建议

隐患 1:对空格敏感

// 当前代码期望格式:
sscanf(ptr, "\"name\": \"%31[^\"]\"", ...);
//                   ↑ 一个空格
 
// 如果API返回的是(没有空格):
// "name":"Hefei"   ← 没有: 后面的空格,sscanf 就匹配失败!

改进方案:格式模板中用空格匹配”任意数量空白字符”:

sscanf(ptr, "\"name\": %31[^\"]", ...);  // 去掉外层引号,空格会自动跳过空白

隐患 2:strstr 可能找错位置

如果JSON结构变化,比如某个地方新增了 "now_name": "xxx" 字段, strstr(response, "\"name\":") 就可能找到错误的位置。

隐患 3:没有类型检查

cJSON 提供 cJSON_IsString()cJSON_IsNumber() 等检查, 手搓法直接信任格式,如果服务器返回了意外的格式,程序可能提取到垃圾数据。


第七部分:如果要引入 cJSON 该怎么做(参考)

cJSON 是一个单文件库,只需要两个文件:cJSON.ccJSON.h。 在 MCU 上使用时需要告诉它用哪个 malloc/free:

// 初始化时告诉 cJSON 用 FreeRTOS 的内存分配
cJSON_Hooks hooks = {
    .malloc_fn = pvPortMalloc,  // FreeRTOS 的 malloc
    .free_fn   = vPortFree      // FreeRTOS 的 free
};
cJSON_InitHooks(&hooks);

之后就可以正常使用 cJSON_Parse() 等函数了。



总结

服务器返回的原始字符串(约300字节):
+HTTPCLIENT:261,{"results":[{"location":{"name":"Hefei",...},"now":{"text":"Cloudy","code":"4","temperature":"32"},...}]}
OK

                    ↓ 两种解析思路

手搓法(项目采用)              专业库 cJSON
strstr 定位到 "name":         cJSON_Parse 完整建树
sscanf 提取引号内的值          cJSON_GetObjectItem 查节点
→ 直接写入结构体              → 从节点取值写入结构体

优势:零内存分配,极快          优势:健壮、易维护、扩展性强
适合:资源极少的 MCU           适合:资源充足的环境

对于你的学习建议:

  1. 先彻底搞懂 strstrsscanf 的用法(C标准库基础)
  2. 理解这两个函数组合可以解析任何格式固定的文本
  3. 如果将来做复杂项目,引入 cJSON 是更好的选择
  4. 这两种方法各有适用场景,都是工程师必须掌握的技能