htcw_json:一个微型流式 JSON 解析器





5.00/5 (1投票)
在内存占用极小的微型设备上高效流式传输 JSON。
引言
REST 无处不在。除了使用 MQTT,对于连接设备来说,能够与 REST 服务器通信并处理 JSON 格式的返回数据几乎是必需的。
市面上有一些解决方案,但它们要么占用大量闪存空间,要么占用大量内存空间,要么不支持跨平台。那些创建内存模型的解决方案在处理大量内容时,在资源受限的设备上很快就会失效。
我采用了不同的 JSON 解析方法。借鉴 Microsoft .NET 的 XmlReader
,我创建了一个类似的“拉取式”解析器来读取 JSON。优点是效率高。缺点是,与 XmlReader
一样,根据您的需求,它可能比传统的 JSON 解析器更难使用。
必备组件
运行演示
- 您需要安装了 Platform IO 的 VS Code
- 您需要 ESP32
- 您需要修改代码或以其他方式设置 ESP32 的 SSID 和 WiFi 密码才能使用 worldtimeapi.org
- 您需要上传文件系统镜像才能将 data.json 放到设备上
背景
这个解析器有些极简,但同时包含了一些很棒的功能,例如能够分块处理长度超过定义的捕获缓冲区大小的值,以及解析一些基本数据类型——即整数、浮点数和布尔值。
您告诉它您希望它使用多少内存。内存的唯一要求是字段名必须能够放入分配的空间。例如,如果您分配 1KB(默认值),字段名不能超过该长度。值没有类似的限制,但如果一个值比分配给捕获缓冲区的空间长,它将以一系列“值部分”的形式检索数据。
然后您需要构建一个循环——通常类似于 while(reader.read())
……然后在该循环内部,您检查解析器的 node_type()
并采取相应措施。您可以使用 value()
检索当前值或字段名。您可以使用 value_int()
、value_real()
和 value_bool()
获取类型化的值。如果您以前使用过 XmlReader
,这个过程应该比较熟悉。
使用代码
我将在下面提供一个使用该库的示例。假设使用的是 ESP32,因为我手头正好有一个并且它可以连接到互联网,但这并不是必需的。它也是为 Arduino 编写的,但可以轻松移植到其他平台。您可以改编此代码并在 PC、STM32、NXP、SAMD51 或任何其他设备上使用它。
让我们看看如何转储文档中的所有数据,这可以充分测试这个拉取式解析器。请注意,这**不是**一个漂亮的打印例程,因为它不输出有效的 JSON,而是以类似 JSON 的分层方式呈现数据。
#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <SPIFFS.h>
#include <json.hpp>
using namespace io;
using namespace json;
void indent(int tabs) {
while(tabs--) Serial.print(" ");
}
// accept any reader regardless of capture size
void dump(json_reader_base& reader, Stream& output) {
// don't de-escape and dequote field names or string values:
//reader.raw_strings(true);
bool first_part=true; // first value part in series
int tabs = 0; // number of "tabs" to indent by
bool skip_read = false; // don't call read() the next iteration
while(skip_read || reader.read()) {
skip_read = false;
switch(reader.node_type()) {
case json_node_type::array:
indent(tabs++);
output.println("[");
break;
case json_node_type::end_array:
indent(--tabs);
output.println("]");
break;
case json_node_type::object:
indent(tabs++);
output.println("{");
break;
case json_node_type::end_object:
indent(--tabs);
output.println("}");
break;
case json_node_type::field:
indent(tabs);
output.printf("%s: ",reader.value());
// we want to spit the value here, so
// we basically hijack the reader and
// read the value subtree here.
while(reader.read() && reader.is_value()) {
output.printf("%s",reader.value());
}
output.println("");
skip_read = true;
break;
case json_node_type::value:
indent(tabs);
output.printf("%s\r\n",reader.value());
break;
case json_node_type::value_part:
// the first value part needs to be indented
if(first_part) {
indent(tabs);
first_part = false; // reset the flag
}
output.printf("%s",reader.value());
break;
case json_node_type::end_value_part:
output.printf("%s,\r\n",reader.value());
// set the first flag
first_part = true;
break;
}
}
}
同样,如果您使用过 XmlReader
,这应该比较熟悉。基本上,它所做的就是读取文档中的每个元素,并在找到它们时以格式化的方式打印出来。
让我们继续一个更实际的例子。在这里,我们将从 JSON 文档中提取信息。
{
"air_date": "2007-06-28",
"episode_number": 1,
"id": 223655,
"name": "Burn Notice",
"overview": "Michael Westen is a spy who receives a \"burn notice\" while on assignment. Spies are not fired, rather they are issued a burn notice to let the agent know their services are no longer required.\n\nPenniless, Michael returns to his roots in Miami where he freelances his skills to earn money. First up, Michael helps a man clear his name after valuable pieces of art and jewelery are stolen.",
"production_code": null,
"season_number": 1,
"show_id": 2919,
"still_path": "/7lypjkgNLkYDxwcqGWmZmHH5ieq.jpg",
"vote_average": 8,
"vote_count": 1,
"crew": [
{
"id": 20833,
"credit_id": "525749d019c29531db098a72",
"name": "Jace Alexander",
"department": "Directing",
"job": "Director",
"profile_path": "/nkmQTpXAvsDjA9rt0hxtr1VnByF.jpg"
},
{
"id": 1233032,
"credit_id": "525749d019c29531db098a46",
"name": "Matt Nix",
"department": "Writing",
"job": "Writer",
"profile_path": null
}
],
"guest_stars": [
{
"id": 6719,
"name": "Ray Wise",
"credit_id": "525749cc19c29531db098912",
"character": "",
"order": 0,
"profile_path": "/z1EXC8gYfFddC010e9YK5kI5NKC.jpg"
},
{
"id": 92866,
"name": "China Chow",
"credit_id": "525749cc19c29531db098942",
"character": "",
"order": 1,
"profile_path": "/kUsfftCYQ7PoFL74wUNwwhPgxYK.jpg"
},
{
"id": 17194,
"name": "Chance Kelly",
"credit_id": "525749cc19c29531db09896c",
"character": "",
"order": 2,
"profile_path": "/hUfIviyweiBZk4JKoCIKyuo6HGH.jpg"
},
{
"id": 95796,
"name": "Dan Martin",
"credit_id": "525749cd19c29531db098996",
"character": "",
"order": 3,
"profile_path": "/u24mFuqwEE7kguXK32SS1UzIQzJ.jpg"
},
{
"id": 173269,
"name": "Dimitri Diatchenko",
"credit_id": "525749cd19c29531db0989c0",
"character": "",
"order": 4,
"profile_path": "/vPScVMpccnmNQSsvYhdwGcReblD.jpg"
},
{
"id": 22821,
"name": "David Zayas",
"credit_id": "525749cd19c29531db0989ea",
"character": "",
"order": 5,
"profile_path": "/eglTZ63x2lu9I2LiDmeyPxhgwc8.jpg"
},
{
"id": 1233031,
"name": "Nick Simmons",
"credit_id": "525749cf19c29531db098a17",
"character": "",
"order": 6,
"profile_path": "/xsc2u2QQA6Nu7SvUYUPKFlGl9fw.jpg"
}
]
}
这不是完整的文档,它近 190KB,而是描述电视剧集的内部“剧集对象”之一。我们将导航到其中每一个,只获取季号、集号、名称和概述字段,然后将它们打印到输出。
首先,快速粗略地搜索 episodes
字段,这些字段每个都包含一个剧集对象数组,如上所示。每个季度都有一个这样的字段。
void read_series(json_reader_base& reader, Stream& output) { while(reader.read()) { // find "episodes" switch(reader.node_type()) { case json_node_type::field: if(0==strcmp("episodes",reader.value())) { read_episodes(reader, output); } break; default: break; } } }
现在我们来看看如何解析剧集数组并将每个剧集的详细信息打印到输出。
char name[2048];
char overview[8192];
void read_episodes(json_reader_base& reader, Stream& output) {
int root_array_depth = 0;
// episodes opens with an array
if(reader.read() && reader.node_type()==json_node_type::array) {
root_array_depth = reader.depth();
while(true) {
// if we're at the end of the array, break
if(reader.depth()==root_array_depth &&
reader.node_type()==json_node_type::end_array) {
break;
}
// read each "episode object"
if(reader.read()&&
reader.node_type()==json_node_type::object) {
int episode_object_depth = reader.depth();
int season_number = -1;
int episode_number = -1;
while(reader.read() &&
reader.depth()>=episode_object_depth) {
// make sure we don't read any nested objects
// under this one
if(reader.depth()==episode_object_depth &&
reader.node_type()==json_node_type::field) {
if(0==strcmp("episode_number",reader.value()) &&
reader.read() &&
reader.node_type()==json_node_type::value) {
episode_number = reader.value_int();
}
if(0==strcmp("season_number",reader.value()) &&
reader.read() &&
reader.node_type()==json_node_type::value) {
season_number = reader.value_int();
}
// gather the name
if(0==strcmp("name",reader.value())) {
name[0]=0;
while(reader.read() && reader.is_value()) {
strcat(name,reader.value());
}
}
// gather the overview
if(0==strcmp("overview",reader.value())) {
overview[0]=0;
while(reader.read() && reader.is_value()) {
strcat(overview,reader.value());
}
}
}
}
if(season_number>-1 && episode_number>-1 && name[0]) {
output.printf("S%02dE%02d %s\r\n",
season_number,
episode_number,
name);
if(overview[0]) {
output.printf("\t%s\r\n",overview);
}
output.println("");
}
}
}
}
}
这有点棘手,但通过一些思考就可以解决。它会遍历数组,通过跟踪 depth()
来记录数组的结束位置。它对每个对象也做类似的处理,以免在查找 name
等内容时遍历嵌套对象。
从文件读取(例如从 SPIFFS 或 SD 卡)一旦您理解了一些概念就相当直接了。第一个是我们将 Arduino 对象封装在自己的流中。这是因为该库的跨平台性质以及与 STL 的解耦。JSON 库只与我们的流一起工作,而我们的流本身可以适应不同的平台,如下所示。另一件需要记住的事情是文件是**二进制**的,而不是技术上的文本。原因是 UTF-8 不是 ASCII,而在 C 中,文本通常意味着 ASCII。我们不希望它处理 Unicode 代理字符或任何其他奇怪的东西,所以二进制模式就是我们要用的。
// use binary mode in case UTF-8
File file = SPIFFS.open("/data.json","rb");
if(!file) {
return;
}
file_stream file_stm(file);
json_reader file_reader(file_stm);
dump(file_reader, Serial);
file_stm.close();
我们基本上只是以二进制模式打开文件——在本例中是从 SPIFFS,但也可以是 SD 卡——然后将其封装,并将其传递给 json_reader
实例。如果您想指定不同的捕获大小,例如 512 字节,则应使用 json_reader_ex<512>
而不是 json_reader
。
之后,我们只需调用前面介绍的 dump()
例程。
如果您需要从其他 Arduino 源读取,可以使用 arduino_stream
来封装 WiFiClient
和 HardwareSerial
实例。显然,这些仅在使用 Arduino 框架时可用。
这里有一个从 ESP32 连接到 worldtimeapi.org REST 服务并转储数据的示例。
constexpr static const char* wtime_url="http://worldtimeapi.org/api/ip";
WiFi.mode(WIFI_STA);
WiFi.disconnect();
WiFi.begin();
while(WiFi.status()!=WL_CONNECTED) {
delay(10);
}
HTTPClient client;
client.begin(wtime_url);
if(0>=client.GET()) {
while(1);
}
WiFiClient& www_client = client.getStream();
arduino_stream www_stm(&www_client);
json_reader www_reader(www_stm);
dump(www_reader, Serial);
WiFi.disconnect();
包含的演示应用程序适用于 ESP32 和 Platform IO,并将演示上面介绍的所有内容。
历史
- 2024 年 3 月 30 日 - 初次提交
- 2024 年 3 月 31 日 - 改进了示例,并进行了一些小的 API 改进