Hlavní navigace

Rozhraní mezi nástrojem jq a programovacím jazykem Python

11. 8. 2020
Doba čtení: 29 minut

Sdílet

 Autor: Pixabay
Minulý týden jsme se seznámili s nástrojem jq, který slouží pro zpracování dat uložených ve formátu JSON z příkazové řádky. Tento nástroj, resp. jeho DSL lze ovšem použít i ve skriptech psaných v Pythonu.

Obsah

1. Rozhraní mezi nástrojem jq a programovacím jazykem Python

2. Balíček jq.py s implementací rozhraní mezi Pythonem a nástrojem jq

3. Rozhraní mezi Pythonem a nástrojem jq z pohledu programátora

4. Chování v případě chybného vstupu, obsluha výjimek

5. Metoda first

6. Použití uvozovek v dotazovacím jazyku nástroje jq

7. Ukázky nepatrně složitějších dotazů

8. Získání složitější datové struktury – slovníku nebo seznamu

9. Dotaz vracející seznam obsahující slovníky

10. Alternativní přístup k nástroji jq

11. Instalace balíčku pyjq a otestování, zda je ho možné naimportovat

12. Struktura volání jq přes rozhraní reprezentované balíčkem pyjq

13. Chování balíčku pyjq ve chvíli, kdy požadovaný prvek neexistuje

14. Použití uvozovek v dotazovacím jazyku

15. Ukázky nepatrně složitějších dotazů

16. Zřetězení dotazů s využitím znaku „|“

17. Přečtení složitější datové struktury – slovníku nebo seznamu

18. Ekvivalenty příkladů z první poloviny článku

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Rozhraní mezi nástrojem jq a programovacím jazykem Python

V článku o užitečném nástroji jq jsme si řekli, že nástroj jq, který je používám pro zpracování dat uložených ve formátu JSON, je určen primárně pro spouštění z příkazové řádky, popř. ze shell skriptů. Ovšem kvůli velké popularitě tohoto nástroje (ostatně viz například počet hvězdiček přidělených jeho repositáři) vzniklo i rozhraní mezi jq a programovacím jazykem Python. Ve skutečnosti, abychom byli více přesní, vznikla dokonce dvě rozhraní reprezentovaná různě pojmenovanými balíčky. První balíček se jmenuje jq, ovšem kvůli rozlišení oproti původnímu nástroji jq se většinou používá jméno jq.py. A druhý podobně koncipovaný balíček, ovšem s odlišným API, se pro změnu jmenuje pyjq (nyní pochopitelně bez tečky).

Před použitím pythoních balíčků jq.py nebo pyjq je vhodné si nainstalovat binární balíček jq se stejně pojmenovaným nástrojem jq spustitelným z příkazové řádky. Podrobnostem jsme se věnovali minule, takže jen krátce – nejrychlejší bývá použití správce balíčků vaší distribuce Linuxu. V případě dnes již dosti muzeální Fedory 27 proběhne instalace následovně:

$ sudo dnf install jq
 
Last metadata expiration check: 1:36:21 ago on Tue 04 Aug 2020 05:00:30 PM CEST.
Dependencies resolved.
================================================================================
 Package            Arch            Version               Repository       Size
================================================================================
Installing:
 jq                 x86_64          1.5-8.fc27            fedora          158 k
Installing dependencies:
 oniguruma          x86_64          6.6.1-1.fc27          fedora          178 k
 
Transaction Summary
================================================================================
Install  2 Packages
 
Total download size: 337 k
Installed size: 1.1 M
Is this ok [y/N]: y
Poznámka: podobně snadno instalace proběhne i na distribucích založených na APT apod.

2. Balíček jq.py s implementací rozhraní mezi Pythonem a nástrojem jq

Nejdříve se budeme věnovat balíčku pojmenovaném jq.py, jenž zpřístupňuje nástroj jq přímo programátorům používajícím Python. Tento balíček lze nainstalovat snadno, typicky nástrojem pip nebo pip3, popř. lze pochopitelně využít virtuální prostředí Pythonu:

$ pip3 install --user jq
 
Collecting jq
  Downloading https://files.pythonhosted.org/packages/37/83/e1f7162986c228cc33768b9c53c1167cafe222f8d81f1325a27cfff42f47/jq-1.0.2-cp36-cp36m-manylinux1_x86_64.whl (502kB)
    100% |████████████████████████████████| 512kB 1.5MB/s
Installing collected packages: jq
Successfully installed jq-1.0.2
Poznámka: instalace proběhne takto rychle pouze v tom případě, že již máte nainstalován i binární balíček jq. Pokud tomu tak není, pokusí se pip nebo pip3 nejdříve získat zdrojové kódy jq a přeložit je. K tomu bude potřebovat základní sadu vývojářských nástrojů gcc, zejména překladač céčka, linker a nástroj make. V případě, že je binární verze jq na systému již nainstalována (viz úvodní kapitolu), jsou tyto kroky přeskočeny.

Následně se můžeme přesvědčit, že je balíček jq.py skutečně dostupný pro vývojáře používající programovací jazyk Python. Následující skript by měl být spustitelný a měl by zobrazit nápovědu k balíčku jq.py:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou jq.py
 
import jq
 
help(jq)

S přibližným výstupem:

Help on module jq:
 
NAME
    jq
 
FUNCTIONS
    all(...)
 
    compile(...)
 
    first(...)
 
    iter(...)
 
    jq(...)
 
    text(...)
 
DATA
    __test__ = {}
 
FILE

Pokud dojde k chybě při importu, zkontrolujte si, zda je jq.py nainstalován v adresáři, který je součástí seznamu cest, na kterých interpret Pythonu hledá balíčky:

import sys
print(sys.path)
Poznámka: ve skutečnosti jsme prozatím otestovali pouze to, že lze naimportovat balíček jq.py do interpretru Pythonu, nikoli samotné rozhraní k binárnímu nástroji jq.

3. Rozhraní mezi Pythonem a nástrojem jq z pohledu programátora

V balíčku jq.py je k dispozici pouze několik funkcí a metod, které zprostředkovávají rozhraní mezi nástrojem jq a skriptem psaným v Pythonu. Jedná se o následující funkce a metody:

# Funkce Stručný popis
1 compile() (funkce) program psaný v DSL nástroje jq je přeložen a vrácen ve formě objektu
2 input() metoda, které se předají vstupní data buď ve formě textu nebo JSON objektu
3 first() získání prvního výsledku aplikace dotazu na JSON data
4 all() získání všech výsledků aplikace dotazu na JSON data
5 text() získání výsledků ve formě textu (a nikoli slovníku nebo seznamu)
6 iter() (funkce) získání iterátoru pro procházení jednotlivými prvky výsledku

Podívejme se nyní na několik základních příkladů použití balíčku jq.py.

Přečtení vstupního souboru do řetězce (tedy do čistého textu), aplikace dotazu na text a získání výsledku ve formě seznamu:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou jq.py
 
import jq
 
with open("openapi.json") as fin:
    content = fin.read()
    print(jq.compile(".openapi").input(text=content).all())

Výsledek:

['3.0.0']

Přečtení vstupního souboru do objektu JSON, aplikace dotazu na JSON a získání výsledku ve formě seznamu:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8

#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou jq.py
 
import jq
import json
 
with open("openapi.json") as fin:
    content = json.load(fin)
    print(jq.compile(".openapi").input(content).all())

Výsledek:

['3.0.0']

Dtto, ale tentokrát získáme výsledek ve formě čistého textu a nikoli seznamu:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou jq.py
 
import jq
 
with open("openapi.json") as fin:
    content = fin.read()
    print(jq.compile(".openapi").input(text=content).text())

Výsledek:

"3.0.0"
#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou jq.py
 
import jq
import json
 
with open("openapi.json") as fin:
    content = json.load(fin)
    print(jq.compile(".openapi").input(content).text())

Výsledek:

"3.0.0"
Poznámka: povšimněte si, že součástí výsledku (tedy řetězce) jsou i uvozovky, které je možné v případě potřeby odstranit prostředky samotného Pythonu. Podobně se vrátí řetězec (i když to tak nemusí na první pohled vypadat) ve chvíli, kdy například přečteme pole a necháme si ho vrátit ve formě textu:
#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou jq.py
 
import jq
import json
 
with open("openapi.json") as fin:
    content = json.load(fin)
    value = jq.compile(".security").input(content).text()
    print(value)
    print(type(value))

Výsledek:

[]
<class 'str'>

4. Chování v případě chybného vstupu, obsluha výjimek

Nyní se pokusme zpracovat chybný soubor nazvaný „broken.json“, který neobsahuje korektní obsah v JSONu. Obsah tohoto souboru nelze přečíst prostředky poskytovanými standardním balíčkem json, takže vyzkoušíme, co se stane při přečtení obsahu souboru do řetězce s předáním tohoto řetězce zkompilovanému dotazu balíčku jq.py:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou jq.py
 
import jq
 
with open("broken.json") as fin:
    content = fin.read()
    print(jq.compile(".openapi").input(text=content).all())

Při pokusu o spuštění výše uvedeného skriptu dojde k výjimce, která naznačuje, že se vzniklá chyba v binárním balíčku jq pouze převede na výjimku a předá se volajícímu kódu:

$ ./04_no_error_handling.py
 
Traceback (most recent call last):
  File "./04_no_error_handling.py", line 11, in <module>
    print(jq.compile(".openapi").input(text=content).all())
  File "jq.pyx", line 211, in jq._ProgramwithInput.all
  File "jq.pyx", line 242, in jq._ResultIterator.__next__
  File "jq.pyx", line 248, in jq._ResultIterator._next_string
  File "jq.pyx", line 275, in jq._ResultIterator._ready_next_input
ValueError: parse error: Expected separator between values at line 11, column 15

Takovou výjimku pochopitelně můžeme velmi snadno odchytit a nějakým způsobem na ni zareagovat (minimálně vypsat informace do logu, když už nic jiného):

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou jq.py
 
import jq
 
with open("broken.json") as fin:
    content = fin.read()
    try:
        print(jq.compile(".openapi").input(text=content).all())
    except Exception as e:
        print(e)

Chování takto upraveného skriptu po jeho spuštění již bude dosti odlišné:

$ ./05_error_handling.py
 
parse error: Expected separator between values at line 11, column 15

5. Metoda first

Víme již, že pokud je vstup do jq zpracován korektně, můžeme výstup získat buď ve formě seznamu (metoda all) nebo textu (metoda text). Mnohdy je ovšem výsledkem dotazu jediná hodnota (prvek z původního JSONu, popř. nějakým způsobem transformovaný prvek). A hodnotu tohoto jediného prvku získáme metodou first, což je ukázáno na dalším demonstračním příkladu:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou jq.py
 
import jq
import json
 
with open("openapi.json") as fin:
    content = json.load(fin)
    print(jq.compile(".openapi").input(content).first())
    print(jq.compile(".info.description").input(content).first())
    print(jq.compile(".tags").input(content).first())

S výsledkem:

3.0.0
A very simple REST API service
[]

O tom, že se skutečně jedná o prvky se správným typem (v Pythonu) se opět můžeme přesvědčit po úpravě skriptu:

with open("openapi.json") as fin:
    content = json.load(fin)
    print(type(jq.compile(".openapi").input(content).first()))
    print(type(jq.compile(".info.description").input(content).first()))
    print(type(jq.compile(".tags").input(content).first()))

Nyní se namísto hodnot získaných prvků vypíšou jejich typy:

<class 'str'>
<class 'str'>
<class 'list'>

V případě, že prvek není nalezen (je zadána špatná cesta, resp. cesta neodpovídající obsahu JSONu), vrátí se hodnota None:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou jq.py
 
import jq
import json
 
with open("openapi.json") as fin:
    content = json.load(fin)
    print(jq.compile(".non_existing_key").input(content).first())

Vytiskne se:

None

Který je skutečně správného typu:

with open("openapi.json") as fin:
    content = json.load(fin)
    print(type(jq.compile(".non_existing_key").input(content).first()))

S výsledkem:

<class 'NoneType'>

6. Použití uvozovek v dotazovacím jazyku nástroje jq

Balíček jq.py ve skutečnosti tvoří pouze velmi úzkou vrstvu mezi Pythonem a nástrojem jq. Například se vlastně žádným způsobem nemodifikuje dotaz (query) zapisovaný v doménově specifickém jazyku (DSL) nástroje jq. Musíme si dát pozor především na správné použití uvozovek ve chvíli, kdy se přistupuje ke klíči, který například obsahuje lomítko či jiný specifický znak (JSON je sice pojmenován podle JavaScriptu, ovšem jeho syntaxe je v tomto ohledu volnější). Podívejme se nyní na demonstrační příklad, ve kterém se přistupuje ke zvýrazněné části JSONu:

{
    ...
    ...
    ...
    "paths": {
        "/": {
            "get": {
                ...
                ...
                ...
            }
        }
    }
}

V dotazu se tedy musí vyskytovat .paths./, což ovšem není korektní zápis. Ve skutečnosti musíme lomítko (poslední část dotazu) zapsat do uvozovek. Pokud do uvozovek vložíme celý dotaz, vrátí se samotný dotaz jako výsledek (řetězec); pokud uvozovky neuvedeme vůbec, dojde k chybě při pokusu o spuštění skriptu:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou jq.py
 
import jq
import json
 
with open("openapi.json") as fin:
    content = json.load(fin)
    print("-----------------------------")
    print(jq.compile('.paths."/"').input(content).first())
    print("-----------------------------")
    print(jq.compile('".paths./"').input(content).first())
    print("-----------------------------")
    print(jq.compile('.paths./').input(content).first())

Po spuštění tohoto demonstračního příkladu je jasně patrné, jakým způsobem jsou výsledné dotazy zpracovány, resp. v posledním případě nezpracovány:

-----------------------------
{'get': {'summary': 'Returns valid HTTP 200 ok status when the service is ready', 'description': '', 'parameters': [], 'operationId': 'main', 'responses': {'default': {'description': 'Default response'}}}}
-----------------------------
.paths./
-----------------------------
Traceback (most recent call last):
  File "./08_escape_characters.py", line 17, in
    print(jq.compile('.paths./').input(content).first())
  File "jq.pyx", line 56, in jq.compile
  File "jq.pyx", line 160, in jq._Program.__cinit__
  File "jq.pyx", line 131, in jq._JqStatePool.__cinit__
  File "jq.pyx", line 84, in jq._compile
  File "jq.pyx", line 72, in jq._compile
  File "jq.pyx", line 78, in jq._compile
ValueError: jq: error: syntax error, unexpected '/', expecting FORMAT or QQSTRING_START (Unix shell quoting issues?) at , line 1:
.paths./
jq: 1 compile error

7. Ukázky nepatrně složitějších dotazů

Získání informace o licenci, pod kterou je soubor vydán:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou jq.py
 
import jq
import json
 
with open("openapi.json") as fin:
    content = json.load(fin)
    print(jq.compile(".info.license.name").input(content).first())

S výsledkem:

Apache 2.0
Poznámka: typ výsledku je v tomto případě pochopitelně řetězec.

Získání souhrnných popisů všech endpointů s HTTP požadavkem typu GET:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou jq.py
 
import jq
import json
 
with open("openapi.json") as fin:
    content = json.load(fin)
    summaries = jq.compile(".paths[] | .get.summary").input(content).all()
    for summary in summaries:
        print(summary)

Nyní se vypíše:

Returns valid HTTP 200 ok status when the service is ready
Read list of all clusters from database and return it to a client
Read cluster specified by its ID and return it to a client
Search for a cluster specified by its ID or name

Dtto, ovšem pro HTTP požadavky typu DELETE (ten existuje pouze pro jediný koncový bod):

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou jq.py
 
import jq
import json
 
with open("openapi.json") as fin:
    content = json.load(fin)
    summaries = jq.compile(".paths[] | .delete.summary").input(content).all()
    for summary in summaries:
        print(summary)

S výsledky:

None
None
Delete a cluster specified by its ID
None
Poznámka: zde je zapotřebí si dát pozor na to, že u některých endpointů získáme řetězce a u jiných hodnotu None.

8. Získání složitější datové struktury – slovníku nebo seznamu

V případě, že je výsledkem dotazu obsah uzlu s dalšími poduzly nebo polem, vrátí se tento obsah ve formě příslušné datové struktury Pythonu, což si ostatně ukážeme na dalším příkladu, který obsahuje tentýž dotaz, jednou ovšem zapsaný v dvojitých uvozovkách (interně vyžaduje „quotování“ vnitřních uvozovek) a podruhé zapsaný s uvozovkami jednoduchými:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou jq.py
 
import jq
import json
from pprint import pprint
 
with open("openapi.json") as fin:
    content = json.load(fin)
    search = jq.compile(".paths.\"/client/cluster/search\"").input(content).first()
    pprint(search)
 
    print("----------------------------------------------------------------------------")
 
    search = jq.compile('.paths."/client/cluster/search"').input(content).first()
    pprint(search)

Výsledek je v obou případech vždy stejný:

{'get': {'description': '',
         'operationId': 'searchCluster',
         'parameters': [{'allowEmptyValue': True,
                         'description': 'Cluster ID',
                         'in': 'query',
                         'name': 'id',
                         'required': False,
                         'schema': {'type': 'string'}},
                        {'allowEmptyValue': True,
                         'description': 'Cluster name',
                         'in': 'query',
                         'name': 'name',
                         'required': False,
                         'schema': {'type': 'string'}}],
         'responses': {'default': {'description': 'Default response'}},
         'summary': 'Search for a cluster specified by its ID or name'}}
----------------------------------------------------------------------------
{'get': {'description': '',
         'operationId': 'searchCluster',
         'parameters': [{'allowEmptyValue': True,
                         'description': 'Cluster ID',
                         'in': 'query',
                         'name': 'id',
                         'required': False,
                         'schema': {'type': 'string'}},
                        {'allowEmptyValue': True,
                         'description': 'Cluster name',
                         'in': 'query',
                         'name': 'name',
                         'required': False,
                         'schema': {'type': 'string'}}],
         'responses': {'default': {'description': 'Default response'}},
         'summary': 'Search for a cluster specified by its ID or name'}}

V dalším příkladu je výsledkem dotazu seznam seznamů a ve druhé části seznam pravdivostních hodnot True nebo False. Seznam obsahuje pro každý koncový bod další seznam s podporovanými metodami, který následně spojíme do řetězce tak, aby byly názvy jednotlivých metod odděleny čárkami:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou jq.py
 
import jq
import json
 
with open("openapi.json") as fin:
    content = json.load(fin)
 
    for endpoint in jq.compile('.paths[]').input(content).all():
        print(",".join(endpoint.keys()))
 
    print("-------------------------")
 
    for has_get in jq.compile('.paths[] | has("get")').input(content).all():
        print(has_get)

Výsledek zobrazený po spuštění tohoto demonstračního příkladu:

get
x-temp,get
get,post,delete
get
-------------------------
True
True
True
True

Dtto, ovšem ve druhém dotazu a na něj navázané programové smyčce zobrazíme informaci, zda daný koncový bod podporuje HTTP metodu DELETE:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou jq.py
 
import jq
import json
 
with open("openapi.json") as fin:
    content = json.load(fin)
 
    for endpoint in jq.compile('.paths[]').input(content).all():
        print(",".join(endpoint.keys()))
 
    print("-------------------------")
 
    for has_delete in jq.compile('.paths[] | has("delete")').input(content).all():
        print(has_delete)

S výsledky:

get
x-temp,get
get,post,delete
get
-------------------------
False
False
True
False

9. Dotaz vracející seznam obsahující slovníky

V posledním demonstračním příkladu, který je založen na balíčku jq.py použijeme dotaz vracející seznam, jehož prvky jsou slovníky. Dotaz totiž získá všechny parametry koncového bodu „/client/cluster/search“ pro HTTP metodu GET:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou jq.py
 
import jq
import json
from pprint import pprint
 
with open("openapi.json") as fin:
    content = json.load(fin)
 
    for parameters in jq.compile('.paths."/client/cluster/search".get.parameters').input(content).all():
        pprint(parameters)

Po spuštění tohoto skriptu by se měl zobrazit seznam s dvojicí prvků, přičemž každý prvek je slovníkem. Povšimněte si, že pravdivostní hodnoty jsou z JSONu převedeny na skutečné pravdivostní hodnoty jazyka Python:

[{'allowEmptyValue': True,
  'description': 'Cluster ID',
  'in': 'query',
  'name': 'id',
  'required': False,
  'schema': {'type': 'string'}},
 {'allowEmptyValue': True,
  'description': 'Cluster name',
  'in': 'query',
  'name': 'name',
  'required': False,
  'schema': {'type': 'string'}}]

10. Alternativní přístup k nástroji jq

V první polovině článku jsme se primárně zabývali balíčkem jq.py, který poskytuje vývojářům pracujícím v programovacím jazyku Python rozhraní pro nástroj jq. Víme již, že kromě balíčku jq.py existuje i alternativní balíček nazvaný pro změnu pyjq. Ve druhé části dnešního článku si tedy ukážeme základní způsob použití tohoto balíčku, který se ovládá nepatrně odlišným způsobem. Ovšem základní princip zůstává stejný – získat ze vstupního JSONu datovou strukturu nebo seznam struktur a tu dále nějakým způsobem dále zpracovat.

Poznámka: většina příkladů je zvolena takovým způsobem, aby se podobaly příkladům z první poloviny článku.

11. Instalace balíčku pyjq a otestování, zda je ho možné naimportovat

Instalaci balíčku pyjq opět provedeme pomocí nástroje pip, popř. pip3. Instalace bude nepatrně delší, protože se bude překládat i nativní část balíčku (počítejte s cca dvaceti sekundami):

$ pip3 install --user pyjq
 
Collecting pyjq
  Downloading https://files.pythonhosted.org/packages/a5/7c/b7fdc7b9653d5f05552cb08b6e9883db13db21ca0c8b0cd100e5a5ed3a35/pyjq-2.4.0.tar.gz (2.0MB)
    100% |████████████████████████████████| 2.0MB 723kB/s
Installing collected packages: pyjq
  Running setup.py install for pyjq ... done
Successfully installed pyjq-2.4.0

A opět si můžeme otestovat, zda je možné naimportovat balíček pyjq do Pythonovského skriptu:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou pyjq
 
import pyjq
 
help(pyjq)

Tento skript by měl po svém spuštění vypsat (dosti stručnou) nápovědu:

Help on module pyjq:
 
NAME
    pyjq
 
DATA
    __all__ = []
 
FILE
    /home/ptisnovs/.local/lib/python3.6/site-packages/pyjq.py
 

12. Struktura volání jq přes rozhraní reprezentované balíčkem pyjq

Ze zdrojových kódů následujících dvou skriptů je patrné, že se struktura volání mezi balíčky jq.py a pyjq odlišuje. Při použití balíčku pyjq je nejdříve nutné přeložit dotaz (naprosto stejně pojmenovanou funkcí – konstruktorem), ovšem následně se již přímo volá metoda all nebo first, které se předá již načtený obsah JSON souboru. Povšimněte si, že není podporováno přímé zpracování dat z textového souboru (resp. z řetězce) – vždy se použije standardní balíček json.

Volání ve chvíli, kdy vyžadujeme získání většího množství hodnot:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou pyjq
 
import pyjq
import json
 
with open("openapi.json") as fin:
    content = json.load(fin)
    print(pyjq.compile(".openapi").all(content))

V tomto konkrétním případě se vrátí seznam s jedinou hodnotou, ovšem stále se bude jednat o seznam:

['3.0.0']

Volání ve chvíli, kdy vyžadujeme přečtení jediné hodnoty, tj. buď prvního prvku nebo jediného prvku odpovídajícího dotazu:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou pyjq
 
import pyjq
import json
 
with open("openapi.json") as fin:
    content = json.load(fin)
    print(pyjq.compile(".openapi").first(content))

Výsledek je tvořen řetězcem:

3.0.0
Poznámka: povšimněte si, že řetězec nyní neobsahuje uvozovky, na rozdíl od podobné konstrukce použité v balíčku jq.py.

Pozor ovšem na to, že čistě textový vstup nelze zpracovat:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou pyjq
 
import pyjq
 
with open("openapi.json") as fin:
    content = fin.read()
    print(pyjq.compile(".openapi").all(content))

Při pokusu o zpracování dojde k chybě:

Traceback (most recent call last):
  File "./14_no_text_processing.py", line 11, in <module>
    print(pyjq.compile(".openapi").all(content))
  File "_pyjq.pyx", line 213, in _pyjq.Script.all
_pyjq.ScriptRuntimeError: Cannot index string with string "openapi"

13. Chování balíčku pyjq ve chvíli, kdy požadovaný prvek neexistuje

V případě, že požadovaný prvek neexistuje (tedy většinou tehdy, kdy není nalezen příslušný klíč), vrací balíček pyjq buď hodnotu None nebo seznam s jediným prvkem None. Otestujme si nejprve první případ, tj. pokus o přečtení jediného prvku, který ovšem v JSON souboru neexistuje:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou pyjq
 
import pyjq
import json
 
with open("openapi.json") as fin:
    content = json.load(fin)
    print(pyjq.compile(".foobar").first(content))

Prvek s klíčem foobar v JSON souboru nelze nalézt, proto se vrátí hodnota:

None

Ve druhém případě budeme chtít získat více prvků, nikoli prvek jediný, takže se použije metoda all:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou pyjq
 
import pyjq
import json
 
with open("openapi.json") as fin:
    content = json.load(fin)
    print(pyjq.compile(".foobar").all(content))

Výsledkem je v tomto případě seznam s jediným prvkem obsahujícím hodnotu None:

[None]

14. Použití uvozovek v dotazovacím jazyku

V šesté kapitole jsme si řekli, že pokud klíče obsahují některé speciální znaky, zejména lomítka, je nutné tyto názvy klíčů (ovšem nikoli celý dotaz!) umístit do uvozovek. Pokud to neuděláme, dojde k chybě. Obě možnosti ilustruje následující demonstrační příklad, který vznikl přímým přepisem příkladu ze šesté kapitoly:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou pyjq
 
import pyjq
import json
 
with open("openapi.json") as fin:
    content = json.load(fin)
    print("-----------------------------")
    print(pyjq.compile('.paths."/"').first(content))
    print("-----------------------------")
    print(pyjq.compile('".paths./"').first(content))
    print("-----------------------------")
    print(pyjq.compile('.paths./').first(content))

První výsledek je korektní, ve druhém případě je dotaz chápán jako konstantní řetězec a v případě třetím (zcela chybějící uvozovky) dojde k chybě při běhu:

-----------------------------
{'get': {'summary': 'Returns valid HTTP 200 ok status when the service is ready', 'description': '', 'parameters': [], 'operationId': 'main', 'responses': {'default': {'description': 'Default response'}}}}
-----------------------------
.paths./
-----------------------------
Traceback (most recent call last):
  File "./06_escape_characters.py", line 17, in <module>
    print(pyjq.compile('.paths./').first(content))
  File "/home/ptisnovs/.local/lib/python3.6/site-packages/pyjq.py", line 19, in compile
    library_paths=library_paths)
  File "_pyjq.pyx", line 190, in _pyjq.Script.__init__
ValueError: jq: error: syntax error, unexpected '/', expecting FORMAT or QQSTRING_START (Unix shell quoting issues?) at , line 1:
.paths./
jq: 1 compile error

15. Ukázky nepatrně složitějších dotazů

Získání informace o licenci, pod kterou je soubor vydán (viz též sedmou kapitolu):

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou pyjq
 
import pyjq
import json
 
with open("openapi.json") as fin:
    content = json.load(fin)
    print(pyjq.compile(".info.license.name").first(content))

S výsledkem:

Apache 2.0
Poznámka: typ výsledku je v tomto případě pochopitelně řetězec.

16. Zřetězení dotazů s využitím znaku „|“

Získání souhrnných popisů všech endpointů s HTTP požadavkem typu GET:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou pyjq
 
import pyjq
import json
 
with open("openapi.json") as fin:
    content = json.load(fin)
    summaries = pyjq.compile(".paths[] | .get.summary").all(content)
    for summary in summaries:
        print(summary)

Po spuštění tohoto demonstračního příkladu se vypíše:

Returns valid HTTP 200 ok status when the service is ready
Read list of all clusters from database and return it to a client
Read cluster specified by its ID and return it to a client
Search for a cluster specified by its ID or name

Dtto, ovšem pro HTTP požadavky typu DELETE (ten existuje pouze pro jediný koncový bod):

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou pyjq
 
import pyjq
import json
 
with open("openapi.json") as fin:
    content = json.load(fin)
    summaries = pyjq.compile(".paths[] | .delete.summary").all(content)
    for summary in summaries:
        print(summary)

Výsledky:

None
None
Delete a cluster specified by its ID
None

Pro jistotu se podíváme i na typy vrácených hodnot:

with open("openapi.json") as fin:
    content = json.load(fin)
    summaries = pyjq.compile(".paths[] | .delete.summary").all(content)
    for summary in summaries:
        print(summary, type(summary))

Výsledky:

None <class 'NoneType'>
None <class 'NoneType'>
Delete a cluster specified by its ID <class 'str'>
None <class 'NoneType'>

17. Přečtení složitější datové struktury – slovníku nebo seznamu

Opět si ukažme, jak je možné získat složitější datovou strukturu z JSON souboru. Většinou se bude jednat o slovník nebo seznam, v našem případě slovník:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou pyjq
 
import pyjq
import json
from pprint import pprint
 
with open("openapi.json") as fin:
    content = json.load(fin)
    search = pyjq.compile(".paths.\"/client/cluster/search\"").first(content)
    pprint(search)
 
    print("----------------------------------------------------------------------------")
 
    search = pyjq.compile('.paths."/client/cluster/search"').first(content)
    pprint(search)

Oba dotazy jsou totožné, ovšem druhý dotaz je díky použití jednoduchých i dvojitých uvozovek mnohem čitelnější:

{'get': {'description': '',
         'operationId': 'searchCluster',
         'parameters': [{'allowEmptyValue': True,
                         'description': 'Cluster ID',
                         'in': 'query',
                         'name': 'id',
                         'required': False,
                         'schema': {'type': 'string'}},
                        {'allowEmptyValue': True,
                         'description': 'Cluster name',
                         'in': 'query',
                         'name': 'name',
                         'required': False,
                         'schema': {'type': 'string'}}],
         'responses': {'default': {'description': 'Default response'}},
         'summary': 'Search for a cluster specified by its ID or name'}}
----------------------------------------------------------------------------
{'get': {'description': '',
         'operationId': 'searchCluster',
         'parameters': [{'allowEmptyValue': True,
                         'description': 'Cluster ID',
                         'in': 'query',
                         'name': 'id',
                         'required': False,
                         'schema': {'type': 'string'}},
                        {'allowEmptyValue': True,
                         'description': 'Cluster name',
                         'in': 'query',
                         'name': 'name',
                         'required': False,
                         'schema': {'type': 'string'}}],
         'responses': {'default': {'description': 'Default response'}},
         'summary': 'Search for a cluster specified by its ID or name'}}

18. Ekvivalenty příkladů z první poloviny článku

Demonstrační příklady, na nichž jsme si ukázali některé možnosti balíčku jq.py, lze pochopitelně velmi snadno přepsat tak, aby používaly alternativní balíček pyjq:

#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou pyjq
 
import pyjq
import json
 
with open("openapi.json") as fin:
    content = json.load(fin)
 
    for endpoint in pyjq.compile('.paths[]').all(content):
        print(",".join(endpoint.keys()))
 
    print("-------------------------")
 
    for has_get in pyjq.compile('.paths[] | has("get")').all(content):
        print(has_get)

Výsledek:

get
x-temp,get
get,post,delete
get
-------------------------
True
True
True
True
#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou pyjq
 
import pyjq
import json
 
with open("openapi.json") as fin:
    content = json.load(fin)
 
    for endpoint in pyjq.compile('.paths[]').all(content):
        print(",".join(endpoint.keys()))
 
    print("-------------------------")
 
    for has_delete in pyjq.compile('.paths[] | has("delete")').all(content):
        print(has_delete)

Výsledek:

skoleni

get
x-temp,get
get,post,delete
get
-------------------------
False
False
True
False
#!/usr/bin/env python3
# vim: set fileencoding=utf-8
 
#  Demonstrační příklad k článku:
#      Zpracování dat uložených ve formátu JSON knihovnou pyjq
 
import pyjq
import json
from pprint import pprint
 
with open("openapi.json") as fin:
    content = json.load(fin)
 
    for parameters in pyjq.compile('.paths."/client/cluster/search".get.parameters').all(content):
        pprint(parameters)

Výsledek:

[{'allowEmptyValue': True,
  'description': 'Cluster ID',
  'in': 'query',
  'name': 'id',
  'required': False,
  'schema': {'type': 'string'}},
 {'allowEmptyValue': True,
  'description': 'Cluster name',
  'in': 'query',
  'name': 'name',
  'required': False,
  'schema': {'type': 'string'}}]
Poznámka: JSON není jediným vhodným formátem pro přenosy strukturovaných dat. V některých ohledech je výhodnější použití formátu EDN pocházejícího ze světa programovacího jazyka Clojure. S tímto formátem se seznámíme příště.

19. Repositář s demonstračními příklady

Zdrojové kódy všech dnes použitých demonstračních příkladů byly uloženy do nového Git repositáře, který je dostupný na adrese https://github.com/tisnik/most-popular-python-libs. V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má přibližně několik jednotek kilobajtů), můžete namísto toho použít odkazy na jednotlivé demonstrační příklady a jejich části, které naleznete v následující tabulce:

# Příklad Stručný popis Cesta
1 01_basic_installation_check.py základní test, zda byl balíček jq.py nainstalován https://github.com/tisnik/most-popular-python-libs/blob/master/jq.py/01_ba­sic_installation_check.py
2 02_process_as_text.py zpracování vstupních dat reprezentovaných řetězcem https://github.com/tisnik/most-popular-python-libs/blob/master/jq.py/02_pro­cess_as_text.py
3 02_process_as_text_to_text.py zpracování vstupních dat reprezentovaných řetězcem, výsledkem je text https://github.com/tisnik/most-popular-python-libs/blob/master/jq.py/02_pro­cess_as_text_to_text.py
4 03_process_as_json.py zpracování již deserializovaných vstupních dat https://github.com/tisnik/most-popular-python-libs/blob/master/jq.py/03_pro­cess_as_json.py
5 03_process_as_json_to_text.py zpracování již deserializovaných vstupních dat, zpracovává se řetězec https://github.com/tisnik/most-popular-python-libs/blob/master/jq.py/03_pro­cess_as_json_to_text.py
6 03_process_as_json_to_text_B.py zpracování již deserializovaných vstupních dat, zpracovává se seznam https://github.com/tisnik/most-popular-python-libs/blob/master/jq.py/03_pro­cess_as_json_to_text_B.py
7 04_no_error_handling.py chování při výskytu chyby ve vstupních datech https://github.com/tisnik/most-popular-python-libs/blob/master/jq.py/04_no_e­rror_handling.py
8 05_error_handling.py reakce na chyby https://github.com/tisnik/most-popular-python-libs/blob/master/jq.py/05_e­rror_handling.py
9 06_first_value.py získání pouze prvního výsledku dotazu https://github.com/tisnik/most-popular-python-libs/blob/master/jq.py/06_fir­st_value.py
10 06_first_value_type.py získání typu prvního výsledku dotazu https://github.com/tisnik/most-popular-python-libs/blob/master/jq.py/06_fir­st_value_type.py
11 07_non_existing_key.py chování v případě, že dotaz nenalezl žádnou hodnotu https://github.com/tisnik/most-popular-python-libs/blob/master/jq.py/07_non_e­xisting_key.py
12 07_non_existing_key_type.py chování v případě, že dotaz nenalezl žádnou hodnotu https://github.com/tisnik/most-popular-python-libs/blob/master/jq.py/07_non_e­xisting_key_type.py
13 08_escape_characters.py problematika speciálních znaků v DSL nástroje jq https://github.com/tisnik/most-popular-python-libs/blob/master/jq.py/08_es­cape_characters.py
14 09_get_license.py přečtení licence uložené v JSONu https://github.com/tisnik/most-popular-python-libs/blob/master/jq.py/09_get_li­cense.py
15 10_summary_for_all_endpoints_get.py složitější dotaz na všechny metody podporované endpointy https://github.com/tisnik/most-popular-python-libs/blob/master/jq.py/10_sum­mary_for_all_endpoints_get­.py
16 11_summary_for_all_endpoints_delete.py složitější dotaz na všechny metody podporované endpointy https://github.com/tisnik/most-popular-python-libs/blob/master/jq.py/11_sum­mary_for_all_endpoints_de­lete.py
17 12_search_endpoint.py získání složitější datové struktury – slovníku nebo seznamu https://github.com/tisnik/most-popular-python-libs/blob/master/jq.py/12_se­arch_endpoint.py
18 13_has_get_method.py získání seznamu obsahujícího další seznamy jako své prvky https://github.com/tisnik/most-popular-python-libs/blob/master/jq.py/13_has_get_met­hod.py
19 14_has_delete_method.py získání seznamu obsahujícího další seznamy jako své prvky https://github.com/tisnik/most-popular-python-libs/blob/master/jq.py/14_has_de­lete_method.py
20 15_get_parameters.py dotaz vracející seznam obsahující slovníky https://github.com/tisnik/most-popular-python-libs/blob/master/jq.py/15_get_pa­rameters.py
       
18 01_basic_installation_check.py základní test, zda byl balíček pyjq nainstalován https://github.com/tisnik/most-popular-python-libs/blob/master/pyjq/01_ba­sic_installation_check.py
19 02_process_as_json.py zpracování již deserializovaných vstupních dat https://github.com/tisnik/most-popular-python-libs/blob/master/pyjq/02_pro­cess_as_json.py
20 03_first_value.py získání pouze prvního výsledku dotazu https://github.com/tisnik/most-popular-python-libs/blob/master/pyjq/03_fir­st_value.py
21 04_non_existing_key.py chování v případě, že dotaz nenalezl žádnou hodnotu (čtení jediného prvku) https://github.com/tisnik/most-popular-python-libs/blob/master/pyjq/04_non_e­xisting_key.py
22 05_non_existing_key.py chování v případě, že dotaz nenalezl žádnou hodnotu (čtení seznamu) https://github.com/tisnik/most-popular-python-libs/blob/master/pyjq/05_non_e­xisting_key.py
23 06_escape_characters.py problematika speciálních znaků v DSL nástroje jq https://github.com/tisnik/most-popular-python-libs/blob/master/pyjq/06_es­cape_characters.py
24 07_get_license.py přečtení licence uložené v JSONu https://github.com/tisnik/most-popular-python-libs/blob/master/pyjq/07_get_li­cense.py
25 08_summary_for_all_endpoints_get.py zřetězení dotazů s využitím znaku „|“ https://github.com/tisnik/most-popular-python-libs/blob/master/pyjq/08_sum­mary_for_all_endpoints_get­.py
26 09_summary_for_all_endpoints_delete.py zřetězení dotazů s využitím znaku „|“ https://github.com/tisnik/most-popular-python-libs/blob/master/pyjq/09_sum­mary_for_all_endpoints_de­lete.py
27 10_search_endpoint.py získání složitější datové struktury – slovníku nebo seznamu https://github.com/tisnik/most-popular-python-libs/blob/master/pyjq/10_se­arch_endpoint.py
28 11_has_get_method.py získání seznamu obsahujícího další seznamy jako své prvky https://github.com/tisnik/most-popular-python-libs/blob/master/pyjq/11_has_get_met­hod.py
29 12_has_delete_method.py získání seznamu obsahujícího další seznamy jako své prvky https://github.com/tisnik/most-popular-python-libs/blob/master/pyjq/12_has_de­lete_method.py
30 13_get_parameters.py dotaz vracející seznam obsahující slovníky https://github.com/tisnik/most-popular-python-libs/blob/master/pyjq/13_get_pa­rameters.py
31 14_no_text_processing.py nelze zpracovávat čistě textový vstup https://github.com/tisnik/most-popular-python-libs/blob/master/pyjq/14_no_tex­t_processing.py

20. Odkazy na Internetu

  1. Zpracování dat reprezentovaných ve formátu JSON nástrojem jq
    https://www.root.cz/clanky/zpracovani-dat-reprezentovanych-ve-formatu-json-nastrojem-jq/
  2. Balíček jq.py na PyPi
    https://pypi.org/project/jq/
  3. Balíček pyjq na PyPi
    https://pypi.org/project/pyjq/
  4. Repositář projektu jq (GitHub)
    https://github.com/stedolan/jq
  5. GitHub stránky projektu jq
    https://stedolan.github.io/jq/
  6. 5 modern alternatives to essential Linux command-line tools
    https://opensource.com/ar­ticle/20/6/modern-linux-command-line-tools
  7. Návod k nástroji jq
    https://stedolan.github.i­o/jq/tutorial/
  8. jq Manual (development version)
    https://stedolan.github.io/jq/manual/
  9. Introducing JSON
    https://www.json.org/json-en.html
  10. jq.py: a lightweight and flexible JSON processor
    https://github.com/mwilliamson/jq.py
  11. Discover how to use jq, a JSON manipulation command line, with GeoJSON
    https://webgeodatavore.com/jq-json-manipulation-command-line-with-geojson.html
  12. Reshaping JSON with jq
    https://programminghistori­an.org/en/lessons/json-and-jq
  13. Python bindings for jq
    https://pypi.org/project/jq/
  14. edn
    https://github.com/edn-format/edn
  15. Why use JSON over XML?
    https://www.sitepoint.com/json-vs-xml/
  16. XML and XPath
    https://www.w3schools.com/XML/xml_xpat­h.asp
  17. XPath (Wikipedia)
    https://en.wikipedia.org/wiki/XPath
  18. RFC7159
    https://www.ietf.org/rfc/rfc7159.txt

Autor článku

Pavel Tišnovský vystudoval VUT FIT a v současné době pracuje ve společnosti Red Hat, kde vyvíjí nástroje pro OpenShift.io.