如何使用 jq 转换 JSON 数据

作者选择了 自由和开源基金作为 写给捐款计划的一部分接受捐款。

介绍

在处理大型JSON文件时,很难找到和操纵所需的信息,你可以复制和粘贴所有相关的片段来手动计算总数,但这是一个耗时的过程,可能容易出现人为错误,另一种选择是使用通用工具来查找和操纵信息,所有现代的Linux系统都配备了三种已知的文本处理工具:sed,awkgrep

jq,一个命令行 JSON 处理工具,是处理机器可读数据格式的好解决方案,在壳脚本中尤其有用。使用 jq 可以帮助您在需要操纵数据时。例如,如果您运行一个 curl 呼叫到 JSON API, jq 可以从服务器的响应中提取特定信息。您也可以将 jq 纳入您的数据摄入过程作为数据工程师。 如果您管理一个 Kubernetes 集群,您可以使用 kubectl 的 JSON 输出作为 jq 的输入源来提取特定部署的可用复制品数量。

在本文中,您将使用jq来转换海洋动物的样本 JSON 文件. 您将使用过滤器应用数据转换并将转换的数据合并为一个新的数据结构。

前提条件

要完成本教程,您将需要以下内容:

  • jq,一个JSON解析和转换工具. 它可以从所有主要的Linux发行版的存储库中获得。 如果您正在使用Ubuntu,请运行sudo apt install jq来安装它。
  • 对JSON语法的理解,您可以在 An Introduction to JSON中更新。

步骤 1 – 执行你的第一个jq命令

在此步骤中,您将设置样本输入文件并通过运行jq命令来测试设置,以生成样本文件数据的输出。

创建并使用您喜爱的编辑器打开名为seaCreatures.json的新文件(本教程使用nano):

1nano seaCreatures.json

将以下内容复制到文件中:

1[label seaCreatures.json]
2[
3    { "name": "Sammy", "type": "shark", "clams": 5 },
4    { "name": "Bubbles", "type": "orca", "clams": 3 },
5    { "name": "Splish", "type": "dolphin", "clams": 2 },
6    { "name": "Splash", "type": "dolphin", "clams": 2 }
7]

至本教程结束时,您将写一个单行jq命令,回答有关此数据的下列问题:

  • 海洋生物名称列表形式是什么?
  • 海洋生物总共拥有多少鱼?
  • 这些鱼中有多少鱼属于海豚?

保存并关闭文件。

除了输入文件外,您还需要一个 filter 来描述您想要做的确切转换。

您可以使用身份运算器来测试您的设置是否奏效. 如果您看到任何解析错误,请检查seaCreatures.json是否包含有效的 JSON。

使用以下命令将 ID 操作员应用于 JSON 文件:

1jq '.' seaCreatures.json

当你使用jq与文件时,你总是通过一个过滤器,然后是输入文件. 由于过滤器可能包含对你的壳具有特殊意义的间隔和其他字符,这是一个很好的做法,将你的过滤器包装成单个引用标记。 这样做会告诉你的壳,过滤器是一个命令参数。 请放心,运行jq不会改变你的原始文件。

您将收到以下输出:

 1[secondary_label Output]
 2[
 3  {
 4    "name": "Sammy",
 5    "type": "shark",
 6    "clams": 5
 7  },
 8  {
 9    "name": "Bubbles",
10    "type": "orca",
11    "clams": 3
12  },
13  {
14    "name": "Splish",
15    "type": "dolphin",
16    "clams": 2
17  },
18  {
19    "name": "Splash",
20    "type": "dolphin",
21    "clams": 2
22  }
23]

默认情况下,jq将非常好的打印其输出。它会自动应用插入,在每个值后添加新行,并在可能时颜色其输出。彩色可以提高可读性,这可以帮助许多开发人员检查其他工具产生的JSON数据。

随着输入文件的设置,您将使用几个不同的过滤器来操纵数据,以计算所有三个属性的值: creatures, totalClamstotalDolphinClams. 在下一步,您将从 creatures 值中找到信息。

第2步:恢复生物的价值

在此步骤中,您将生成所有海洋生物的列表,使用生物值来找到它们的名字。

1[secondary_label Output]
2[
3  "Sammy",
4  "Bubbles",
5  "Splish",
6  "Splash"
7],

生成此列表需要提取生物的名字,然后将它们合并成一个数组。

您将需要改进您的过滤器以获取所有生物的名称,并丢弃其他所有东西. 由于您正在处理一个数组,您需要告诉‘jq’您想要操作该数组的值,而不是数组本身。

使用修改过滤器运行jq:

1jq '.[]' seaCreatures.json

每个数组值现在分别输出:

 1[secondary_label Output]
 2{
 3  "name": "Sammy",
 4  "type": "shark",
 5  "clams": 5
 6}
 7{
 8  "name": "Bubbles",
 9  "type": "orca",
10  "clams": 3
11}
12{
13  "name": "Splish",
14  "type": "dolphin",
15  "clams": 2
16}
17{
18  "name": "Splash",
19  "type": "dolphin",
20  "clams": 2
21}

要输出名称属性的值,而不是输出每个数组项目的全部值,你需要输出名称属性的值,然后丢掉其余的值。 pipe operator annoo 将允许你对每个输出应用过滤器。

可以通过键入.name来访问 JSON 对象的名称属性,将管道与过滤器相结合,并在seaCreatures.json上运行此命令:

1jq '.[] | .name' seaCreatures.json

您会注意到其他属性已从输出中消失:

1[secondary_label Output]
2"Sammy"
3"Bubbles"
4"Splish"
5"Splash"

默认情况下,jq输出是有效的JSON,所以字符串将出现在双引文标记("")中。如果您需要没有双引文的字符串,请添加-r标志以启用原始输出:

1jq -r '.[] | .name' seaCreatures.json

标签已经消失了:

1[secondary_label Output]
2Sammy
3Bubbles
4Splish
5Splash

您现在知道如何从 JSON 输入中提取特定信息. 您将使用此技术在下一步找到其他特定信息,然后在最后一步中生成生物值。

步骤 3 — 计算totalClams值与mapadd

在此步骤中,您将找到所有生物拥有多少的总信息. 您可以通过汇总几个数据来计算答案. 一旦您熟悉jq,这将比手动计算更快,更不容易出现人为错误。

步骤 2中,您从项目列表中提取了特定信息位. 您可以重复使用此技术来提取 clams 属性的值。

1jq '.[] | .clams' seaCreatures.json

clams属性的个别值将是输出:

1[secondary_label Output]
25
33
42
52

要查找个别值的总和,你需要添加过滤器。添加过滤器在数组上工作,但是,你目前正在输出数组值,所以你必须先将它们包装在一个数组中。

将现有过滤器围绕到 [],如下:

1jq '[.[] | .clams]' seaCreatures.json

值将出现在列表中:

1[secondary_label Output]
2[
3  5,
4  3,
5  2,
6  2
7]

在应用添加过滤器之前,您可以通过使用 map函数来提高您的命令的可读性,这也使其更容易维护。在一个数组上迭代,将过滤器应用于每个元素,然后将结果包装到一个数组中,可以通过一个map召唤来实现。在一个元素的数组中, map 将其参数作为过滤器应用于每个元素。例如,如果您将过滤器 map(.name) 应用到 [{"name": "Sammy"}, {"name": "Bubbles"},结果的 JSON 对象将是 `["Sammy", "Bubbles"]。

重写过滤器以生成数组以使用地图函数,然后运行它:

1jq 'map(.clams)' seaCreatures.json

您将获得与以前相同的输出:

1[secondary_label Output]
2[
3  5,
4  3,
5  2,
6  2
7]

既然您现在有一个数组,您可以将其导入添加过滤器中:

1jq 'map(.clams) | add' seaCreatures.json

您将收到一个数组的总和:

1[secondary_label Output]
212

使用此过滤器,您已经计算了截图的总数,您将在以后使用它来生成TotalClams值. 您已为三个问题中的两个写过滤器. 您还需要创建一个过滤器,然后可以生成最终的输出。

步骤 4 — 使用添加过滤器计算TotalDolphinClams

现在你知道生物拥有多少,你可以确定海豚有多少。你可以通过只添加满足特定条件的数组元素的值来生成答案。此步骤结束时的预期值为4,即海豚拥有的的总数。

相反,你会像你在 步骤 3中所做的那样,只计算具有海豚类型的生物所持有的海豚值。你会使用选择函数来选择一个特定的条件:选择(条件)。任何将条件评估为的输入都被传递。所有其他输入都被丢弃。例如,如果你的JSON输入是海豚,你的过滤器是选择(. ==海豚),输出将是海豚

若要将选择应用于数组中的每个值,您可以将其与地图相配,这样做时,不满足条件的数组值将被丢弃。

在您的情况下,您只想保留类型值等于海豚的数组值。

1jq 'map(select(.type == "dolphin"))' seaCreatures.json

您的过滤器不会匹配Sammy的鲨鱼和Orca的泡沫,但它将匹配两个海豚:

 1[secondary_label Output]
 2[
 3  {
 4    "name": "Splish",
 5    "type": "dolphin",
 6    "clams": 2
 7  },
 8  {
 9    "name": "Splash",
10    "type": "dolphin",
11    "clams": 2
12  }
13]

此输出包含每个生物的键数,以及一些不相关的信息. 若要只保留值,您可以将字段名称附加到地图参数的末尾:

1jq 'map(select(.type == "dolphin").clams)' seaCreatures.json

地图函数接收一个数组作为输入,并将地图的过滤器(作为参数)应用于每个数组元素. 因此,选择被调用四次,每一个生物一次。

您的输出将是一个仅包含两个匹配的生物的粘合值的数组:

1[secondary_label Output]
2[
3  2,
4  2
5]

将数组值输入到add中:

1jq 'map(select(.type == "dolphin").clams) | add' seaCreatures.json

您的输出将返回来自海豚类型的生物的``值的总和:

1[secondary_label Output]
24

您已成功结合地图选择来访问一个数组,选择匹配条件的数组项目,转换它们,并总结该转换的结果。

步骤五:将数据转化为新数据结构

在之前的步骤中,您编写过滤器来提取和操纵样本数据. 现在,您可以将这些过滤器结合起来生成一个答案有关数据的问题的输出:

  • 海洋生物名称列表形式是什么?
  • 海洋生物总共拥有多少鱼?
  • 这些鱼中有多少鱼属于海豚?

要查找海洋生物名称的列表形式,你使用了地图函数:地图(.name)。 要查找生物拥有总数多少块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块块

您将将这些过滤器合并为一个jq命令,以完成所有工作,您将创建一个新的JSON对象,将三个过滤器合并在一起,以创建一个新的数据结构,显示您想要的信息。

作为提醒,您的开始 JSON 文件符合以下内容:

1[label seaCreatures.json]
2[
3    { "name": "Sammy", "type": "shark", "clams": 5 },
4    { "name": "Bubbles", "type": "orca", "clams": 3 },
5    { "name": "Splish", "type": "dolphin", "clams": 2 },
6    { "name": "Splash", "type": "dolphin", "clams": 2 }
7]

您的转换 JSON 输出将产生如下:

 1[secondary_label Final Output]
 2{
 3  "creatures": [
 4    "Sammy",
 5    "Bubbles",
 6    "Splish",
 7    "Splash"
 8  ],
 9  "totalClams": 12,
10  "totalDolphinClams": 4
11}

以下是用空输入值的完整jq命令的语法示范:

1jq '{ creatures: [], totalClams: 0, totalDolphinClams: 0 }' seaCreatures.json

使用此过滤器,您可以创建包含三个属性的 JSON 对象:

1[secondary_label Output]
2{
3  "creatures": [],
4  "totalClams": 0,
5  "totalDolphinClams": 0
6}

这开始看起来像最终的输出,但输入值不是正确的,因为它们没有从您的seaCreatures.json文件中提取。

用您在每个前一步中创建的过滤器替换硬代码属性值:

1jq '{ creatures: map(.name), totalClams: map(.clams) | add, totalDolphinClams: map(select(.type == "dolphin").clams) | add }' seaCreatures.json

上面的过滤器告诉jq创建包含的 JSON 对象:

  • 包含每个生物的名称值的生物属性。 包含每个生物的Clams值的总和的TotalClams属性。

运行命令,该过滤器的输出应该是:

 1[secondary_label Output]
 2{
 3  "creatures": [
 4    "Sammy",
 5    "Bubbles",
 6    "Splish",
 7    "Splash"
 8  ],
 9  "totalClams": 12,
10  "totalDolphinClams": 4
11}

您现在有一个单个 JSON 对象,为所有三个问题提供相关数据. 如果数据集发生变化,您输入的 jq 过滤器将允许您随时重新应用转换。

结论

在使用 JSON 输入时,jq 可以帮助您执行各种不同的数据转换,这对于文本操纵工具(如 sed)来说是困难的。 在本教程中,您使用选择函数过滤数据,用地图转换数组元素,用添加过滤器总结数组,并学习如何将转换合并为一个新的数据结构。

若您经常使用非 JSON 命令输出,您可以浏览我们的指南 sed, awkgrep以获取有关文本处理技术的信息。

Published At
Categories with 技术
comments powered by Disqus