學習軌道
/
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,全部免費。