一、JSON 文件解析实战:使用 cJSON 库解析 JSON 格式文件
说实话,JSON 解析这事儿,我当年刚入行时也头疼过一阵子。那时候项目里要对接一个第三方 API,返回的全是 JSON 格式的数据。我心想,不就是个文本格式吗?自己写个解析器不就完了?结果折腾了两天,各种边界情况处理得我头皮发麻。后来同事告诉我:「别傻了,用 cJSON 啊!」
嗯,从那以后,我就再也没自己手写过 JSON 解析器了。
1.1 什么是 cJSON?为什么选它?
cJSON 是一个轻量级的 C 语言 JSON 解析库。它只有一个 .c 文件和一个 .h 文件,加起来不到 2000 行代码。你想想看,这么小的体量,却能完成 JSON 的序列化、反序列化、增删改查全套操作,是不是很划算?
我个人习惯在嵌入式项目里用它。因为资源受限的环境下,你不可能去装什么 Python 或者用 Java 的 Jackson 库。cJSON 纯 C 实现,没有依赖,直接拖进工程就能编译。说白了,它就是为 C 程序员量身定做的 JSON 瑞士军刀。
核心优势:
- 纯 C 语言实现,零依赖
- 支持 ANSI C(C89/C90),兼容性极强
- 解析速度快,内存占用低
- API 设计简洁,上手快
1.2 安装与引入 cJSON
你不需要什么复杂的安装步骤。直接从 GitHub 上把 cJSON.c 和 cJSON.h 下载下来,放到你的项目目录里,然后在代码里 #include "cJSON.h" 就行了。
我记得有一次在 Linux 下做项目,直接用包管理器安装也很方便:
# Ubuntu/Debian
sudo apt-get install libcjson-dev
# CentOS/RHEL
sudo yum install libcjson-devel
# macOS (Homebrew)
brew install cjson
编译的时候记得链接库:
gcc -o myapp myapp.c -lcjson
1.3 JSON 文件的结构与 cJSON 的数据模型
我们先看一个典型的 JSON 文件长什么样。假设我们有一个学生信息文件 student.json:
{
"name": "张三",
"age": 20,
"is_graduated": false,
"scores": [88, 92, 76],
"address": {
"city": "北京",
"district": "海淀区"
}
}
cJSON 内部是怎么表示这个结构的呢?它用了一个叫 cJSON 的结构体,每个节点都有类型、值、子节点和兄弟节点指针。说白了,它把 JSON 解析成了一棵多叉树。
小技巧: 我个人习惯把 JSON 的根节点想象成「根目录」,对象就是「文件夹」,数组就是「文件列表」,字符串和数字就是「文件」。这样理解起来特别直观。
cJSON 支持的数据类型:
| 类型常量 | 对应 JSON 类型 | 说明 |
|---|---|---|
| cJSON_Invalid | 无效 | 未初始化或解析失败 |
| cJSON_False | false | 布尔假值 |
| cJSON_True | true | 布尔真值 |
| cJSON_NULL | null | 空值 |
| cJSON_Number | 数字 | 整数或浮点数 |
| cJSON_String | 字符串 | 双引号括起来的文本 |
| cJSON_Array | 数组 | 有序的值集合 |
| cJSON_Object | 对象 | 无序的键值对集合 |
1.4 核心 API 速览
你不需要记住所有 API,常用的就那么几个。我列出来给你看:
// 解析 JSON 字符串
cJSON *cJSON_Parse(const char *value);
// 从文件读取并解析
cJSON *cJSON_ParseWithLength(const char *value, size_t buffer_length);
// 获取对象中的指定键
cJSON *cJSON_GetObjectItem(const cJSON *object, const char *string);
// 获取数组元素个数
int cJSON_GetArraySize(const cJSON *array);
// 获取数组中的第 n 个元素
cJSON *cJSON_GetArrayItem(const cJSON *array, int index);
// 判断节点类型
int cJSON_IsInvalid(const cJSON *item);
int cJSON_IsBool(const cJSON *item);
int cJSON_IsNumber(const cJSON *item);
int cJSON_IsString(const cJSON *item);
int cJSON_IsArray(const cJSON *item);
int cJSON_IsObject(const cJSON *item);
// 获取值
int cJSON_GetBoolValue(const cJSON *item);
double cJSON_GetNumberValue(const cJSON *item);
const char *cJSON_GetStringValue(const cJSON *item);
// 释放内存
void cJSON_Delete(cJSON *item);
注意: 我曾经犯过一个低级错误——解析完 JSON 后忘了调用 cJSON_Delete()。结果程序跑着跑着内存就爆了。尤其是嵌入式设备,内存本来就紧张,这种泄漏很致命。所以请记住:谁分配,谁释放。
1.5 实战:解析 student.json
光说不练假把式。我们直接写代码来解析上面那个 student.json 文件。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cJSON.h"
// 读取整个文件到字符串
char *read_file(const char *filename) {
FILE *fp = fopen(filename, "rb");
if (!fp) {
printf("无法打开文件: %s\n", filename);
return NULL;
}
fseek(fp, 0, SEEK_END);
long size = ftell(fp);
rewind(fp);
char *buffer = (char *)malloc(size + 1);
if (!buffer) {
fclose(fp);
return NULL;
}
fread(buffer, 1, size, fp);
buffer[size] = '\0';
fclose(fp);
return buffer;
}
int main() {
// 1. 读取文件
char *json_str = read_file("student.json");
if (!json_str) {
return 1;
}
// 2. 解析 JSON
cJSON *root = cJSON_Parse(json_str);
free(json_str); // 文件内容用完了,释放掉
if (!root) {
const char *error_ptr = cJSON_GetErrorPtr();
if (error_ptr) {
printf("解析错误在: %s\n", error_ptr);
}
return 1;
}
// 3. 提取数据
cJSON *name = cJSON_GetObjectItem(root, "name");
cJSON *age = cJSON_GetObjectItem(root, "age");
cJSON *is_grad = cJSON_GetObjectItem(root, "is_graduated");
cJSON *scores = cJSON_GetObjectItem(root, "scores");
cJSON *address = cJSON_GetObjectItem(root, "address");
// 4. 打印结果
if (cJSON_IsString(name) && name->valuestring) {
printf("姓名: %s\n", name->valuestring);
}
if (cJSON_IsNumber(age)) {
printf("年龄: %d\n", age->valueint);
}
if (cJSON_IsBool(is_grad)) {
printf("是否毕业: %s\n", cJSON_IsTrue(is_grad) ? "是" : "否");
}
// 遍历数组
if (cJSON_IsArray(scores)) {
int count = cJSON_GetArraySize(scores);
printf("成绩: ");
for (int i = 0; i < count; i++) {
cJSON *item = cJSON_GetArrayItem(scores, i);
if (cJSON_IsNumber(item)) {
printf("%.0f ", item->valuedouble);
}
}
printf("\n");
}
// 解析嵌套对象
if (cJSON_IsObject(address)) {
cJSON *city = cJSON_GetObjectItem(address, "city");
cJSON *district = cJSON_GetObjectItem(address, "district");
if (cJSON_IsString(city) && cJSON_IsString(district)) {
printf("地址: %s %s\n", city->valuestring, district->valuestring);
}
}
// 5. 释放内存
cJSON_Delete(root);
return 0;
}
运行结果:
姓名: 张三
年龄: 20
是否毕业: 否
成绩: 88 92 76
地址: 北京 海淀区
1.6 避坑指南:我踩过的那些坑
做 JSON 解析,有几个地方特别容易出问题。我一个个跟你说:
- 空指针检查不能省:
cJSON_GetObjectItem返回 NULL 时,千万别直接访问它的成员。我曾经在项目里没做检查,结果线上程序直接段错误,被运维大哥骂了一顿。 - 字符串生命周期:
cJSON_GetStringValue返回的指针指向 cJSON 内部的内存。如果你cJSON_Delete了,这个指针就变成野指针了。所以要么在 delete 之前用完,要么用strdup复制一份。 - 大文件处理:如果 JSON 文件很大(比如几 MB),一次性读到内存里再解析可能会爆内存。我建议用流式解析或者分块读取。不过 cJSON 本身不支持流式解析,你可以考虑用
cJSON_ParseWithLength分段处理。 - 编码问题:cJSON 默认只支持 UTF-8 编码。如果你遇到 GBK 或者其他编码的 JSON 文件,记得先转码再解析。我当年在某个国产系统上就吃过这个亏。
我的习惯: 每次解析完 JSON,我都会写一个简单的单元测试,把每个字段的类型和值都验证一遍。这样即使上游接口改了格式,我这边也能第一时间发现,而不是等到程序崩溃了才去排查。
1.7 知识体系总览
下面这张图是我自己整理的 cJSON 解析流程,你可以把它当作一个检查清单:
1.8 小结
cJSON 这个库,说白了就是「小身材,大能量」。你只要记住几个核心 API,再注意一下内存管理和类型检查,基本就能应付 90% 的 JSON 解析场景了。
我个人建议你把这个实战代码自己敲一遍,然后换一个自己的 JSON 文件试试。比如把你的项目配置文件拿来解析一下。只有亲手写过,才能真正理解那些 API 的用法和坑点。
嗯,这一章就到这里。记住:解析 JSON 不难,难的是把每一步都做对、做稳。
公众号:蓝海资料掘金营,微信 deep3321