隨著 JSON 格式作為資料交換格式大行其道,使用工具對 JSON 資料進行操作也逐漸成為家常便飯,各種工具中, jq 可說是一套十分易於操作的 JSON 操作工具。

本文將透過實際資料學習如何使用 jq 。

本文環境

  • macOS 10.16
  • homebrew 3.0.1
  • jq 1.6
$ brew install jq

jq 基本操作

列印 JSON 資料

很多時候 JSON 資料都沒有經過排版,因此難以閱讀,這時可以簡單以 pipe | 將 JSON 字串交給 jq 進行排版:

$ echo '{"b":2,"a":1,"c":3}' | jq

上述指令結果如下,可看到排版後的 JSON 字串變得相對友善:

{
  "b": 2,
  "a": 1,
  "c": 3
}

除了排版之外, jq 能夠加上 -S 參數為欄位進行排序,例如上述的 b, a, c 可以排成 a, b, c ,讓 JSON 資料更加好閱讀:

$ echo '{"b":2,"a":1,"c":3}' | jq -S
{
  "a": 1,
  "b": 2,
  "c": 3
}

取得欄位值

在 jq 的語法中,一定得認識的符號為 .

該符號被稱為 identity 代表輸入的資料,例如以下範例中的 . 就代表 {“b”:2,”a”:1,”c”:3}

$ echo '{"b":2,"a":1,"c":3}'|jq '.'

p.s. 輸入的資料可以不是 JSON 資料,但基本上不具任何意義

如果要取得 JSON 資料中特定欄位的值,則是以 .(欄位名) 就可取得,例如以下取得 a 欄位的值:

$ echo '{"b":2,"a":1,"c":3}' | jq '.a'
1

當然,有時 JSON 資料不見得是單 1 個 object ,而是 JSON 資料組成的陣列(array) ,此時可以用 [(編號)] 取得該陣列中的元素,例如以下取得 JSON 資料陣列中的第 2 個元素(編號從 0 開始,因此第 2 個元素的編號為 1):

$ echo '[{"b":2},{"a":1},{"c":3}]' | jq '.[1]'
{
  "a": 1
}

取得特定元素之後,同樣可以進一步取得該元素內的特定欄位值,例如進一步取得欄位 a 的值:

$ echo '[{"b":2},{"a":1},{"c":3}]' | jq '.[1].a'
1

值得注意的是,上述範例中的 '.[1].a' 第 2 個 . 並不是指 [{"b":2},{"a":1},{"c":3}] 而是指 {"a":1} ,因為 .[1].a 其實是 .[1]|.a 的簡寫,代表資料 [{"b":2},{"a":1},{"c":3}] 經過 .[1] 取得第 2 個元素之後,再將之傳遞給 .a 取得 a 欄位的值,因此第 2 個 . 代表的是 {"a":1}

如果是想取得 JSON 資料陣列中所有 a 欄位的值,則直接省略 [] 中的編號即可,例如:

$ echo '[{"b":2},{"a":1},{"c":3}]' | jq '.[].a'
null
1
null

上述結果可以看到 jq 也完美處理消失欄位的問題,並將其值以 null 進行表示。

範例資料

學會基本操作之後,就能用更複雜的資料進行進階的操作。

以下資料取自 政府資料開放平臺 - 新北市綠色餐廳 ,其原始格式為 CSV ,本文已轉為 JSON 格式,後續範例皆以該資料作為範例。

請將以下內容複製並存為檔案 test.json

[
  {
    "district": "八里區",
    "name": "芭達桑原住民主題餐廳",
    "address": "新北市八里區觀海大道111號"
  },
  {
    "district": "三重區",
    "name": "古拉爵 義式屋-家樂福重新店",
    "address": "新北市三重區重新路5段654號B1"
  },
  {
    "district": "三峽區",
    "name": "茲啦啦小吃店",
    "address": "新北市三峽區中華路74-5號"
  },
  {
    "district": "中和區",
    "name": "鹿兒島燒肉專賣店-中和中山店",
    "address": "新北市中和區中山路二段28號"
  },
  {
    "district": "中和區",
    "name": "布拉德施維根廚房 Brother Su Vegan Kitchen",
    "address": "新北市中和區南華路29號"
  },
  {
    "district": "永和區",
    "name": "阿妃餃子館",
    "address": "新北市永和區中正路470號"
  },
  {
    "district": "永和區",
    "name": "猛港海鮮餐廳",
    "address": "新北市永和區永和路一段62號"
  },
  {
    "district": "永和區",
    "name": "哩賀涼麵",
    "address": "新北市永和區秀朗路一段169號"
  },
  {
    "district": "汐止區",
    "name": "涮乃葉-汐止遠雄店",
    "address": "新北市汐止區新台五路一段99號B1"
  },
  {
    "district": "坪林區",
    "name": "田媽媽快樂農家米食餐飲坊",
    "address": "新北市坪林區北宜路八段141號"
  },
  {
    "district": "板橋區",
    "name": "涮乃葉-板橋遠百中山店",
    "address": "新北市板橋區中山路1段152號10樓"
  },
  {
    "district": "板橋區",
    "name": "鍋賣局(板橋新板店)",
    "address": "新北市板橋區中山路一段130號"
  },
  {
    "district": "板橋區",
    "name": "神灯咖啡",
    "address": "新北市板橋區中山路一段160-1號1樓"
  },
  {
    "district": "板橋區",
    "name": "彭園婚宴會館(新板店)",
    "address": "新北市板橋區中山路一段161號"
  },
  {
    "district": "板橋區",
    "name": "鹿兒島燒肉專賣店-板橋新板店",
    "address": "新北市板橋區中山路一段164-1號"
  },
  {
    "district": "板橋區",
    "name": "燒肉眾精緻炭火燒肉(二代目)-板橋文化店",
    "address": "新北市板橋區文化路一段325號"
  },
  {
    "district": "板橋區",
    "name": "粩泰泰泰式料理",
    "address": "新北市板橋區四川路2段47巷4弄6號"
  },
  {
    "district": "板橋區",
    "name": "八方悅鍋物-板橋民族店",
    "address": "新北市板橋區民族路73號"
  },
  {
    "district": "板橋區",
    "name": "88thai 發發泰-泰式飯行家",
    "address": "新北市板橋區莒光路45號1樓"
  },
  {
    "district": "板橋區",
    "name": "古拉爵 義式屋-板橋愛買店",
    "address": "新北市板橋區貴興路101號3樓"
  },
  {
    "district": "板橋區",
    "name": "藍屋-板橋大遠百新站店",
    "address": "新北市板橋區新站路28號9樓"
  },
  {
    "district": "板橋區",
    "name": "古拉爵 義式屋-板橋大遠百新站店",
    "address": "新北市板橋區新站路28號9樓"
  },
  {
    "district": "板橋區",
    "name": "板橋凱撒大飯店(大廳酒吧)",
    "address": "新北市板橋區縣民大道二段8號"
  },
  {
    "district": "板橋區",
    "name": "板橋凱撒大飯店(卡拉拉餐廳)",
    "address": "新北市板橋區縣民大道二段8號"
  },
  {
    "district": "板橋區",
    "name": "板橋凱撒大飯店(朋派自助餐)",
    "address": "新北市板橋區縣民大道二段8號"
  },
  {
    "district": "板橋區",
    "name": "板橋凱撒大飯店(家宴中餐廳)",
    "address": "新北市板橋區縣民大道二段8號"
  },
  {
    "district": "板橋區",
    "name": "燒肉哦爺-日韓燒肉吃到飽(板橋府中店)",
    "address": "新北市板橋區館前東路48號2.3樓"
  },
  {
    "district": "林口區",
    "name": "古拉爵 義式屋-林口三井店",
    "address": "新北市林口區文化三路1段356號2樓"
  },
  {
    "district": "林口區",
    "name": "心伝的家",
    "address": "新北市林口區文化三路一段394巷6號1樓"
  },
  {
    "district": "金山區",
    "name": "田中芳園養生食坊",
    "address": "新北市金山區清水路53號"
  },
  {
    "district": "烏來區",
    "name": "雲仙大飯店",
    "address": "新北市烏來區烏來里瀑布路1-1號"
  },
  {
    "district": "淡水區",
    "name": "將捷金鬱金香酒店",
    "address": "新北市淡水區中正路一段2-1號"
  },
  {
    "district": "新店區",
    "name": "古拉爵 義式屋-新店家樂福店",
    "address": "新北市新店區中興路3段1號7樓"
  },
  {
    "district": "新店區",
    "name": "SKYLARK加州風洋食館-新店家樂福店",
    "address": "新北市新店區中興路3段1號7樓"
  },
  {
    "district": "新店區",
    "name": "原粹蔬食作",
    "address": "新北市新店區北新路三段206巷1弄7號一樓"
  },
  {
    "district": "新莊區",
    "name": "鹿兒島燒肉專賣店-新莊中華店",
    "address": "新北市新莊區中華路二段65號1樓"
  },
  {
    "district": "瑞芳區",
    "name": "雲山水小築民宿",
    "address": "新北市瑞芳區金瓜石石山里山尖路72-1號"
  },
  {
    "district": "雙溪區",
    "name": "雙溪平林休閒農場",
    "address": "新北市雙溪區平林里外平林35號"
  },
  {
    "district": "蘆洲區",
    "name": "魚缸珈琲館",
    "address": "新北市蘆洲區三民路78號"
  },
  {
    "district": "鶯歌區",
    "name": "蕃薯藤有限公司鶯歌分公司",
    "address": "新北市鶯歌區尖山路5號"
  },
  {
    "district": "鶯歌區",
    "name": "台灣緹娜餐飲有限公司鶯歌分公司",
    "address": "新北市鶯歌區尖山路5號"
  }
]

JSON 轉 JSON lines

範例資料 test.json 是 1 個很大的 JSON 資料陣列,如果想將其轉換成 JSON lines 的檔案(每 1 行都是 1 筆 JSON 資料),可以用以下指令搭配 -c 參數,就能夠將 JSON 資料陣列中的每 1 筆資料轉為 JSON lines:

$ jq -c '.[]' test.json > test.jsonl

p.s. -c—compact-output ,該參數會將單筆 JSON 資料縮為 1 行顯示

上述指令的執行結果如下,可以看到 test.jsonl 檔案中每 1 行都是 1 筆 JSON 資料:

{"district":"八里區","name":"芭達桑原住民主題餐廳","address":"新北市八里區觀海大道111號"}
{"district":"三重區","name":"古拉爵 義式屋-家樂福重新店","address":"新北市三重區重新路5段654號B1"}
{"district":"三峽區","name":"茲啦啦小吃店","address":"新北市三峽區中華路74-5號"}
{"district":"中和區","name":"鹿兒島燒肉專賣店-中和中山店","address":"新北市中和區中山路二段28號"}

JSON lines 轉 JSON

前述範例將 JSON 轉成 JSON lines 。

同樣地,也能透過 jq 將 JSON lines 轉為 JSON, 其方法是只要利用 -s 參數即可:

$ jq -s '.' test.jsonl > test.json

-s 的功用在於將所有讀取到的 JSON 資料放到一個陣列內。

讀取特定筆數

第 n 筆到第 m 筆

基本操作中提到 jq 能夠以編號取得 JSON 資料陣列中的元素。不過 jq 也支援類似 Python 切片(slicing)的操作,例如以下指令取得第 4 筆至第 5 筆:

$ jq '.[3:5]' test.json
[
  {
    "district": "中和區",
    "name": "鹿兒島燒肉專賣店-中和中山店",
    "address": "新北市中和區中山路二段28號"
  },
  {
    "district": "中和區",
    "name": "布拉德施維根廚房 Brother Su Vegan Kitchen",
    "address": "新北市中和區南華路29號"
  }
]

上述指令中的 [3:5] 可以理解為 [開始編號:結束編號]

由於陣列元素編號從 0 開始,因此第 4 筆的編號為 3 ,第 5 的編號為 4 ,然而切片的第 2 個數字代表結束編號,該編號並不會被放入結果之中,因此切片結果要包含第 4 第 5 筆的話,就得將第 2 個數字設定為 5 ,代表到第 6 筆就結束。

最後 1 筆

同樣地, jq 也支援與 Python 類似的負數編號用法,代表從最後數來第 n 筆,因此取得最後 1 筆資料的指令為:

$ jq '.[-1:]' test.json
[
  {
    "district": "鶯歌區",
    "name": "台灣緹娜餐飲有限公司鶯歌分公司",
    "address": "新北市鶯歌區尖山路5號"
  }
]

刪除特定欄位

事實上, jq 也提供一些函數(functions)讓使用者能夠進一步按照需求修改 JSON 資料,例如以 del() 刪除 JSON 資料中的特定欄位,以下範例刪除每 1 筆 JSON 資料中的 address 與 district 欄位:

$ jq -c '.[] | del(.address,.district)' test.json

上述指令執行結果如下,可以看到每筆 JSON 資料最後只剩下欄位 name:

{"name":"芭達桑原住民主題餐廳"}
{"name":"古拉爵 義式屋-家樂福重新店"}
{"name":"茲啦啦小吃店"}
{"name":"鹿兒島燒肉專賣店-中和中山店"}

排序資料

除了 del() 函數外, sort_by() 也是相當便捷的工具,讓人能夠按照特定欄位排序資料,例如以下指令按照 district 欄位進行排序,如此一來就能夠清楚地將同一區的資料排在一起:

$ jq 'sort_by(.district)' test.json

上述指令結果如下:

[
  {
    "district": "三峽區",
    "name": "茲啦啦小吃店",
    "address": "新北市三峽區中華路74-5號"
  },
  {
    "district": "三重區",
    "name": "古拉爵 義式屋-家樂福重新店",
    "address": "新北市三重區重新路5段654號B1"
  },
  {
    "district": "中和區",
    "name": "鹿兒島燒肉專賣店-中和中山店",
    "address": "新北市中和區中山路二段28號"
  },
  {
    "district": "中和區",
    "name": "布拉德施維根廚房 Brother Su Vegan Kitchen",
    "address": "新北市中和區南華路29號"
  },
  ...(略)...
]

如果想進一步取得排序後的 district 欄位值,可以利用 pipe | 結合 .[].district 將資料中的 district 欄位取出:

$ jq 'sort_by(.district) | .[].district' test.json

上述指令執行結果如下:

"三峽區"
"三重區"
"中和區"
"中和區"
"八里區"
"坪林區"
"新店區"
"新店區"
"新店區"
"新莊區"
"板橋區"
"板橋區"
"板橋區"
"板橋區"
"板橋區"
"板橋區"

去除重複資料

如果 JSON 資料陣列中存在重複的資料,例如範例資料中的 district 欄位存在重複的地區,若要去除重複的資料可以使用 unique_by() 函數,例如:

$ jq 'unique_by(.district)' test.json

上述指令結果如下,可以看到使用 unique_by(.district) 後每個地區都只有 1 筆資料被保留,重複的地區都已被去除:

[
  {
    "district": "三峽區",
    "name": "茲啦啦小吃店",
    "address": "新北市三峽區中華路74-5號"
  },
  {
    "district": "三重區",
    "name": "古拉爵 義式屋-家樂福重新店",
    "address": "新北市三重區重新路5段654號B1"
  },
  {
    "district": "中和區",
    "name": "鹿兒島燒肉專賣店-中和中山店",
    "address": "新北市中和區中山路二段28號"
  },
  {
    "district": "八里區",
    "name": "芭達桑原住民主題餐廳",
    "address": "新北市八里區觀海大道111號"
  },
  ...(略)...
]

篩選欄位特定值

如果要列出含有特定值的欄位,則可以使用 select() 函數,例如以下範例僅列出鶯歌區的餐廳:

$ jq '.[] | select(.district=="鶯歌區")' test.json

上述指令結果如下:

{
  "district": "鶯歌區",
  "name": "蕃薯藤有限公司鶯歌分公司",
  "address": "新北市鶯歌區尖山路5號"
}
{
  "district": "鶯歌區",
  "name": "台灣緹娜餐飲有限公司鶯歌分公司",
  "address": "新北市鶯歌區尖山路5號"
}

.[] | select(.district=="鶯歌區") 可以理解為將所有文件 .[] 經由 pipe | 交由 select() 函數處理,選取其中 .district 欄位為 鶯歌區 的資料。

前述指令結果可以發現 JSON 資料是 1 筆接著 1 筆被列印,如果希望其結果維持 JSON 陣列顯示的話,只要在 .[] | select(.district=="鶯歌區")最外圍加上 [] 括住即可:

$ jq '[.[] | select(.district=="鶯歌區")]' test.json
[
  {
    "district": "鶯歌區",
    "name": "蕃薯藤有限公司鶯歌分公司",
    "address": "新北市鶯歌區尖山路5號"
  },
  {
    "district": "鶯歌區",
    "name": "台灣緹娜餐飲有限公司鶯歌分公司",
    "address": "新北市鶯歌區尖山路5號"
  }
]

總結

實際上 jq 遠比想像方便與強大,本文所羅列之各種功能也僅僅只是冰山一角,礙於篇幅無法一一詳述各種操作與說明,建議有空可以閱讀 jq 官方文件 學習更多。

以上! Happy Coding!

References

https://stedolan.github.io/jq/