轨道
/
jq
jq
/
练习
/
购物清单
购物清单

购物清单

学习练习

简介

基础

我们假设你已经熟悉 JSON 语法和数据类型。

jq 的工作方式是让传入的 JSON 数据经过一个_单一表达式_(写作_过滤器管道_),从而得到你想要的转换后的数据。

Note

jq语言由一个jq_程序_实现。这个程序提供了一些便捷的命令行选项,用来控制输入如何被读取,以及输出如何被呈现。

在下面的示例中,你会遇到:

  • -n或--null-input

    通常jq程序会拿到一个要读取的文件,或者你把数据发送到它的输入。--null-input选项让你可以在没有任何输入的情况下生成 JSON 数据。

  • -c或--compact-output

    默认情况下,jq会把输出打印得漂漂亮亮。当数据被整齐地格式化之后,人查看起来会极其方便。但机器并不需要这样:--compact-output选项会去掉格式化用的空白,把生成的 JSON 体积降到最小。

  • -f filename或--from-file filename

    从filename读取jq程序,而不是在命令行里直接给出它。sed和awk都用-f选项来达到同样的目的。你会在练习的测试脚本里看到它的用法。

本课的其余部分将聚焦于jq_语言_本身。

过滤器与管道

过滤器也叫表达式。

一个_过滤器_接收输入并产生输出。就像你在 Unix shell 里工作的方式,你可以用管道|把多个_过滤器_连接起来,让一个_过滤器_的输出接到另一个_过滤器_的输入上。

恒等过滤器:.

这是最简单的_过滤器_。它只是把输入原样传给输出。

$ echo '[1, 2, 3]' | jq '.'
[
  1,
  2,
  3
]

数组

下面快速介绍一下如何处理数组。我们稍后会更详细地讲这个主题。

_数组_的元素用方括号访问,下标从 0 开始。

$ echo '[10, 20, 30]' | jq '.[1]'
20

一个_过滤器_可以把表达式包在[和]里来构建一个_数组_。

  • 用一组已知的元素:

    jq -n '[1, 2, 3]'
    
  • 收集一串元素:例如,range是一个函数,会输出一串数字

    $ jq -n 'range(10; 70; 15)'
    10
    25
    40
    55
    

    用[]把表达式的结果收集成一个数组

    $ jq -c -n '[range(10; 70; 15)]'
    [10,25,40,55]
    

逗号是一个运算符

_逗号_不只是分隔_数组_元素的语法。逗号是一个运算符,用来连接数据流。

例如,[1, 2, 3]是一个_过滤器_,它用_数组_构造器[]把1、2和3这三个表达式连接后的结果收集起来。

你注意到上面range(10; 70; 15)里的分号了吗?因为_逗号_在jq语言里有特定的用途,所以接收多个实参的函数用分号来分隔实参。

对象

快速介绍一下对象。

和许多编程语言类似,用点号来访问_对象_的属性

$ echo '{"foo": {"bar": "qux"}}' | jq '.foo.bar'
"qux"
Note

方括号也可以用于_对象_,但这时字符串字面量需要加上引号。这是处理包含空格的键的一种方法。

$ echo '{"foo bar": "qux"}' | jq '.["foo bar"]'
"qux"

你可以用{}和key: value键值对来构造一个_对象_。对于“简单”字符串的_键_,不需要加引号。

jq -n '{question: (6 * 9), answer: 42}'

输出

{
  "question": 54,
  "answer": 42
}

如果要把_键_当作_表达式_来处理,就必须把它包在圆括号里(下面这段的输出也和上面相同)。

echo '[{"key":"question", "value":54}, {"key":"answer", "value":42}]' \
| jq '{(.[0].key): .[0].value, (.[1].key): .[1].value}'

管道

例如,假设file.json的内容是

{
  "key1": "value1",
  "key2": [5, 15, 25]
}

我们来计算一下 key2 这个_数组_的长度:

$ jq '.key2 | length' file.json
3

我们把.key2表达式的输出_通过管道_传给length作为输入,它不出所料地输出了_数组_中元素的个数。

过滤器可以忽略它的输入

在这个例子里,输入的 JSON 数据被忽略,对输出没有任何影响:

$ echo '{"answer": 42}' | jq '6 * 9'
54

过滤器可以输出数据流

一个_过滤器_可以输出多个值。例如,.[]这个_过滤器_会把_数组_的每个元素作为一个单独的值输出:

$ jq -n -c '[1, 2, 3]'
[1,2,3]

$ jq -n -c '[1, 2, 3] | .[]'
1
2
3

把这样一个_过滤器_用管道接到另一个_过滤器_上,就会**对每个值**执行第二个_过滤器_:

$ jq -n -c '[1, 2, 3] | .[] | . * 2'
2
4
6

这有点像隐式迭代。一旦你理解了这个技巧,就会发现,非常强大的jq_过滤器_可以写得非常简洁。

圆括号

圆括号用来把子表达式组合在一起,以强制运算顺序,就像其他语言里一样。在jq里,需要用到它们的地方有时会显得有点出人意料。

例如,假设我们想构造一个含 2 个元素的_数组_:9 的平方根,以及 e 的 1 次幂。这两个单独的表达式是9 | sqrt和1 | exp。我们期望输出的是_数组_[3, 2.7...]

$ jq -n '[ 9|sqrt, 1|exp ]'
[
  20.085536923187668,
  2.718281828459045
]

为什么没有得到我们期望的结果?jq是这样解释的:

[ ((9|sqrt), 1) | exp ]

jq构建了一个含两个元素(3和1)的数据流,它们各自被传给了exp。

我们需要确保exp只接收一个数字作为输入。换句话说,我们要强制管道先于逗号求值。

$ jq -n '[ 9|sqrt, (1|exp) ]'
[
  3,
  2.718281828459045
]

函数与运算符

这里不深入展开(函数会是另一个练习的主题),下面是一些有用的内置函数:

  • length

    给定一个数组作为输入,输出数组中元素的个数。

    $ jq -n '[10, 20, 30, 40] | length'
    4
    
  • +

    这个运算符会根据操作数的类型做不同的事:相加数字,拼接字符串,追加数组,合并对象。

    $ jq -c -n '
        3 + 4,
        "foo" + "bar",
        ["a", "b"] + ["c"],
        {"m": 10} + {"n": 20}
    '
    7
    "foobar"
    ["a","b","c"]
    {"m":10,"n":20}
    

    add是一个函数,它接收一个数组,按照+的规则把所有元素加在一起后返回一个结果。[1, 2, 3] | add输出6。

  • map

    给定一个数组作为输入、一个过滤器作为实参,输出一个把该过滤器应用到每个元素上的数组

    $ jq -c -n '[10, 20, 30, 40] | map(. / 5)'
    [2,4,6,8]
    
  • select

    给定_某些_输入和一个过滤器作为实参:

    • 如果对输入应用该过滤器后得到一个 true 值,就原样输出输入
    • 否则,就_什么也不输出_(不是null值,而是真正的没有输出)

    例如,给定一些数字,选出能被 3 整除的那些

    $ jq -n 'range(10) | select(. % 3 == 0)'
    0
    3
    6
    9
    

    回想一下,range输出的是一个数字_数据流_。select会对每个数字调用一次。只有“通过”这个表达式的数字才会被输出。

    你经常需要从数组中选出想要的元素。有几种方法可以做到。

    给定输入["Anne", "Bob", "Cathy", "Dave"],选出长度为 4 的名字。

    • 把map和select一起用

      map(select(length == 4))
      
    • 把数组拆成一个个元素,对那个数据流做select,再把结果收集起来

      [ .[] | select(length == 4) ]
      

注释

注释以#字符开头,一直延续到行尾。

说明

你突然很想吃薄煎饼。 作为一个喜欢钻研技术的现代人,你用 JSON 格式写了一份薄煎饼食材的购物清单。 清单的结构如下:

{
  "name": "name of shopping list",
  "ingredients": [ ...list of ingredients... ],
  "optional ingredients": [ ...list of ingredients... ]
}

每一种食材都用一个 JSON 对象表示,例如:

{
  "item": "flour",
  "amount": {
    "quantity": 2,
    "unit": "cup"
  }
}

食材还有一个名为 "substitute" 的可选属性,它的值是一个字符串,表示可以用来替代它的另一种食材。

我们来看看如何用jq查看这份购物清单。

1. 提取购物清单的名称

写一个表达式,输出购物清单的 "name" 元素。

2. 数一数需要多少食材

只统计“必需”的食材。不要算上可选食材。

3. 需要多少糖?

写一个表达式,输出糖的用量。 只需要数字部分。

4. 映射可以被替换的食材

有些食材是可以替换的(如果没有食材 X,可以用 Y 代替)。 输出一个 JSON 对象,把推荐的食材映射到它的替代品。 映射中要包含可选食材。

通过 GitHub 编辑 链接将在新窗口或新标签页中打开
jq Exercism

准备好开始 购物清单 了吗?

注册 Exercism,借助 12 个概念75 个练习 和真人导师指导,学习并掌握 jq,全部免费。