Arduino 上的大型 JSON:在小型设备上解析海量数据





5.00/5 (4投票s)
使用小型设备流式传输您的数据并即时解析
引言
如今 JSON 无处不在。物联网设备也在缓慢但稳定地普及。自然,这两者将不可避免地结合。市面上有用于处理 JSON 的 Arduino 库,但它们流式处理效果不佳,因为它不是拉取式解析器,而这正是这个小库所提供的。这个库是我Json 库中 JsonTextReader
的一部分移植过来的,它允许您通过一次只检查一小部分数据来读取几乎任何大小的 JSON 数据。
此外,本文旨在为您提供一种在流式源上实时简化词法分析和解析的技巧。为此,我还将 LexContext 移植到了这个平台,并将 JsonReader
移植回来,使其能够使用它。
您可能会认为将 C# 的东西移植到 C++ 很奇怪,但这两个类之所以是移植到 Arduino SDK 的绝佳选择,是因为它们本身就很简单、小巧且快速。它们只需要一点点“魔法”就能将它们转化为 C++,并让它们在小型设备上运行。
有了这些便捷的工具,您现在就可以自由地扫描和解析大型 JSON,或者在流式源上构建自己的高效解析器。
更新:代码清理,文章增加了对内部工作原理的深入探讨。
更新 2:改进了错误处理,修复了 bug。请注意,文章中的代码未反映这些更改。错误处理会使代码变得杂乱,因此我决定此处省略。用户可以通过 lastError()
获取错误代码,通过 value()
获取错误文本。
更新 3:修复了在某些内存不足情况下报告错误消息不正确的 bug。
更新 4:修复了跳过(部分解析)的 bug - 移除了非标准跳过,因为它不再需要。为了保持一致性,将 Key
改名为 Field
。更新了文章中的代码。
概念化这个混乱的局面
拉取式解析的工作方式是每次获取结构化文档的一小部分,并进行足够的记账来了解您所处的位置以及下一步该做什么。这通常涉及构建一个有限状态机来承担繁重的工作。这是指一个简单的 switch
/case
语句,通过一个整数成员变量来更新我们正在进行的状态。
int _state = -1; // initial state
bool read() {
switch(_state) {
case -1: // initial
_state = 0;
// fall through
case 0: // thing A
// do thing A
_state = 1;
return true;
case 1: // repeat thing B or end
// do thing b or end
if(textUnderCursorIsB()) {
// do thing B
_state = 1;
return true;
}
// done
_state = -2;
return false;
}
}
这很混乱,对人类来说并不友好,但 CPU 却很喜欢。我们在这些小型设备上运行非常接近底层代码,因此像状态机这样的复杂东西如果高效的话是可以接受的。
不过,这样做的好处是我们现在可以简单地通过以下方式进行迭代调用:
while(parser.read()) { ... }
每次调用 read()
都会执行解析的一步。总体的想法是,我们设置状态机,使 read()
返回 true
,直到没有更多数据可读为止。
这就是解析,但在此之前,我们需要一种方法来管理流式光标。以前,LexContext
封装了 TextReader
或 IEnumerable<char>
源等内容。但现在,我们只需要封装 Arduino 的 Stream
类,因为它例如派生自 File
类和 WiFiClient
类。
LexContext
提供了在流式源上执行基本操作的工具,例如捕获光标下的当前字符、前进输入、读取和跳过空格,或读取或跳过直到遇到特定字符。当我们创建一个实例时,我们会用一个固定长度的缓冲区来存储捕获的数据。该缓冲区的大小应为一次预期处理数据的最大长度。对于 JSON 数据,这可能是字段名或标量值的长度。
解析器随后可以使用它来管理其输入,以便进行解析。
编写这个混乱的程序
要进行设置,请将 SD 卡读卡器连接到 32 位设备的PRIMARY SPI 端口和PRIMARY CS/SS。将包含的 data.json 文件复制到格式为 Fat32 的 SD 卡的根目录,然后您就可以运行演示了。
LexContext
LexContext 的工作方式是:current()
始终检索光标下的当前字符,而 advance()
移动到下一个位置并返回该字符。capture()
将字符捕获到缓冲区,captureBuffer()
返回缓冲区中的字符串。line()
、column()
和 position()
跟踪光标的位置。我们首先通过指定捕获缓冲区大小来声明它。
LexContext<1024> lc; // allocate 1kB for the capture
以下是一个使用它从串口读取数据直到遇到非数字字符的示例:
while (LexContext<1024>::EndOfInput != lc.advance() && isdigit((char)lc.current())) {
Serial.print((char)lc.current());
}
Serial.println();
在上面,我们使用 Serial
作为输入源对其进行初始化。然后我们前进,并检查字符是否不是输入结束标记,然后检查它是否是数字。如果是,我们就打印它并继续。请记住,我们在执行任何其他操作之前都会前进。光标在初始化后必须先前进一次。如果您的例程需要确保这一点已发生但尚不确定,您可以调用 ensureStarted()
。
API 基本与 C# API 相同,只是将大小写风格更改为适应。更多详情请参阅该文章。主要区别在于初始化时指定捕获量,然后调用 begin()
并传入输入源。请注意,与 C# API 不同,这里没有相应的 close()
机制。由于底层架构的差异,输入源必须在完成时从该类外部关闭。
JsonReader
现在我们已经了解了光标管理,让我们回到 JSON 解析。使用拉取式解析器非常高效,但需要一些时间来适应。我们上面看到,我们在一系列循环中读取部分解析的数据,直到没有更多数据。那么,有趣的事情就发生在那个循环内部。
首先,我们可能需要检查 nodeType()
来查看我们当前处于什么类型的节点。它可以是 Initial
、Value
、Field
、Array
、EndArray
、Object
、EndObject
、EndDocument
或 Error
。这些常量都可以从 JsonReader
类本身访问。如果它是一个 Value
节点,我们可能还需要检查 valueType()
,它可以是 String
、Boolean
、Number
或 Null
。如果是 String
,很可能会调用 undecorate()
来去除引号并将转义字符转换为实际字符。请注意,执行此操作后,对此同一节点进行后续的 valueType()
调用将不可靠。最后,您可以调用 value()
将值作为 char*
获取,调用 numericValue()
将其作为 double
获取,或调用 booleanValue()
将其作为 bool
获取。请注意,undecorate()
也会使这些调用变得不可靠。undecorate()
会影响所有这些函数的原因是它会就地修改字符串值以节省空间。
让我们看看提供的 ino 文件。
#include <SD.h>
#include <Json.h>
// for easier access
typedef JsonReader<2048> JsonReader2k;
// our reader. if we declare this locally it would require slightly over 2k on the stack
JsonReader2k jsonReader;
void dumpJsonFile() {
// open data.json off the SD reader. You wired in an SD reader, right?
File file = SD.open("/data.json", FILE_READ);
if (!file) {
Serial.println(F("/data.json not found or could not be opened"));
while (true); // halt - no, no you did not. or you didn't insert the card
}
// initialize the reader with our file
jsonReader.begin(file);
// pull parsers return portions of the parse which you retrieve
// by calling their parse/read method in a loop.
while (jsonReader.read()) {
// what kind of JSON element are we on?
switch (jsonReader.nodeType()) {
case JsonReader2k::Value: // we're on a scalar value
Serial.print("Value ");
switch (jsonReader.valueType()) { // what type of value?
case JsonReader2k::String: // a string!
Serial.print("String: ");
jsonReader.undecorate(); // remove all the nonsense
Serial.println(jsonReader.value()); // print it
break;
case JsonReader2k::Number: // a number!
Serial.print("Number: ");
Serial.println(jsonReader.numericValue()); // print it
break;
case JsonReader2k::Boolean: // a boolean!
Serial.print("Boolean: ");
Serial.println(jsonReader.booleanValue()); // print it!
break;
case JsonReader2k::Null: // a null!
Serial.print("Null: ");
Serial.println("null"); // print it!
break;
}
break;
case JsonReader2k::Field: // this is a field
Serial.print("Field ");
Serial.println(jsonReader.value());
break;
case JsonReader2k::Object: // an object start {
Serial.println("Object");
break;
case JsonReader2k::EndObject: // an object end }
Serial.println("End Object");
break;
case JsonReader2k::Array: // an array start [
Serial.println("Array");
break;
case JsonReader2k::EndArray: // an array end ]
Serial.println("End Array");
break;
case JsonReader2k::Error: // a bad thing
// maybe we ran out of memory, or the document was poorly formed
Serial.print("Error: (");
Serial.print(jsonReader.lastError());
Serial.print(") ");
Serial.println(jsonReader.value());
break;
}
}
// don't forget this
file.close();
}
void dumpId(bool recurse) {
// open the file
File file = SD.open("/data.json", FILE_READ);
if (!file) {
Serial.println(F("/data.json not found or could not be opened"));
while (true); // halt
}
jsonReader.begin(file);
// find the next field in the document named id.
// If recurse is specified, then subjects will be searched.
// Otherwise, only objects on this level of the hierarchy are considered.
// Once we find it, read what comes right after it to get the value
if (jsonReader.skipToField("id", recurse) && jsonReader.read()) {
Serial.println((int32_t)jsonReader.numericValue(), DEC); // get the value as a number
}
// always close the file
file.close();
}
void dumpShowName() {
File file = SD.open("/data.json", FILE_READ);
if (!file) {
Serial.println(F("/data.json not found or could not be opened"));
while (true); // halt
}
jsonReader.begin(file);
// look for the field named "name" on this level of the hierarchy.
// then read what comes right after it.
if (jsonReader.skipToField("name") && jsonReader.read()) {
jsonReader.undecorate(); // deescape the string
Serial.println(jsonReader.value());
}
// close the file
file.close();
}
void setup() {
Serial.begin(115200);
// initialize SD on default SPI bus and CS/SS
if (!SD.begin()) {
Serial.println(F("SD card mount failed"));
while (true); // halt
}
dumpJsonFile();
//dumpShowName();
//dumpId(true);
//dumpId(false);
}
void loop() {
if (Serial.available()) {
LexContext<1024> lc;
lc.begin(Serial);
while (LexContext<1024>::EndOfInput != lc.advance() && isdigit((char)lc.current())) {
Serial.print((char)lc.current());
}
Serial.println();
}
}
这里有很多内容。特别值得关注的是 dumpShowName()
、dumpId()
和 dumpJsonFile()
。
在后者中,我们正在遍历文件中的数据。请注意,我们可以通过调用适当的方法来获取字段的类型信息并从中获取类型化的值。目前所有数字都解析为 double
。如果您想要 int
,您需要将 value()
的结果传递给 atoi()
。
其他例程展示了如何遍历文档。它们并未展示所有功能,但确实演示了使用一个重要功能:skipToField()
。此方法查找文档中具有给定名称的下一个字段,并可以选择性地搜索子元素。通常,找到字段后,您会希望调用 read()
一次以获取下一个元素——字段的值。我们在上面的 ino 文件中就是这样做的。
还有 skipSubtree()
,用于跳过您所在的整个子树;skipToEndArray()
和 skipToEndObject()
,分别用于跳到同一级别的结束数组或对象标记。
我想添加的一个功能是在内存中创建一个与此集成的树形表示,或者可能与 ArduinoJson 集成。我可能会先创建一个 JSONPath 到 CPP 代码生成器,该生成器可以使用此库来满足 JSONPath 查询。一切都会按部就班。
很酷,但它是如何工作的?
正如我一开始所说,整个过程基本上是一个在 LexContext
上的状态机,它管理着流式输入的光标。状态机从 Initial
状态开始。每个状态直接对应于 nodeType()
返回的值。
bool read() {
int16_t qc;
int16_t ch;
switch (_state) {
case JsonReader<S>::Error:
case JsonReader<S>::EndDocument:
return false;
case JsonReader<S>::Initial:
_lc.ensureStarted();
_state = Value;
// fall through
case JsonReader<S>::Value:
value_case:
_lc.clearCapture();
switch (_lc.current()) {
case LexContext<S>::EndOfInput:
_state = EndDocument;
return false;
case ']':
_lc.advance();
_lc.trySkipWhiteSpace();
_lc.clearCapture();
_state = EndArray;
return true;
case '}':
_lc.advance();
_lc.trySkipWhiteSpace();
_lc.clearCapture();
_state = EndObject;
return true;
case ',':
_lc.advance();
_lc.trySkipWhiteSpace();
if (!read()) { // read the next value
_lastError = JSON_ERROR_UNTERMINATED_ARRAY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_UNTERMINATED_ARRAY_MSG,S-1);
_state = Error;
}
return true;
case '[':
_lc.advance();
_lc.trySkipWhiteSpace();
_state = Array;
return true;
case '{':
_lc.advance();
_lc.trySkipWhiteSpace();
_state = Object;
return true;
case '-':
case '.':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
qc = _lc.current();
if (!_lc.capture()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
while (LexContext<S>::EndOfInput != _lc.advance() &&
('E' == _lc.current() ||
'e' == _lc.current() ||
'+' == _lc.current() ||
'.' == _lc.current() ||
isdigit((char)_lc.current()))) {
if (!_lc.capture()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
}
_lc.trySkipWhiteSpace();
return true;
case '\"':
_lc.capture();
_lc.advance();
if(!_lc.tryReadUntil('\"', '\\', true)) {
if(LexContext<S>::EndOfInput==_lc.current()) {
_lastError = JSON_ERROR_UNTERMINATED_STRING;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_UNTERMINATED_STRING_MSG,S-1);
_state = Error;
return true;
} else {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
}
_lc.trySkipWhiteSpace();
if (':' == _lc.current())
{
_lc.advance();
_lc.trySkipWhiteSpace();
if (LexContext<S>::EndOfInput == _lc.current()) {
_lastError = JSON_ERROR_FIELD_NO_VALUE;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_FIELD_NO_VALUE_MSG,S-1);
_state = Error;
return true;
}
_state = Field;
}
return true;
case 't':
if (!_lc.capture()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
if ('r' != _lc.advance()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
if (!_lc.capture()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
if ('u' != _lc.advance()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
if (!_lc.capture()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
if ('e' != _lc.advance()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
if (!_lc.capture()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
_lc.advance();
_lc.trySkipWhiteSpace();
ch = _lc.current();
if (',' != ch && ']' != ch && '}' != ch && LexContext<S>::EndOfInput != ch) {
_lastError = JSON_ERROR_UNEXPECTED_VALUE;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_UNEXPECTED_VALUE_MSG,S-1);
_state = Error;
}
return true;
case 'f':
if (!_lc.capture()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
if ('a' != _lc.advance()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
if (!_lc.capture()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
if ('l' != _lc.advance()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
if (!_lc.capture()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
if ('s' != _lc.advance()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
if (!_lc.capture()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
if ('e' != _lc.advance()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
if (!_lc.capture()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
_lc.advance();
_lc.trySkipWhiteSpace();
ch = _lc.current();
if (',' != ch && ']' != ch && '}' != ch && LexContext<S>::EndOfInput != ch) {
_lastError = JSON_ERROR_UNEXPECTED_VALUE;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_UNEXPECTED_VALUE_MSG,S-1);
_state = Error;
}
return true;
case 'n':
if (!_lc.capture()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
if ('u' != _lc.advance()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
if (!_lc.capture()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
if ('l' != _lc.advance()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
if (!_lc.capture()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
if ('l' != _lc.advance()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
if (!_lc.capture()) {
_lastError = JSON_ERROR_OUT_OF_MEMORY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_OUT_OF_MEMORY_MSG,S-1);
_state = Error;
return true;
}
_lc.advance();
_lc.trySkipWhiteSpace();
ch = _lc.current();
if (',' != ch && ']' != ch && '}' != ch && LexContext<S>::EndOfInput != ch) {
_lastError = JSON_ERROR_UNEXPECTED_VALUE;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_UNEXPECTED_VALUE_MSG,S-1);
_state = Error;
}
return true;
default:
// Serial.printf("Line %d, Column %d,
// Position %d\r\n",_lc.line(),_lc.column(),(int32_t)_lc.position());
_lastError = JSON_ERROR_UNEXPECTED_VALUE;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_UNEXPECTED_VALUE_MSG,S-1);
_state = Error;
return true;
}
default:
_state = Value;
goto value_case;
}
}
这里的每个部分都会检查当前状态和光标下的字符,以确定下一步该做什么。这种解析形式类似于 LL(1)。实际上,由于 JSON 比 LL(1) 更容易解析,所以它并不比匹配正则表达式难多少。由于我们没有单独的词法分析器,我们的解析器也负责词法分析,除了低级别的词法分析,例如 trySkipWhiteSpace()
,它委托给 LexContext
。繁琐的部分是确定我们正在处理一个数字的部分,以及确定我们遇到了 true
、false
或 null
的部分。除此之外,它相当直接。
作为一种优化,此解析器支持在跳过文档部分的同时进行部分解析。它只进行足够的解析来确定文档是否格式正确,但否则不对任何内容进行规范化,从而加快了操作速度。
我们有两个例程用于跳过嵌套对象和数组。当数组嵌套在对象中或反之亦然时,它们会递归地调用对方。由于它们几乎相同,我们将只介绍其中一个。
void skipObjectPart()
{
int depth = 1;
while (Error!=_state && LexContext<S>::EndOfInput != _lc.current())
{
switch (_lc.current())
{
case '[':
if(LexContext<S>::EndOfInput==_lc.advance()) {
_lastError = JSON_ERROR_UNTERMINATED_ARRAY;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_UNTERMINATED_ARRAY_MSG,S-1);
_state = Error;
return;
}
skipArrayPart();
break;
case '{':
++depth;
_lc.advance();
if(LexContext<S>::EndOfInput==_lc.current())
_lastError = JSON_ERROR_UNTERMINATED_OBJECT;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_UNTERMINATED_OBJECT_MSG,S-1);
_state = Error;
break;
case '\"':
skipString();
break;
case '}':
--depth;
_lc.advance();
if (depth == 0)
{
_lc.trySkipWhiteSpace();
return;
}
if(LexContext<S>::EndOfInput==_lc.current()) {
_lastError = JSON_ERROR_UNTERMINATED_OBJECT;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_UNTERMINATED_OBJECT_MSG,S-1);
_state = Error;
}
break;
default:
_lc.advance();
break;
}
}
}
这些又被 skipSubtree()
使用。
bool skipSubtree()
{
switch (_state)
{
case JsonReader<S>::Error:
return false;
case JsonReader<S>::EndDocument: // eos
return false;
case JsonReader<S>::Initial: // initial
if (read())
return skipSubtree();
return false;
case JsonReader<S>::Value: // value
return true;
case JsonReader<S>::Field: // field
if (!read())
return false;
return skipSubtree();
case JsonReader<S>::Array:// begin array
skipArrayPart();
_lc.trySkipWhiteSpace();
_state = EndArray; // end array
return true;
case JsonReader<S>::EndArray: // end array
return true;
case JsonReader<S>::Object:// begin object
skipObjectPart();
_lc.trySkipWhiteSpace();
_state = EndObject; // end object
return true;
case JsonReader<S>::EndObject: // end object
return true;
default:
_lastError = JSON_ERROR_UNKNOWN_STATE;
strncpy_P(_lc.captureBuffer(),JSON_ERROR_UNKNOWN_STATE_MSG,S-1);
_state = Error;
return true;
}
}
在这里,我们根据您当前所在的位置跳过下一个子树。如果您处于初始节点,子树就是整个文档。如果您正在处理一个值,它已经被下一个 read()
调用跳过了。如果您正在处理一个字段,请读取到下一个元素——字段的值,然后跳过它。如果我们正在处理一个数组或对象,我们将使用上面概述的嵌套跳过例程。
为了搜索,我们提供了 skipToIndex()
和 skipToField()
等方法。这些方法允许您通过查询字段名称或数组索引来遍历文档。
bool skipToIndex(int index) {
if (Initial==_state || Field == _state) // initial or field
if (!read())
return false;
if (Array==_state) { // array start
if (0 == index) {
if (!read())
return false;
}
else {
for (int i = 0; i < index; ++i) {
if (EndArray == _state) // end array
return false;
if (!read())
return false;
if (!skipSubtree())
return false;
}
if ((EndObject==_state || EndArray==_state) && !read())
return false;
}
return true;
}
return false;
}
请注意,上面的是针对数组的。以下是针对对象的。
bool skipToField(const char* field, bool searchDescendants = false) {
if (searchDescendants) {
while (read()) {
if (Field == _state) { // field
undecorate();
if (!strcmp(field , value()))
return true;
}
}
return false;
}
switch (_state)
{
case JsonReader<S>::Initial:
if (read())
return skipToField(field);
return false;
case JsonReader<S>::Object:
while (read() && Field == _state) { // first read will move
// to the child field of the root
undecorate();
if (strcmp(field,value()))
skipSubtree(); // if this field isn't the target so just skip over the rest of it
else
break;
}
return Field == _state;
case JsonReader<S>::Field: // we're already on a field
undecorate();
if (!strcmp(field,value()))
return true;
else if (!skipSubtree())
return false;
while (read() && Field == _state) { // first read will move to the child field of the root
undecorate();
if (strcmp(field , value()))
skipSubtree(); // if this field isn't the target just skip over the rest of it
else
break;
}
return Field == _state;
default:
return false;
}
}
这比 skipToIndex()
复杂得多,主要是因为有太多特殊情况需要处理。此外,与前一种方法不同,这种方法需要能够搜索或跳过后代。乍一看可能有点奇怪,但实际上“递归”搜索字段更容易,因为您不必为了保持在同一层级的层次结构而跳过子树。
至于存储和检索元素标量值和字段名称,我们使用 LexContext
的 captureBuffer()
。这样做可以节省宝贵的 RAM,而不是将其从缓冲区复制出来。为了节省 RAM,我们还采取了另一项措施,即规定任何数据转换都应尽可能就地进行。这意味着我们有一个 undecorate()
函数,它可以删除字符串中的引号并转换转义字符。它这样做是就地进行的,因为它知道结果字符串总是比输入字符串短。这是因为非转义字符总是比转义字符短,并且引号总是被剥离。
void undecorate() {
char *src = _lc.captureBuffer();
char *dst = src;
char ch = *src;
if ('\"' != ch)
return;
++src;
uint16_t uu;
while ((ch = *src) && ch != '\"') {
switch (ch) {
case '\\':
ch = *(++src);
switch (ch) {
case '\'':
case '\"':
case '\\':
case '/':
*(dst++) = ch;
++src;
break;
case 'r':
*(dst++) = '\r';
++src;
break;
case 'n':
*(dst++) = '\n';
++src;
break;
case 't':
*(dst++) = '\t';
++src;
break;
case 'b':
*(dst++) = '\b';
++src;
break;
case 'u':
uu = 0;
ch = *(++src);
if (isHexChar(ch)) {
uu = fromHexChar(ch);
ch = *(++src);
uu *= 16;
if (isHexChar(ch)) {
uu |= fromHexChar(ch);
ch = *(++src);
uu *= 16;
if (isHexChar(ch)) {
uu |= fromHexChar(ch);
ch = *(++src);
uu *= 16;
if (isHexChar(ch)) {
uu |= fromHexChar(ch);
ch = *(++src);
}
}
}
}
if (0 < uu) {
// no unicode
if (256 > uu) {
*(dst++) = (char)uu;
} else
*(dst++) = '?';
}
}
break;
default:
*dst = ch;
++dst;
++src;
}
}
*dst = 0;
}
这不太方便。它所做的是:它管理着同一个缓冲区上的两个光标。目标光标 *dst
比源光标 *src
至少慢一个位置,因为前面有一个引号。基本上,我们只是将字符从源复制到目标,直到遇到转义字符,此时我们进行转换。当我们找到最后一个引号时,我们就完成了,并在新结束位置重新终止字符串。
另一个有趣的函数是 valueType()
,它告诉我们正在查看的 JSON 值是什么类型——请注意,在调用 undecorate()
之后不应调用它。
int8_t valueType() {
char *sz = _lc.captureBuffer();
char ch = *sz;
if('\"'==ch)
return String;
if('t'==ch || 'f'==ch)
return Boolean;
if('n'==ch)
return Null;
return Number;
}
我们在这里采取了一些自由。我们所做的就是检查第一个字符,对于数字,我们甚至都不这样做,我们只是通过排除法来达到它。这只有在我们解析时已经检查过这些值时才可靠。例如,如果它以 t
开头,我们就知道它将是 true
,因为除了被引号包围之外,没有其他东西允许以 t
开头。我们已经知道它不是 tree
,因为解析器在此之前就会出错。现在您可以看到如果在调用 undecorate()
之前调用它所造成的破坏!
我们现在已经涵盖了整个库的核心内容,接下来的事情就取决于您了。我希望您喜欢这项贡献,并希望您的代码能够精简、美观且不易出错。
历史
- 2020 年 12 月 9 日 - 初次提交
- 2020 年 12 月 9 日 - 更新:添加了“工作原理”部分。
- 2020 年 12 月 10 日 - 更新 2:添加了更好的错误处理和 bug 修复。
- 2020 年 12 月 10 日 - 更新 3:修复了在某些内存不足情况下错误消息不正确的 bug。
- 2020 年 12 月 11 日 - 更新 4:修复了跳过 bug,将 Key 改为 Field,移除了非标准跳过并更新了文章代码。