如何使用jq转换JSON数据

简介

在处理大型JSON文件时,很难找到和操作所需的信息。您可以复制和粘贴所有相关片段手动计算总数,但这是一个耗时的过程,也容易出错。另一个选择是使用通用的工具来查找和操作信息。所有现代Linux系统都预装了三个已建立的文本处理工具,即sed、awk和grep。虽然在处理松散结构化数据时这些命令很有用,但在处理类似JSON这样的可机器读取的数据格式时,还有其他选择。

jq是一个命令行的JSON处理工具,对于处理机器可读格式的数据是一个很好的解决方案,尤其在Shell脚本中特别有用。使用jq可以帮助你处理数据。例如,当你运行一个针对JSON API的curl调用时,jq可以从服务器的响应中提取特定的信息。作为数据工程师,你也可以将jq纳入到数据摄取过程中。如果你管理一个Kubernetes集群,你可以使用kubectl的JSON输出作为jq的输入源,从中提取特定部署可用副本的数量。

在本文中,您将使用jq来转换一个关于海洋动物的示例JSON文件。您将使用过滤器应用数据转换,并将转换后的数据组合成一个新的数据结构。通过本教程的结束,您将能够使用jq脚本来回答有关您已操作的数据的问题。

先决条件

完成本教程,您需要以下物品:

  • jq, a JSON parsing and transformation tool. It is available from the repositories for all major Linux distributions. If you are using Ubuntu, run sudo apt install jq to install it.
  • An understanding of JSON syntax, which you can refresh in An Introduction to JSON.

第一步—执行您的第一个jq命令。

在这一步中,您将设置样本输入文件,并通过运行jq命令来测试设置,以生成样本文件数据的输出。jq可以从文件或管道获取输入。您将使用前者。

首先,您需要生成示例文件。使用您偏好的编辑器(本教程使用nano)创建并打开一个名为seaCreatures.json的新文件。

  1. nano seaCreatures.json

 

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

海洋生物.json
[
    { "name": "Sammy", "type": "shark", "clams": 5 },
    { "name": "Bubbles", "type": "orca", "clams": 3 },
    { "name": "Splish", "type": "dolphin", "clams": 2 },
    { "name": "Splash", "type": "dolphin", "clams": 2 }
]

在接下来的教程中,你将使用这些数据进行工作。在教程结束时,你将写出一行jq命令来回答有关这些数据的以下问题。

  • What are the names of the sea creatures in list form?
  • How many clams do the creatures own in total?
  • How many of those clams are owned by dolphins?

保存并关闭文件。

除了输入文件外,您还需要一个描述您希望执行的确切转换的过滤器。点(.)过滤器,也称为身份运算符,将JSON输入不变地传递为输出。

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

使用以下命令将身份操作符应用于JSON文件:

  1. jq ‘.’ seaCreatures.json

 

当使用jq处理文件时,您总是先传递一个过滤器,然后是输入文件。由于过滤器可能包含对您的shell具有特殊含义的空格和其他字符,所以最好将过滤器用单引号括起来。这样做会告诉您的shell,过滤器是一个命令参数。请放心,运行jq不会修改您的原始文件。

您将收到以下的输出:

Output

[ { “name”: “Sammy”, “type”: “shark”, “clams”: 5 }, { “name”: “Bubbles”, “type”: “orca”, “clams”: 3 }, { “name”: “Splish”, “type”: “dolphin”, “clams”: 2 }, { “name”: “Splash”, “type”: “dolphin”, “clams”: 2 } ]

默认情况下,jq会对其输出进行漂亮打印。它会自动添加缩进,在每个值之后添加新行,并在可能时给输出添加颜色。着色可以提高可读性,这对许多开发人员在检查其他工具生成的JSON数据时非常有帮助。例如,当向JSON API发送curl请求时,您可能想将JSON响应导入jq ‘.’以进行漂亮打印。

您现在已经安装并运行了jq。设置好您的输入文件后,您将使用几个不同的过滤器来操作数据,以计算出所有三个属性的值:creatures、totalClams和totalDolphinClams。在下一步中,您将找到creatures值的相关信息。

步骤2 — 获取生物的价值

在这一步骤中,您将生成一个包含所有海洋生物的列表,利用生物的值来找到它们的名称。在完成这一步骤后,您将生成以下名称的列表:

Output

[ “Sammy”, “Bubbles”, “Splish”, “Splash” ],

生成此列表需要提取生物名称,然后将其合并到一个数组中。

你需要改进你的过滤器,以便获取所有生物的名称并丢弃其他所有内容。由于你在处理数组,你需要告诉 jq 你希望操作的是该数组的值,而不是数组本身。数组值迭代器写作 .[],可以实现这个目的。

使用修改后的过滤器运行jq。

  1. jq ‘.[]’ seaCreatures.json

 

现在每个数组值都单独输出。

Output

{ “name”: “Sammy”, “type”: “shark”, “clams”: 5 } { “name”: “Bubbles”, “type”: “orca”, “clams”: 3 } { “name”: “Splish”, “type”: “dolphin”, “clams”: 2 } { “name”: “Splash”, “type”: “dolphin”, “clams”: 2 }

不必输出每个数组项的全部内容,而是只需输出名字属性的值并丢弃其它内容。管道操作符“|”可以让你对每个输出应用一个过滤器。如果你在命令行上使用了find | xargs命令来对每个搜索结果应用一个命令,那么这种模式会感到很熟悉。

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

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

 

你会注意到输出中其他的属性已经消失了。

Output

“Sammy” “Bubbles” “Splish” “Splash”

默认情况下,jq输出有效的JSON,因此字符串将出现在双引号(“”)中。如果你需要一个没有双引号的字符串,可以加上-r标志来启用原始输出。

  1. jq -r ‘.[] | .name’ seaCreatures.json

 

引号消失了。

Output

Sammy Bubbles Splish Splash

你现在知道如何从JSON输入中提取特定信息。你将使用这种技术来在下一步中找到其他特定信息,然后在最后一步中生成”creatures”值。

第三步—使用map和add计算totalClams的值

在这一步中,你将找到这些生物拥有多少蛤蜊的总信息。你可以通过聚合一些数据来计算答案。一旦你熟悉jq,这将比手动计算更快,而且更不容易出现人为错误。在这一步骤结束时,预期值为12。

在步骤2中,您从项目列表中提取了特定的信息。您可以重复使用这种技术来提取蛤蜊属性的值。调整这个新属性的过滤器,并运行命令:

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

 

会输出蛤蜊属性的各个值。

Output

5 3 2 2

为了得到每个值的总和,你需要使用添加过滤器。添加过滤器适用于数组。然而,你目前输出的是数组值,所以你必须先将它们包装在一个数组中。

将现有的筛选器用[]包围起来,如下所示:

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

 

这些值将会以列表的形式呈现出来。

Output

[ 5, 3, 2, 2 ]

在应用添加过滤器之前,你可以通过使用map函数来提高命令的可读性,这也使得命令更易于维护。通过一次map调用,你可以在循环数组的过程中对每个项应用过滤器,然后将结果包装在一个数组中。给定一个项的数组,map函数将其参数作为过滤器应用到每个项上。例如,如果你将过滤器map(.name)应用到[{“name”: “Sammy”}, {“name”: “Bubbles”}],那么生成的JSON对象将是[“Sammy”, “Bubbles”]。

将过滤器重写为生成一个数组,使用映射函数代替,然后运行它。

  1. jq ‘map(.clams)’ seaCreatures.json

 

你将会得到和之前一样的输出。

Output

[ 5, 3, 2, 2 ]

既然你现在拥有一个数组,你可以将它传递到添加过滤器中。

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

 

您将收到数组的总和。

Output

12

通过这个筛选器,您已经计算出了蛤蜊的总数量,您将在以后使用这个计算结果生成totalClams的值。您已经编写了两个问题的过滤器。您还需要创建一个过滤器,然后您就可以生成最终的输出。

第四步 – 使用add过滤器计算总的海豚蛤蜊价值

现在你知道了这些生物拥有多少贝壳,你可以确定海豚拥有其中多少贝壳。你可以通过将满足特定条件的数组元素的值相加来得出答案。在这一步骤结束时,预期的值是4,这是海豚拥有的贝壳总数。在最后一步中,所得到的值将被用于totalDolphinClams属性。

与其像你在步骤3中那样添加所有蛤蜊值,你只会计算由具有“海豚”类型的生物所持有的蛤蜊。你将使用select函数来选择特定条件:select(condition)。所有满足条件的输入将被传递下去,而其他所有输入将被丢弃。例如,如果你的JSON输入是“海豚”,你的过滤器是select(. == “海豚”),输出将是“海豚”。对于输入“Sammy”,相同的过滤器将不产生任何输出。

要对数组中的每个值应用选择,可以将其与映射函数配对使用。通过这样做,不满足条件的数组值将被丢弃。

在你的情况下,你只想保留类型值等于”海豚”的数组值。结果过滤器为:

  1. jq ‘map(select(.type == “dolphin”))’ seaCreatures.json

 

你的过滤器将无法匹配到鲨鱼山米和虎鲸巴布尔,但它能匹配到那两只海豚。

Output

[ { “name”: “Splish”, “type”: “dolphin”, “clams”: 2 }, { “name”: “Splash”, “type”: “dolphin”, “clams”: 2 } ]

这个输出包含每个生物的蛤蜊数量,以及一些不相关的信息。为了仅保留蛤蜊的值,您可以将字段的名称追加到映射参数的末尾。

  1. jq ‘map(select(.type == “dolphin”).clams)’ seaCreatures.json

 

地图函数接收一个数组作为输入,并将map的筛选条件(作为参数)应用于每个数组元素。结果是,select被调用了四次,每个生物都调用了一次。select函数将为两只海豚生成输出(因为它们符合条件),并省略其他的生物。

您的输出将是一个只包含两个匹配生物的贝壳值的数组。

Output

[ 2, 2 ]

将数组值输入到add函数中。

  1. jq ‘map(select(.type == “dolphin”).clams) | add seaCreatures.json

 

您的输出将返回“海豚”类型生物的蛤蜊价值之和。

Output

4

你已成功地将map和select结合起来访问一个数组,选择满足条件的数组项,对它们进行转换并汇总转换的结果。你可以使用这个策略来计算最终输出中的totalDolphinClams,在下一步中你将这样做。

步骤5 – 将数据转换为新的数据结构

在之前的步骤中,你编写了一些过滤器来提取和处理样本数据。现在,你可以将这些过滤器结合起来生成一个输出,以回答你对于数据的问题。

  • What are the names of the sea creatures in list form?
  • How many clams do the creatures own in total?
  • How many of those clams are owned by dolphins?

为了以列表形式找出海洋生物的名称,您使用了map函数:map(.name)。为了找出这些生物拥有多少贝壳,您将所有的贝壳值导入了add过滤器:map(.clams) | add。为了找出其中有多少贝壳是由海豚拥有的,您使用了select函数,并加上了.type == “dolphin”的条件:map(select(.type == “dolphin”).clams) | add。

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

提醒一下,你的初始JSON文件与以下内容相匹配:

海洋生物.json
[
    { "name": "Sammy", "type": "shark", "clams": 5 },
    { "name": "Bubbles", "type": "orca", "clams": 3 },
    { "name": "Splish", "type": "dolphin", "clams": 2 },
    { "name": "Splash", "type": "dolphin", "clams": 2 }
]

您转换的JSON输出会生成以下内容:

Final Output

{ “creatures”: [ “Sammy”, “Bubbles”, “Splish”, “Splash” ], “totalClams”: 12, “totalDolphinClams”: 4 }

以下是使用空输入值进行完整jq命令语法的演示:

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

 

使用此筛选器,您可以创建一个包含三个属性的JSON对象。

Output

{ “creatures”: [], “totalClams”: 0, “totalDolphinClams”: 0 }

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

将硬编码的属性值替换为您在每个之前步骤中创建的过滤器。

  1. jq ‘{ creatures: map(.name), totalClams: map(.clams) | add, totalDolphinClams: map(select(.type == “dolphin”).clams) | add }’ seaCreatures.json

 

以上的过滤器告诉 jq 创建一个包含以下内容的 JSON 对象:

  • A creatures attribute containing a list of every creature’s name value.
  • A totalClams attribute containing a sum of every creature’s clams value.
  • A totalDolphinClams attribute containing a sum of every creature’s clams value for which type equals “dolphin”.

运行该命令,该筛选器的输出应该是:

Output

{ “creatures”: [ “Sammy”, “Bubbles”, “Splish”, “Splash” ], “totalClams”: 12, “totalDolphinClams”: 4 }

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

结论

当使用JSON输入时,jq可以帮助您执行许多在sed等文本操作工具中很难实现的数据转换。在本教程中,您可以使用select函数筛选数据,使用map函数转换数组元素,使用add过滤器对数字数组进行求和,并学会如何将转换合并成新的数据结构。

要了解jq高级功能,请浏览jq参考文档。如果您经常使用非JSON命令输出,您可以查阅我们的sed、awk或grep指南,了解适用于任意格式的文本处理技巧的信息。

广告
将在 10 秒后关闭
bannerAds