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
值為 down
的 host
資料,就可能會用以下 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 個分別是 AND
與 OR
的意思,例如 [?state=='down' && host == 'b']
是過濾出 state == 'down' AND host == 'b'
的意思。
選擇多個欄位
舉以下資料為例,假設我們只想列出 users
的 name
以及 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
https://github.com/jmespath/jmespath.py