65.9K
CodeProject 正在变化。 阅读更多。
Home

jRead -就地 JSON 元素读取器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.72/5 (26投票s)

2015 年 3 月 12 日

CPOL

11分钟阅读

viewsIcon

55421

downloadIcon

1874

不是“另一个解析器”,它以简单且无内存开销的方式在 C 中读取 JSON 元素

引言

jRead 是一个简单的原位 JSON 元素读取器,它将输入的 JSON 保持为未更改的文本,并允许直接对其进行查询。jRead 用 C 语言编写,体积小、速度快,并且不分配任何内存 - 这使其成为嵌入式应用程序或只需要从 JSON 块中读取少量值而无需学习大型 API 且程序会生成大量相互关联的结构来表示 JSON 的理想选择。

基本上,API 是一个单一函数调用

jRead( pJsonText, pQueryString, pOutputStruct );

如果您真的很有冒险精神,还有另外两个函数和一些可选的“辅助函数”。

背景

在许多情况下,在 C 或 C++ 中处理 JSON 非常痛苦。我查看了几个 C++ JSON 解析器,它们都有一个学习曲线,并且必须使用结构或类来表示 JSON 的节点。

此外,“我只想从这么多 JSON 中获取几个值!”、“我该如何????为如此简单的工作使用这个 API?”、“我不在乎编写 JSON 回复 - 那是容易的部分!”以及“我的嵌入式目标不是由内存组成的!”之类的感叹,都是我最终编写 jRead 的原因。

关注点

代码是用纯 C 编写的,全部在 jRead.cjRead.h 中,它没有任何依赖项,因此应该可以在任何系统上为任何目标进行编译。它不会 malloc() 任何内存,只需要一些栈用于函数递归。

它与其说是解析 JSON,不如说是遍历它 - 查找您感兴趣的元素。它可以用于提取终端值(string、数字等),也可以用于提取整个对象或数组。它使得读取 JSON 并根据您的应用程序将值放入本机 C 变量、数组或结构中变得容易。

代码还附带 main.c,其中包含大量示例,并作为命令行实用程序运行以查询 JSON 文件。项目文件包含用于 Visual Studio 10 的项目文件以及用于 Windows 的命令行 .exe

jRead 的示例

假设我们有一个简单的 JSON string,我们已将其读入某个缓冲区 pJson,如下所示:

        {
            "astring":"This is a string",
            "anumber":42,
            "myarray":[ "zero", 1, {"description":"element 2"}, null ],
            "yesno":true,
            "PI":"3.1415926",
            "foo":null
        }

我们可以使用 jRead 获取任何元素的值,例如:

struct jReadElement result;
jRead( pJson, "{'astring'", &result );

在这种情况下,结果将是

result.dataType= JREAD_STRING
result.elements= 1
result.byteLen=  16
result.pValue -> "This is a string"

请注意,没有任何内容被复制,结果结构只是为您提供指向元素开始位置的指针及其长度。

查询字符串定义了如何遍历 JSON 以最终获得返回的值。查询的元素可以是:

"{'keyname'"           Object element "keyname", returns value of that key
"{NUMBER"              Object element[NUMBER], returns keyname of that element
"[NUMBER"              Array element[NUMBER], returns value from array

用您的 JavaScript 思维方式来考虑,很明显

jRead( pJson, "{'myarray'", &result );

将返回

result.dataType= JREAD_ARRAY
result.elements= 4
result.byteLen=  46
result.pValue -> [ "zero", 1, {"description":"element 2"}, null ]

将查询元素串联起来,我们可以按此方式检索封闭对象中的值:

jRead( pJson, "{'myarray' [2 {'description'", &result );

给出

result.pValue -> "element 2"

您可能已经注意到,查询字符串使用 '单引号' 来括住键名,而不是 JSON 所需的 "双引号" - 这只是为了方便输入查询字符串(有一个选项可以更改此设置)。必须使用双引号会显得很乱,例如:

jRead( pJson, "{\"myarray\"[2{\"description\"", &result );      \\ ugh!

辅助函数可以轻松地将单个值提取到 C 变量中,例如:

jRead_string( pJson, "{'astring'", destString, MAXLEN );
my_int= jRead_int( pJson, "{'myarray'[1" );
my_long= jRead_long( pJson, "{'anumber'" );
my_double= jRead_double( pJson, "{'PI'[3" );

请注意,'int' 辅助函数会执行一些“类型强制转换” - 因为无论如何一切都是 string,调用 jReadInt() 总是会返回一个值:对于 JSON 42 或 "42" 它将返回 42,对于 "foo" 则返回零。它还为 true 返回 1,为 falsenull 返回 0。

高级用法

假设我们有一些合理的 JSON(而不是上面示例中那种牵强的混搭),您可能会有对象数组,然后您会说,通过数组索引很麻烦,因为您必须构建一个查询字符串。

啊哈!我说,您可以使用 查询参数

更可能的是,您会得到一些 JSON,其中您想要的部分嵌入其中,如下所示:

{
  "Company": "The Most Excellent Example Company",
  "Address": "Planet Earth",
  "Numbers":[
              { "Name":"Fred",   "Ident":12345 },
              { "Name":"Jim",    "Ident":"87654" },
              { "Name":"Zaphod", "Ident":"0777621" }
            ]
}

您可以通过以下方式提取名称和数字:

#define NAMELEN 32
struct NamesAndNumbers{      // our application wants the JSON "Numbers" array data
    char Name[NAMELEN];      // in an array of these structures
    long Number;
};
...
struct NamesAndNumbers people[42];
struct jReadElement element;
int i;
  jRead( pJson, "{'Numbers'", &element );    // we expect "Numbers" to be an array
  if( element.dataType == JREAD_ARRAY ) 
  {
      for( i=0; i<element.elements; i++ )    // loop for no. of elements in JSON
      {
          jRead_string( pJson, "{'Numbers'[*{'Name'", people[i].Name, NAMELEN, &i );
          people[i].Number= jRead_long( pJson, "{'Numbers'[*{'Ident'", &i ); 
      }
  }

...换句话说,您可以提供一个 int(或 int[])的指针,其值在查询字符串中用于替换 * 标记(有点像 prinf() 中的参数)。有关索引数组的使用,请参见 main.c runExamples()

代码工作原理

包含 JSON 的输入 string 必须以 '\0' 结尾,这为我们提供了搜索的后备,同样,查询字符串也以 '\0' 结尾。

与普通解析器不同,它在遍历查询 string 的同时遍历 JSON,只需跳过元素同时跟踪对象和数组的层次结构。

该例程从 JSON 和查询中提取一个标记,这有两个有效的结果:

  1. 查询中没有剩余内容
  2. 标记匹配

如果查询中没有剩余内容,我们希望结果是此时剩余的 JSON,即我们返回整个 JSON 值(如果查询最初为空,则返回整个输入 JSON)。

如果标记匹配(例如,它们都是 '{',即标记 JREAD_OBJECT),那么我们期望查询字符串有一个附加的说明符:

  1. 如果它是数组(或者我们想要对象的“键”)的索引。
  2. 如果它是对象的键值。

此时,我们需要逐步遍历 JSON 直到找到感兴趣的元素,在对象的情况下,我们期望元素的形式为:

"key string" JREAD_COLON <value> JREAD_COMMA | JREAD_EOBJECT

如果键字符串是我们想要的(它与查询键匹配),那么我们递归调用 jReadParam(),指向 JSON 中预期 <value> 的开始位置,并指向查询字符串的剩余部分。

如果此键字符串不是我们想要的,我们只需调用 jRead() 并传入一个空查询字符串 来遍历我们不感兴趣的整个 JSON 值。

在查询字符串的末尾,我们可能有一个“终端值”(数字、字符串、布尔值等),或者我们可能有一个对象或数组。如果是终端值,我们只返回该单个值元素。否则,我们希望返回对象/数组中的元素数量。

使用 jReadCountObject()jReadCountArray() 这两个单独的例程来遍历对象和数组并返回它们的长度和元素计数。

“遍历”与“解析”

虽然严格来说,“解析”意味着将某些输入分解为其组成部分,但 JSON 解析器通常被期望创建表示输入 JSON 的数据结构。jRead 也必须“解析”输入,但可能更好地描述为遍历 JSON 以提取元素。

为了使应用程序能够利用任何 JSON 数据,这些数据最终必须成为某种本机数据类型(或结构/对象)。解析器通常不会这样做,解析后的 JSON 会进入一个数据结构,该结构由形成 API 的函数或方法访问,而 jRead 的目的是直接填充某些应用程序特定的数据结构。

这里的示例是将 JSON 对象数组中的整数值(“Users”)读取到 C 整数数组中。

速度

嗯,这取决于...

由于 jRead 遍历 JSON 以查找要返回的元素,因此它可以很快地完成此操作 - 但是,它必须为每个您想要的值执行此操作。此外,位于前面的元素可以快速找到(输入 JSON 的其余部分甚至不需要查看),而对于位于输入末尾的元素,我们必须遍历整个输入。

在上面的“高级”查询参数示例中,我们通过从对象中获取“Numbers”元素,然后对其执行进一步查询(即,我们启动一个 jRead 查询,指向 JSON 中嵌入的数组)来进一步混淆了“速度”问题 - 这可以节省一次又一次地遍历封闭对象。

我进行了一些速度测试,最新版本中的命令行程序中内置了一个简单的测试。使用此程序,我在 Windows 7 下的 DOS 框中的 3.4GHz i7 处理器每秒可以处理 770,000 次查询(通过让它在一个循环中执行数百万次来计时),大约是每秒 1.3 微秒/查询。进一步捣鼓更长的 JSON 文件使我能够估计每遍历一个 JSON 元素大约需要 50 纳秒。这听起来相当快!

啊,没那么快... 如果您有一个包含 10,000 个对象的 JSON 数组,例如:

[ {"DateTime":"2014-02-08T00:00:00Z","Users":1},
  ... 
  {"DateTime":"2014-02-14T22:39:00Z","Users":10000} ]

那么获取最后一个大约需要 1.4 毫秒。但是,如果您调用 jRead 10,000 次来读取每个条目,平均每条目需要 0.74 毫秒 - 总共需要 7.4 才能完成所有操作。

这就是为什么我添加了“步进”函数 - 见下文。

添加数组步进函数

由于 jRead 在大型数组上的性能非常差,我添加了另一个函数:

jReadArrayStep( char *pJsonArray, struct jsonElement *result )

意识到 jRead 的全部意义在于简单地将 JSON 中的值提取到您拥有的任何漂亮的 C 变量中,并且 jRead 的工作方式是遍历 JSON - 添加此函数是很自然的。

jReadArrayStep() 仅从数组中提取一个元素,将其返回在 jsonElement 中,并返回指向数组其余部分的指针。

// identify the whole JSON element
jRead( (char *)json.data, "", &arrayElement );
if( arrayElement.dataType == JREAD_ARRAY )              // make sure we have an array
{
    int step;
    pJsonArray= (char *)arrayElement.pValue;
    for( step=0; step<arrayElement.elements; step++ )   // step thru all array elements
    {
        pJsonArray= jReadArrayStep( pJsonArray, &objectElement );
        if( objectElement.dataType == JREAD_OBJECT )    // we expect an object
        {
            // query the object and save in our application array
            UserCounts[step]= jRead_int( (char *)objectElement.pValue, "{'Users'", NULL  );
        }else
        {
            printf("Array element wasn't an object!\n");
        }
    }
}

现在我们开始有点进展了:与单独执行索引查询需要 7.4 秒相比,使用 jReadArrayStep() 可以在短短 3.1 毫秒内读取所有 10,000 个值(使用上述代码,识别整个输入以获取 arrayElement 需要额外的 1.5 毫秒 - 总共 4.8 毫秒)。

您可以为 JSON 对象添加类似的“步进”函数,并返回 KeyValue。我没有实现这一点,因为您仍然需要进行 string 匹配返回的 Key 才能使用 Value,而且 JSON 对象通常没有大量的键。

与解析器的比较

我将 jRead 与我修改过的 C++ 中的 simpleJSON 版本进行了比较(原始项目是:https://github.com/MJPA/SimpleJSON

使用 SimpleJSON 解析包含 10,000 个对象的数组 JSON 需要 48.4 毫秒,再加上另外 0.4 毫秒将 10,000 个值读取到 int 数组中,总共 48.8 毫秒,并使用了几个兆字节的内存用于结构。

与无内存开销的 4.8 毫秒相比,jRead 在这种情况下看起来是明显的赢家。

当然,几毫秒的差异可能无关紧要 - 但这都是在快速的 3.4GHz i7 机器上进行的,在嵌入式设备上运行可能会慢 100 倍,差异会非常显著(在一台旧的 Sony 1.2GHz Pentium 'M' 笔记本电脑上,simpleJson 需要 282.2 毫秒,而 jRead 需要 19.1 毫秒)。

jRead 的用例

如果您有嵌入式系统或无法承受内存占用,那么 jRead 具有明显优势。如果您的输入 JSON 相对较小(几 KB) - 通常用于在通信应用程序之间传递参数 - 那么 jRead 可以节省解析的内存和时间。如果您只需要整个 JSON 的一部分,那么您可以使用 jRead 定位您感兴趣的元素,然后要么对其进行查询 - 甚至对其使用解析器。

如果您使用数组步进函数,则将大型对象数组转换为某些本机格式的速度相当快,因为它只需遍历源一次即可将所有值放到您想要的位置。

请记住,在 C(或 C++)中,JSON 文件中定义的结构与语言的映射非常不好,解析器必须构建数组/结构/对象来表示 JSON 中的每个节点。一旦解析器完成了它的工作,您仍然需要遍历结构才能真正访问终端值。

jRead 的最后一个优势是它的简单性:它只需编译并运行,您无需学习某些 API 或在调用 yetAnotherParser::Parse() 后弄清楚如何遍历一堆类对象。

如果您必须处理大量 JSON 并且不想将数据翻译成您自己的应用程序存储,但又想不断访问值,那么 jRead 在每次都必须遍历源时都处于不利地位。如果您想读取/修改/写入一定量的 JSON,那么解析器会是更好的选择。

Arduino 版本

jRead 的同一版本已“移植”用于 Arduino。源代码是 .cpp 文件,并进行了一些小的修改以兼容 Arduino IDE。演示 sketch 完全自包含,可以在 Arduino UNO 上运行。许多示例查询的结果会打印到串行监视器。

结论

希望这对大家有所帮助,它是免费使用的,可以复制、重写或随意使用。看看代码,里面有很多注释、信息和示例。

如果您使用它,并且碰巧在一家出售好啤酒的酒吧里遇到我……我会来一杯 Tim Taylors - 谢谢!

TonyWilk

© . All rights reserved.