Python 套件介紹 - JMESPath (功用與 jq 相似的 JSON 查詢語言)

Posted on  Jul 12, 2023  in  Python 模組/套件推薦  by  Amo Chen  ‐ 4 min read

JMESPath 是 1 款與 jq 功用類似的 Python 套件,可以讓 Python 開發者用與 jq 相似的語法查詢、重組 JSON 格式(需要用 json 模組轉成 Python 的原生資料型態)的資料,如果運用得當可以簡化程式碼,或者改善可讀性。

本文將介紹 JMESPath 相關的使用方法。

本文環境

  • Python 3
  • JMESPath
$ pip install jmespath

常見巢狀(nested)字典資料讀取方式

假設有以下巢狀的字典型態的變數 data ,該變數的特點在於儲存的資料巢狀結構很多層,共有 a > b > c 3 層:

data = {
    'a': {
        'b': {
            'c': 'foo',
        }
    }
}

在這種情況下,如果要讀取 c 的資料,我們通常會用以下形式,串連多個字典(dictionary)所提供的方法 get() ,一層一層往下取得 c 的值:

c = data.get('a', {}).get('b', {}).get('c')
if c is None:
   print('Not found')
else:
   print(c)

上述範例執行結果如下,可以看到 c 的值為: foo

foo

不過這種方式遇到更加複雜的資料時,就容易造成可讀性的問題。

比較優雅的做法是像 jq, lodash 提供類似查詢語言的做法,例如data.get('a.b.c') 就能夠拿到 c 的值,不過 Python 並沒有提供這項方便的功能。

所幸 JMESPath 已經為我們提供這項簡便的功能。

JMESPath

JMESPath 不僅僅支援 Python, 也支援 Go, PHP, Rust 等語言,所以學會 JMESPath 之後,也能夠在這些語言無痛使用相同的語法。

同樣以前述的巢狀資料為例:

data = {
    'a': {
        'b': {
            'c': 'foo',
        }
    }
}

使用 JMESPath 之後可以改為以下形式,其中 jmespath.compile('a.b.c') 與正規表示式 re 模組類似,會檢查語法 a.b.c 是否正確,並轉成 JMESPath 的表示式(expression)實例(instance),最後用該實例對資料 data 進行查詢:

import jmespath


expr = jmespath.compile('a.b.c')
c = expr.search(data)
if c is None:
   print('Not found')
else:
   print(c)

上述範例執行結果與前述範例相同,但看起來是否優雅多了?

支援 Python 的 index/slice 用法

對於 list 型態的資料, JMESPath 也支援與 Python 相同的 index 用法,例如以下資料:

data = {
    'a': {
        'b': {
            'c': [1, 2, 3, 4, 5, 6, 7, 8],
        }
    }
}

如果要取得 c 的最後 1 個值,可以用 a.b.c[-1] 進行表示:

import jmespath


expr = jmespath.compile('a.b.c[-1]')
c = expr.search(data)
if c is None:
   print('Not found')
else:
   print(c)

上述範例執行結果如下:

8

除了 index 的用法之外,也支援 slice 的用法,例如 a.b.c[0:2] 代表取出 c list 以索引值 0 開始,到索引值 1 為止(到索引值 2 結束,不包含索引值 2)的資料:

import jmespath


expr = jmespath.compile('a.b.c[0:2]')
c = expr.search(data)
if c is None:
   print('Not found')
else:
   print(c)

上述範例執行結果如下:

[1, 2]

Python list 倒序的用法也完全沒問題,也就是以下範例 [::-1] 的部分:

import jmespath


expr = jmespath.compile('a.b.c[::-1]')
c = expr.search(data)
if c is None:
   print('Not found')
else:
   print(c)

上述範例執行結果如下,可以看到 c 的資料被倒序列出:

[8, 7, 6, 5, 4, 3, 2, 1]

Filter 用法

JMESPath 也支援與 jq 類似的 filter 語法,舉以下資料為例:

servers = [
    {"host": "a", "state": "up"},
    {"host": "b", "state": "down"},
    {"host": "c", "state": "up"},
]

假如我們要過濾出 state 值為 downhost 資料,就可能會用以下 Python 程式碼達成:

for s in servers:
    if s['state'] == 'down':
        print(s['host'])

如果改為 JMESPath ,則可以用語法 [?state=='down'].host 代替,其中 [?<表示式>] 就是 filter 語法,而 state=='down' 代表找出 state 值為 down 的資料:

import jmespath


expr = jmespath.compile("[?state=='down'].host")
bad_servers = expr.search(servers)
if bad_servers is None:
   print('No bad servers')
else:
   print(bad_servers)

上述範例執行結果如下:

['b']

如果要串連多個過濾條件,則可以用 &&|| 進行串聯,這 2 個分別是 ANDOR 的意思,例如 [?state=='down' && host == 'b'] 是過濾出 state == 'down' AND host == 'b' 的意思。

選擇多個欄位

舉以下資料為例,假設我們只想列出 usersname 以及 gender 欄位的話:

data = {
    'users': [
        {'id': 1, 'name': 'a', 'gender': 'f'},
        {'id': 2, 'name': 'b', 'gender': 'm'},
        {'id': 3, 'name': 'c', 'gender': 'f'},
    ]
}

用 JMESPath 則只要寫成 users[*][name, gender] 即可,代表從 users 所有元素,取出 name 以及 gender 欄位:

import jmespath


expr = jmespath.compile("users[*][name, gender]")
users = expr.search(data)
print(users)

上述範例執行結果如下,可以看到 name 以及 gender 欄位被以 list 方式輸出:

[['a', 'f'], ['b', 'm'], ['c', 'f']]

這種結果可以用以下 Python 程式碼進行 unpack:

for name, gender in users:
    pass

如果想保留原有欄位的話,則需要用 projection 的方式輸出。

Projection 用法

舉前述資料為例,如果想讓欄位名稱也跟著輸出的話,則是將 users[*][name, gender] 改為 users[*].{Name: name, Gender: gender} 即可,其中 {Name: name, Gender: gender} 則是所謂的 Projection, 意思為將 name 與 gender 的值分別映射到 Name 與 Gender 2 個欄位(此處用大寫避免混淆)

import jmespath


expr = jmespath.compile("users[*].{Name: name, Gender: gender}")
users = expr.search(data)
print(users)

上述範例執行結果如下:

[
    {"Name": "a", "Gender": "f"},
    {"Name": "b", "Gender": "m"},
    {"Name": "c", "Gender": "f"},
]

因此,要保留原有的欄位名稱的話,只要照著寫一遍欄位名稱做 projection 即可,例如改成 {name: name, gender: gender} 就能夠與原有欄位名稱相同的方式輸出。

總結

JMESPath 其實提供更多本文未提及的功能,例如排序、內建函式、 Pipe 等等,本文僅就較常用且直覺的功能進行介紹,其原因在於 JMESPath 的語法與 jq 都不是一種標準,除非團隊約定好要用 JMESPath 做為主要開發套件之一,否則過於複雜的語法,其可讀性反而不如 Python 程式碼來得更好。

總之,使用這類需要額外學習 1 套語法的套件,最好要拿捏它是否有效達到提高可讀性與可維護性的目標,而非拉高團隊成員的學習曲線以及程式碼的複雜度。

以上, Happy Coding!

References

JMESPath — JMESPath

https://github.com/jmespath/jmespath.py

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!