如何在万行 JSON 中提取元素?答案是… 🧐

JSON,全称 JavaScript Object Notation,2001 的时候由道格拉斯 (Douglas Crockford)所发现并定义。22 岁的大学生才刚毕业,而 22 岁的 JSON 已经成为互联网数据交换的标准之一并且为广大开发者所熟知。

作为开发者之一,也算是阅 JSON 无数,JSON 有胖有瘦,有长有短。短个的 JSON 可以仅仅是仅含一个布尔值的数组:

[true]

而长的 JSON,则可以到达惊人的上万行:

在阅读长长的 JSON 的过程中,最常遇到的需求是想要提取某一个字段的值,或者遍历数组的元素对象值,而这些,都埋在了深深的 JSON 长河中。

对于一个编程新鸟,最先想到的方式是,通过编程来获取想要的字段,假设有如下结构的 JSON:

{
  "human": {
    "person": {
      "man": [
        {
          "name": "Jack",
          "age": "17"
        },
        {
          "name": "Mike",
          "age": "32"
        },
        {
          "name": "John",
          "age": "23"
        },
        {
          "name": "David",
          "age": "41"
        },
        {
          "name": "Eric",
          "age": "29"
        },
        {
          "name": "Chris",
          "age": "38"
        },
        {
          "name": "Tom",
          "age": "27"
        },
        {
          "name": "Peter",
          "age": "35"
        },
        {
          "name": "Robert",
          "age": "26"
        },
        {
          "name": "Daniel",
          "age": "33"
        }
      ]
    }
  }
}

现在我需要获取 man 数组下所有元素中的 name,那么我们肯定会写这边一份 JS 代码:

const nameSet = data.human.person.man.map(manItem => manItem.name)

得益于现代 JavaScript 的语法糖和 API,写出来的代码很简洁。但代码简洁,却背后还有很多问题要考虑:一是需要保存这份代码文件,二是需要运行这份代码的环境,三是需要打印或者保存执行代码的结果,四是需要不断调试这份代码和处理异常边界情况等等

编程新鸟继续思考,在 JSON 中索引值的需求场景很多,笨拙地编程似乎不是高效的方法,于是想要借助最近如火如荼的 ChatGPT:

  • 🧑‍💻 我有如上的 JSON 结构,帮我取出 man 字段下所有元素的 name 字段

  • 🤖 帮你取出 man 字段下所有元素的 name,结果如下:
    Jack
    Mike,
    John,
    David,
    Eric,
    Chris,
    Tom,
    Peter,
    Robert,
    Daniel

编程新鸟兴冲冲地获得了结果,很开心,但很快就遇到另外一个麻烦了,当把上万的 JSON 粘贴到 ChatGPT 输入框,很快就得到了这样的异常提示:

强如 ChatGPT 在又臭又长的文本面前也是摆烂。当然,解决方法也不是没有,分段输入,或者使用编程式调用 ChatGPT 的 API,如果是这样的话,也就回到了起点。

那么,还有没有更好的方式呢?🧐

那必须是 JSON Path!这里我们来使用 He3 的 JSON Path 工具举例,对于上述取 name 字段集合的场景,我们只需要在输入框中输入 $..name 就可以实现:

JSON Path 工具地址:https://t.he3app.com?c9yj

一开始看到 $..name 的时候一脸懵逼,这是什么语法?为什么可以实现?

首先,让我来介绍下 JSON Path:

JSON Path 是一种用于在 JSON 数据中定位和提取特定元素的表达式语言,提供了一种简洁的语法,使得从复杂的 JSON 结构中提取数据变得容易。

简单讲,JSON Path 同 Markdown 一样,一门轻量级的语法,能够提取 JSON 结构片段。

既然 JSON Path 是一门语法,那么有些人就会犯难了,语法就要掌握它的语法点、关键词,有学习成本在。但请相信,JSON Path 学习成本低,学会了之后,你就可以提取特定键的值、遍历数组、根据条件筛选元素、切片等等高端操作,处理 JSON 将游刃有余。

首先先介绍下 JSON Path 的一些常见语法:

  • $:根节点,表示 JSON 数据的最外层。
  • .:子节点操作符,用于访问对象中的属性。
  • []:索引操作符,用于访问数组中的元素或通过条件筛选元素。
  • *:通配符,用于匹配任意属性名或数组索引。
  • ..:递归下降符号,用于搜索嵌套结构中的所有层级。
  • @:当前节点,可以用于在筛选条件中引用当前节点。

假设我们现在在上诉例子 JSON 中新增了一个 "god": "God" 键值对,并且我们想要用 JSON Path 取得 god 字段,那么就可以用 $.god

对于 $..name,我们就知道它表达的是”返回 JSON 数据中所有层级中具有 name 键的值“,那么如果我们要获取所有 age,则可以改成 $..age

如果我们要获取 man 数组的第一个元素,则可以输入$.human.person.man[0]

当然, 使用递归下降符号更加方便 $..man[0]

对于索引引用符 [] ,还有更多高阶操作。

比如要获取第 1、2、3 个元素,则可以输入 $..man[0,1,2]

对于取 1, 2, 3 这样连续的数组元素,JSON Path 有 [start:end] 更加方便的表达式,上诉可以改成 $..man[0:3]:

❗️ 这里要注意一下,表达式为 [0,3 是因为取出来的元素包含 start 但不包含 end

JSON Path 还提供了两种表达式:

  1. ()表达式,用于进行条件判断或进行逻辑操作。可以在括号内使用比较运算符(如 >, <, == 等)和逻辑运算符(如 &&, ||)来定义条件。例如,(@.length) 表示获取数组中的最后一个元素。

  2. ?()过滤表达式。在 ?() 中,可以使用任意合法的 JavaScript 表达式来对元素进行筛选。这样的表达式在过滤器内部被计算,并根据其结果决定是否选择或排除当前元素。例如,[?(@.age > 25)] 表示根据元素的 “age” 属性筛选出年龄大于 25 的元素。

有了这两个表达式,我们就可以进一步提高对数据提取的精度。比如现在要获取 man 的最后一个元素,我们可以用 $..man[(@.length-1)]

@ 表示当前元素,.length 表示当前元素的长度,(@.length-1) 就能够表示获取数组最后一个元素。

如果我们想要过滤 man 字段中 age 大于等于 33 岁的人,则可以用过滤表达式 $..man[?(@. age >= 33)]

以上,就掌握了 JSON Path 大部分常用的语法,对于剩下的不常用,可以见:JSON Path Plus 实现

JSON Path 没有一个官方的标准文档,但有一些被广泛接受和使用的实现和文档,JSON Path Plus 就是其中之一,He3 的 JSON Path 工具采用 JSON Path Plus 实现。

回归题目,在如下上万行 JSON 中:

想要查看是否有 friends 叫做 Ellen Rowland 的人,则可以: