Obsah
1. Zpracování dat reprezentovaných ve formátu JSON nástrojem jq
3. Nástroj jq ve funkci unixového filtru
4. Přeformátování souborů ve formátu JSON
5. Volby při formátování čitelných JSONů
6. Vytvoření kompaktního souboru ve formátu JSON
7. Základy dotazovacího jazyka
10. Složitější dotazy založené na zřetězení
11. Regulární výrazy v dotazovacím jazyku
12. Praktičtější ukázka – zpracování souborů openapi.json
13. Dotazy nad obsahem souborů openapi.json
14. Složitější dotazy obsahující volání funkcí nástroje jq, popř. podmínky
15. Práce s rozsáhlejšími JSON soubory
16. Balíček jq určený pro programovací jazyk Python
17. Základní použití balíčku jq
1. Zpracování dat reprezentovaných ve formátu JSON nástrojem jq
Dalším užitečným nástrojem určeným primárně pro volání z příkazové řádky, popř. ze shell skriptů je nástroj nazvaný jq. Tento nástroj dokáže zpracovávat data uložená ve formátu JSON, který je v současnosti využíván (a někdy též až neskutečným způsobem zneužíván) v mnoha oblastech výpočetní techniky. jq dokáže soubory JSON naformátovat, aby byly čitelné, naopak dokáže odstranit přebytečné bílé znaky, ale především dokáže nad daty vytvářet různé dotazy (queries), které mnohou být mnohdy i velmi komplikované. Zajímavé – a například z pohledu administrátorů i užitečné – je, že nástroj jq je vyvinut v programovacím jazyku C a po jeho překladu vznikne spustitelný soubor (taktéž nazvaný „jq“), který pro svůj běh vyžaduje pouze základní glibc. Díky tomu se značným způsobem zjednodušuje instalace, nehledě na to, že start i práce jq je velmi rychlá.
2. Instalace nástroje jq
Nástroj jq je možné stáhnout v jeho spustitelné podobě ze stránky https://stedolan.github.io/jq/. Postačuje pouze vybrat si správnou architekturu. Získaný spustitelný soubor závisí, jak jsme se již ostatně řekli v úvodní kapitole, pouze na glibc. Samozřejmě je také možné použít správce balíčků vaší distribuce Linuxu, což se týká například Fedory:
$ 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
A pochopitelně je možné zvolit i překlad jq přímo ze zdrojových kódů. Jedná se o pouhých několik kroků:
$ git@github.com:stedolan/jq.git $ cd jq $ autoreconf -fi $ ./configure --with-oniguruma=builtin $ make -j8 $ make check
3. Nástroj jq ve funkci unixového filtru
jq je pojat jako klasický unixový filtr, tj. vstupní data (ve formátu JSON) dokáže načíst ze standardního vstupu a výsledek můžeme přesměrovat do standardního výstupu. Ovšem navíc je nutné specifikovat operaci, která se má provést. Základní operací je naformátování JSONu, což se provede výrazem zapisovaným jako tečka:
$ cat openapi.json | jq . > new.json
nebo:
$ jq . < openapi.json > new.json
popř. lze namísto přesměrování přímo uvést jméno vstupního souboru nebo souborů:
$ jq . openapi.json > new.json
$ cat openapi.json | jq '.' > new.json $ jq '.' < openapi.json > new.json $ jq '.' openapi.json > new.json
4. Přeformátování souborů ve formátu JSON
Soubory obsahující data ve formátu JSON jsou vytvářeny různými nástroji a mnohdy se jedná o soubory určené spíše pro strojové zpracování, z nichž jsou odstraněny bílé znaky použité pouze pro odsazení a naformátování souboru za účelem jeho větší čitelnosti. Nástroj jq dokáže i takové soubory přeformátovat do čitelnější podoby nebo naopak dokáže bílé znaky ze souboru odstranit pro zmenšení jeho velikosti. Pro otestování této funkcionality použijeme demonstrační příklad ukázaný na stránce https://programminghistorian.org/en/lessons/json-and-jq. Soubor ve formátu JSON, který budeme testovat, vypadá následovně. Uložte si ho pod jménem nightwatch.json:
{ "links": { "self": "https://www.rijksmuseum.nl/api/nl/collection/SK-C-5", "web": "https://www.rijksmuseum.nl/nl/collectie/SK-C-5" }, "id": "nl-SK-C-5", "objectNumber": "SK-C-5", "title": "Schutters van wijk II onder leiding van kapitein Frans Banninck Cocq, bekend als de ‘Nachtwacht’", "hasImage": true, "principalOrFirstMaker": "Rembrandt Harmensz. van Rijn", "longTitle": "Schutters van wijk II onder leiding van kapitein Frans Banninck Cocq, bekend als de ‘Nachtwacht’, Rembrandt Harmensz. van Rijn, 1642", "showImage": true, "permitDownload": true, "webImage": { "guid": "3ae88fe0-021c-41ae-a4ce-cc70b7bc6295", "offsetPercentageX": 50, "offsetPercentageY": 100, "width": 2500, "height": 2034, "url": "http://lh6.ggpht.com/ZYWwML8mVFonXzbmg2rQBulNuCSr3rAaf5ppNcUc2Id8qXqudDL1NSYxaqjEXyDLSbeNFzOHRu0H7rbIws0Js4d7s_M=s0" }, "headerImage": { "guid": "29a2a516-f1d2-4713-9cbd-7a4458026057", "offsetPercentageX": 50, "offsetPercentageY": 50, "width": 1920, "height": 460, "url": "http://lh3.ggpht.com/rvCc4t2BWHAgDlzyiPlp1sBhc8ju0aSsu2HxR8rN_ZVPBcujP94pukbmF3Blmhi-GW5cx1_YsYYCDMTPePocwM6d2vk=s0" }, "productionPlaces": [ "Amsterdam", "Root" ] }
Pokud použijeme výraz „tečka“, bude vstupní soubor načten, deserializován a vypsán zpět v naformátované podobě. Ta by měla být prakticky shodná se vstupem, který již naformátovaný je:
$ jq . nightwatch.json { "links": { "self": "https://www.rijksmuseum.nl/api/nl/collection/SK-C-5", "web": "https://www.rijksmuseum.nl/nl/collectie/SK-C-5" }, "id": "nl-SK-C-5", "objectNumber": "SK-C-5", "title": "Schutters van wijk II onder leiding van kapitein Frans Banninck Cocq, bekend als de ‘Nachtwacht’", "hasImage": true, "principalOrFirstMaker": "Rembrandt Harmensz. van Rijn", "longTitle": "Schutters van wijk II onder leiding van kapitein Frans Banninck Cocq, bekend als de ‘Nachtwacht’, Rembrandt Harmensz. van Rijn, 1642", "showImage": true, "permitDownload": true, "webImage": { "guid": "3ae88fe0-021c-41ae-a4ce-cc70b7bc6295", "offsetPercentageX": 50, "offsetPercentageY": 100, "width": 2500, "height": 2034, "url": "http://lh6.ggpht.com/ZYWwML8mVFonXzbmg2rQBulNuCSr3rAaf5ppNcUc2Id8qXqudDL1NSYxaqjEXyDLSbeNFzOHRu0H7rbIws0Js4d7s_M=s0" }, "headerImage": { "guid": "29a2a516-f1d2-4713-9cbd-7a4458026057", "offsetPercentageX": 50, "offsetPercentageY": 50, "width": 1920, "height": 460, "url": "http://lh3.ggpht.com/rvCc4t2BWHAgDlzyiPlp1sBhc8ju0aSsu2HxR8rN_ZVPBcujP94pukbmF3Blmhi-GW5cx1_YsYYCDMTPePocwM6d2vk=s0" }, "productionPlaces": [ "Amsterdam", "Root" ] }
Ve skutečnosti je výstup na terminál obarven:
Obrázek 1: Naformátovaný a obarvený výstup z nástroje jq.
Nyní se můžeme pokusit o napodobení činnosti nástrojů, které produkují „nečitelné“ JSONy. Ze začátků řádků odstraníme bílé znaky a následně pospojujeme všechny řádky dohromady. To lze zařídit například tímto příkazem, v nichž se používají filtry sed a paste:
$ sed 's/^[ ]*//g' nightwatch.json | paste -s --delimiters="" > ugly.json
Výsledek bude vypadat následovně:
{"links": {"self": "https://www.rijksmuseum.nl/api/nl/collection/SK-C-5","web": "https://www.rijksmuseum.nl/nl/collectie/SK-C-5"},"id": "nl-SK-C-5","objectNumb er": "SK-C-5","title": "Schutters van wijk II onder leiding van kapitein Frans Banninck Cocq, bekend als de ‘Nachtwacht’","hasImage": true,"principalOrFirstM aker": "Rembrandt Harmensz. van Rijn","longTitle": "Schutters van wijk II onde r leiding van kapitein Frans Banninck Cocq, bekend als de ‘Nachtwacht’, Rembra ndt Harmensz. van Rijn, 1642","showImage": true,"permitDownload": true,"webIma ge": {"guid": "3ae88fe0-021c-41ae-a4ce-cc70b7bc6295","offsetPercentageX": 50," offsetPercentageY": 100,"width": 2500,"height": 2034,"url": "http://lh6.ggpht. com/ZYWwML8mVFonXzbmg2rQBulNuCSr3rAaf5ppNcUc2Id8qXqudDL1NSYxaqjEXyDLSbeNFzOHRu 0H7rbIws0Js4d7s_M=s0"},"headerImage": {"guid": "29a2a516-f1d2-4713-9cbd-7a4458 026057","offsetPercentageX": 50,"offsetPercentageY": 50,"width": 1920,"height" : 460,"url": "http://lh3.ggpht.com/rvCc4t2BWHAgDlzyiPlp1sBhc8ju0aSsu2HxR8rN_ZV PBcujP94pukbmF3Blmhi-GW5cx1_YsYYCDMTPePocwM6d2vk=s0"},"productionPlaces": ["Am sterdam","Root"]}
Původní naformátovaný soubor získáme z „mimifikovaného“ souboru ugly.json právě pomocí nástroje jq:
$ jq . ugly.json { "links": { "self": "https://www.rijksmuseum.nl/api/nl/collection/SK-C-5", "web": "https://www.rijksmuseum.nl/nl/collectie/SK-C-5" }, "id": "nl-SK-C-5", "objectNumber": "SK-C-5", "title": "Schutters van wijk II onder leiding van kapitein Frans Banninck Cocq, bekend als de ‘Nachtwacht’", "hasImage": true, "principalOrFirstMaker": "Rembrandt Harmensz. van Rijn", "longTitle": "Schutters van wijk II onder leiding van kapitein Frans Banninck Cocq, bekend als de ‘Nachtwacht’, Rembrandt Harmensz. van Rijn, 1642", "showImage": true, "permitDownload": true, "webImage": { "guid": "3ae88fe0-021c-41ae-a4ce-cc70b7bc6295", "offsetPercentageX": 50, "offsetPercentageY": 100, "width": 2500, "height": 2034, "url": "http://lh6.ggpht.com/ZYWwML8mVFonXzbmg2rQBulNuCSr3rAaf5ppNcUc2Id8qXqudDL1NSYxaqjEXyDLSbeNFzOHRu0H7rbIws0Js4d7s_M=s0" }, "headerImage": { "guid": "29a2a516-f1d2-4713-9cbd-7a4458026057", "offsetPercentageX": 50, "offsetPercentageY": 50, "width": 1920, "height": 460, "url": "http://lh3.ggpht.com/rvCc4t2BWHAgDlzyiPlp1sBhc8ju0aSsu2HxR8rN_ZVPBcujP94pukbmF3Blmhi-GW5cx1_YsYYCDMTPePocwM6d2vk=s0" }, "productionPlaces": [ "Amsterdam", "Root" ] }
5. Volby při formátování čitelných JSONů
Při formátování JSONů je možné namísto mezer použít znaky tabulátoru:
$ jq --tab . ugly.json { "links": { "self": "https://www.rijksmuseum.nl/api/nl/collection/SK-C-5", "web": "https://www.rijksmuseum.nl/nl/collectie/SK-C-5" }, "id": "nl-SK-C-5", "objectNumber": "SK-C-5", "title": "Schutters van wijk II onder leiding van kapitein Frans Banninck Cocq, bekend als de ‘Nachtwacht’", "hasImage": true, "principalOrFirstMaker": "Rembrandt Harmensz. van Rijn", "longTitle": "Schutters van wijk II onder leiding van kapitein Frans Banninck Cocq, bekend als de ‘Nachtwacht’, Rembrandt Harmensz. van Rijn, 1642", "showImage": true, "permitDownload": true, "webImage": { "guid": "3ae88fe0-021c-41ae-a4ce-cc70b7bc6295", "offsetPercentageX": 50, "offsetPercentageY": 100, "width": 2500, "height": 2034, "url": "http://lh6.ggpht.com/ZYWwML8mVFonXzbmg2rQBulNuCSr3rAaf5ppNcUc2Id8qXqudDL1NSYxaqjEXyDLSbeNFzOHRu0H7rbIws0Js4d7s_M=s0" }, "headerImage": { "guid": "29a2a516-f1d2-4713-9cbd-7a4458026057", "offsetPercentageX": 50, "offsetPercentageY": 50, "width": 1920, "height": 460, "url": "http://lh3.ggpht.com/rvCc4t2BWHAgDlzyiPlp1sBhc8ju0aSsu2HxR8rN_ZVPBcujP94pukbmF3Blmhi-GW5cx1_YsYYCDMTPePocwM6d2vk=s0" }, "productionPlaces": [ "Amsterdam", "Root" ] }
Pokud přesto preferujete používání mezer pro odsazení, lze zvolit jejich počet v rozsahu 1 až 7. Typické bývá použití čtyř mezer:
$ jq --indent 4 . ugly.json { "links": { "self": "https://www.rijksmuseum.nl/api/nl/collection/SK-C-5", "web": "https://www.rijksmuseum.nl/nl/collectie/SK-C-5" }, "id": "nl-SK-C-5", "objectNumber": "SK-C-5", "title": "Schutters van wijk II onder leiding van kapitein Frans Banninck Cocq, bekend als de ‘Nachtwacht’", "hasImage": true, "principalOrFirstMaker": "Rembrandt Harmensz. van Rijn", "longTitle": "Schutters van wijk II onder leiding van kapitein Frans Banninck Cocq, bekend als de ‘Nachtwacht’, Rembrandt Harmensz. van Rijn, 1642", "showImage": true, "permitDownload": true, "webImage": { "guid": "3ae88fe0-021c-41ae-a4ce-cc70b7bc6295", "offsetPercentageX": 50, "offsetPercentageY": 100, "width": 2500, "height": 2034, "url": "http://lh6.ggpht.com/ZYWwML8mVFonXzbmg2rQBulNuCSr3rAaf5ppNcUc2Id8qXqudDL1NSYxaqjEXyDLSbeNFzOHRu0H7rbIws0Js4d7s_M=s0" }, "headerImage": { "guid": "29a2a516-f1d2-4713-9cbd-7a4458026057", "offsetPercentageX": 50, "offsetPercentageY": 50, "width": 1920, "height": 460, "url": "http://lh3.ggpht.com/rvCc4t2BWHAgDlzyiPlp1sBhc8ju0aSsu2HxR8rN_ZVPBcujP94pukbmF3Blmhi-GW5cx1_YsYYCDMTPePocwM6d2vk=s0" }, "productionPlaces": [ "Amsterdam", "Root" ] }
6. Vytvoření kompaktního souboru ve formátu JSON
Opakem formátování s účelem získání co nejčitelnějšího souboru ve formátu JSON je vytvoření co nejkompaktnějšího souboru, z něhož jsou odstraněny všechny přebytečné znaky. Již ve čtvrté kapitole jsme si ukázali poměrně nešikovný způsob, ovšem stejnou operaci stejně dobře (ve skutečnosti mnohem lépe) dokáže provést nástroj jq při použití volby -c, která znamená „compact“:
$ jq -c . nightwatch.json {"links":{"self":"https://www.rijksmuseum.nl/api/nl/collection/SK-C-5","web":"h ttps://www.rijksmuseum.nl/nl/collectie/SK-C-5"},"id":"nl-SK-C-5","objectNumber" :"SK-C-5","title":"Schutters van wijk II onder leiding van kapitein Frans Banni nck Cocq, bekend als de ‘Nachtwacht’","hasImage":true,"principalOrFirstMaker":" Rembrandt Harmensz. van Rijn","longTitle":"Schutters van wijk II onder leiding van kapitein Frans Banninck Cocq, bekend als de ‘Nachtwacht’, Rembrandt Harmens z. van Rijn, 1642","showImage":true,"permitDownload":true,"webImage":{"guid":"3 ae88fe0-021c-41ae-a4ce-cc70b7bc6295","offsetPercentageX":50,"offsetPercentageY" :100,"width":2500,"height":2034,"url":"http://lh6.ggpht.com/ZYWwML8mVFonXzbmg2r QBulNuCSr3rAaf5ppNcUc2Id8qXqudDL1NSYxaqjEXyDLSbeNFzOHRu0H7rbIws0Js4d7s_M=s0"}," headerImage":{"guid":"29a2a516-f1d2-4713-9cbd-7a4458026057","offsetPercentageX" :50,"offsetPercentageY":50,"width":1920,"height":460,"url":"http://lh3.ggpht.co m/rvCc4t2BWHAgDlzyiPlp1sBhc8ju0aSsu2HxR8rN_ZVPBcujP94pukbmF3Blmhi-GW5cx1_YsYYCD MTPePocwM6d2vk=s0"},"productionPlaces":["Amsterdam","Root"]}
7. Základy dotazovacího jazyka
Ve skutečnosti má nástroj jq mnohem širší oblast použití, než pouhé přeformátování souborů obsahujících data ve formátu JSON. Důležitý je jeho dotazovací jazyk, v němž je možné s využitím výrazu nebo kombinací více výrazů přečíst konkrétní informace, které se v JSONu nachází. V tomto ohledu jsou možnosti jq a jeho jazyka alespoň teoreticky podobné jazyku XPath (i když XPath je mocnější).
Pokud se podíváme na soubor nightwatch.json uvedený ve čtvrté kapitole, zjistíme, že se jedná o slovník, který kromě jiného obsahuje i vnořený slovník uložený pod klíčem links. Jak lze získat obsah tohoto slovníku? Použijeme výraz .links, který lze považovat za formu selektoru:
$ jq .links nightwatch.json { "self": "https://www.rijksmuseum.nl/api/nl/collection/SK-C-5", "web": "https://www.rijksmuseum.nl/nl/collectie/SK-C-5" }
Můžeme se pokusit získat i hodnotu konkrétní dvojice web, která se nachází ve slovníku links. Výsledkem tentokrát bude řetězec, což sice podle stránky json.org není validní JSON, ovšem jedná se o zcela korektní reprezentaci objektu typu string (a podle RFC7159 se jedná o validní JSON):
$ jq .links.web nightwatch.json "https://www.rijksmuseum.nl/nl/collectie/SK-C-5"
Pokud hodnota neexistuje (selektor pro daný vstup není korektní), nahlásí se chyba:
$ jq .links.web.foo nightwatch.json jq: error (at nightwatch.json:34): Cannot index string with string "foo"
Někdy nám toto chování nemusí vyhovovat. Poté postačuje za selektorem použít znak ? (otazník):
$ jq .links.web.foo? nightwatch.json
Zajímavá situace nastane, pokud se pokusíme získat hodnotu typu pole. V tomto případě se vrátí obsah pole zapsaný tak, že se opět jedná o validní JSON:
$ jq .productionPlaces nightwatch.json [ "Amsterdam", "Root" ]
Někdy však potřebujeme obsah pole zpracovat jinými nástroji (řekněme sort). V tomto případě použijte poněkud upravený příkaz obsahující prázdné hranaté závorky:
$ jq .productionPlaces[] nightwatch.json "Amsterdam" "Root"
Nebo ještě lépe bez uložení řetězců do uvozovek:
$ jq -r .productionPlaces[] nightwatch.json Amsterdam Root
8. Indexování prvků v polích
Pokud je výsledkem nějakého dotazu pole (což je případ z předchozí kapitoly), lze do hranatých závorek napsat index prvku, který nás zajímá. Indexuje se od nuly, takže pro získání hodnoty prvního prvku použijeme:
$ jq .productionPlaces[0] nightwatch.json "Amsterdam" $ jq -r .productionPlaces[0] nightwatch.json Amsterdam
Podobně pro získání prvku druhého:
$ jq .productionPlaces[1] nightwatch.json "Root" $ jq -r .productionPlaces[1] nightwatch.json Root
Pokud prvek s daným indexem neexistuje, vrátí se hodnota null (což není řetězec – není vytištěna v uvozovkách):
$ jq .productionPlaces[2] nightwatch.json null
Použít lze i záporné indexy – v tomto případě se index prvku počítá od konce pole a nikoli od jeho začátku:
$ jq .productionPlaces?[-1] nightwatch.json "Root"
popř.:
$ jq .productionPlaces?[-2] nightwatch.json "Amsterdam"
9. Řez polem
Nyní si vstupní soubor nightwatch.json upravíme takovým způsobem, aby pole uložené pod klíčem productionPlaces obsahovalo namísto dvou prvků prvků pět (viz též zvýrazněná část):
{ "links": { "self": "https://www.rijksmuseum.nl/api/nl/collection/SK-C-5", "web": "https://www.rijksmuseum.nl/nl/collectie/SK-C-5" }, "id": "nl-SK-C-5", "objectNumber": "SK-C-5", "title": "Schutters van wijk II onder leiding van kapitein Frans Banninck Cocq, bekend als de ‘Nachtwacht’", "hasImage": true, "principalOrFirstMaker": "Rembrandt Harmensz. van Rijn", "longTitle": "Schutters van wijk II onder leiding van kapitein Frans Banninck Cocq, bekend als de ‘Nachtwacht’, Rembrandt Harmensz. van Rijn, 1642", "showImage": true, "permitDownload": true, "webImage": { "guid": "3ae88fe0-021c-41ae-a4ce-cc70b7bc6295", "offsetPercentageX": 50, "offsetPercentageY": 100, "width": 2500, "height": 2034, "url": "http://lh6.ggpht.com/ZYWwML8mVFonXzbmg2rQBulNuCSr3rAaf5ppNcUc2Id8qXqudDL1NSYxaqjEXyDLSbeNFzOHRu0H7rbIws0Js4d7s_M=s0" }, "headerImage": { "guid": "29a2a516-f1d2-4713-9cbd-7a4458026057", "offsetPercentageX": 50, "offsetPercentageY": 50, "width": 1920, "height": 460, "url": "http://lh3.ggpht.com/rvCc4t2BWHAgDlzyiPlp1sBhc8ju0aSsu2HxR8rN_ZVPBcujP94pukbmF3Blmhi-GW5cx1_YsYYCDMTPePocwM6d2vk=s0" }, "productionPlaces": [ "Amsterdam", "Root", "Brno", "Prague", "Znojmo" ] }
Na pětiprvkovém poli si již můžeme snadno vyzkoušet, jak se získá řez polem. Jedná se o dnes již klasickou operaci, kterou nalezneme například v Pythonu nebo v programovacím jazyku Go. Je nutné specifikovat první prvek řezu a prvek, který se nachází za druhým koncem řezu.
Prvky s indexy 0 a 1:
$ jq .productionPlaces[0:2] nightwatch.json [ "Amsterdam", "Root" ]
Prvky s indexy 2 a 3:
$ jq .productionPlaces[2:4] nightwatch.json [ "Brno", "Prague" ]
Horní index může přesahovat meze pole, aniž by se jednalo o hlášenou chybu:
$ jq .productionPlaces[0:10] nightwatch.json [ "Amsterdam", "Root", "Brno", "Prague", "Znojmo" ]
Prvky 2, 3 … až předposlední prvek pole:
$ jq .productionPlaces[2:-1] nightwatch.json [ "Brno", "Prague" ]
Vynecháme spodní index (bude dosazena nula):
$ jq .productionPlaces[:-1] nightwatch.json [ "Amsterdam", "Root", "Brno", "Prague" ]
Vynecháme horní index (použijí je všechny prvky až do konce pole):
$ jq .productionPlaces[0:] nightwatch.json [ "Amsterdam", "Root", "Brno", "Prague", "Znojmo" ]
Ovšem pozor – oba indexy vynechat nelze (na rozdíl od některých dalších jazyků):
$ jq .productionPlaces[:] nightwatch.json jq: error: syntax error, unexpected ']' (Unix shell quoting issues?) at <top-level>, line 1: .productionPlaces[:] jq: 1 compile error
10. Složitější dotazy založené na zřetězení
Možnosti nabízené dotazovacím jazykem jq jsou ovšem ještě mnohem širší. Například existuje možnost s využitím znaku | („kolona“) zřetězit několik operací za sebe. Pokud totiž zadáme příkaz:
$ jq '.productionPlaces[]' nightwatch.json "Amsterdam" "Root" "Brno" "Prague" "Znojmo"
…znamená to, že se na výstupu vytvoří pětice nových JSON objektů, které v tomto konkrétním případě reprezentují řetězce. A na tuto pětici objektů lze aplikovat další operaci, například filtraci, která vrátí pouze ty řetězce, které jsou delší než pět znaků:
$ jq '.productionPlaces[] | select(. | length >= 5)' nightwatch.json "Amsterdam" "Prague" "Znojmo"
Podobně je možné získat jen ta místa, která ve svém jménu obsahují znak „o“:
$ jq '.productionPlaces[] | select(. | contains("o"))' nightwatch.json "Root" "Brno" "Znojmo"
Popř. ta místa, která začínají znakem „A“ nebo „Z“:
$ jq '.productionPlaces[] | select(. | startswith("A"))' nightwatch.json "Amsterdam" $ jq '.productionPlaces[] | select(. | startswith("Z"))' nightwatch.json "Znojmo"
11. Regulární výrazy v dotazovacím jazyku
V dotazovacím jazyku nástroje jq lze použít i regulární výrazy, které jsou zapisovány v predikátu (funkci vracející pravdivostní hodnotu) nazvaném test. Podívejme se nyní na několik velmi jednoduchých příkladů, v nichž budou regulární výrazy použity.
Jakékoli místo, v jehož jménu se (kdekoli) nachází znak „t“:
$ jq '.productionPlaces[] | select(. | test("t"))' nightwatch.json "Amsterdam" "Root"
Jakékoli místo končící znakem „t“:
$ jq '.productionPlaces[] | select(. | test("t$"))' nightwatch.json "Root"
Jakékoli místo začínající znaky „A“, „B“ nebo „Z“:
$ jq '.productionPlaces[] | select(. | test("^[ABZ]"))' nightwatch.json "Amsterdam" "Brno" "Znojmo"
Místo obsahující znak „o“, ovšem nepočítá se „o“ na konci:
$ jq '.productionPlaces[] | select(. | test("o."))' nightwatch.json "Root" "Znojmo"
12. Praktičtější ukázka – zpracování souborů openapi.json
V navazujících dvou kapitolách si ukážeme praktické použití nástroje jq, a to konkrétně při zpracování souborů openapi.json, které obsahují popis REST API nějaké webové služby. Konkrétně použijeme následující obsah, jenž vznikl zkrácením reálného souboru openapi.json. V popisu REST API je uvedeno několik koncových bodů, každý s různými HTTP metodami:
{ "openapi": "3.0.0", "servers": [ { "url": "" } ], "info": { "description": "A very simple REST API service", "version": "1.0.0", "title": "REST API Service", "termsOfService": "", "contact": { "name": "Pavel Tisnovsky" }, "license": { "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" } }, "tags": [], "paths": { "/": { "get": { "summary": "Returns valid HTTP 200 ok status when the service is ready", "description": "", "parameters": [], "operationId": "main", "responses": { "default": { "description": "Default responsepaste -s --delimiters="" " } } } }, "/client/cluster": { "x-temp": { "summary": "Read list of all clusters from database and return it to a client", "description": "", "parameters": [], "operationId": "getClusters", "responses": { "default": { "description": "Default response" } } }, "get": { "summary": "Read list of all clusters from database and return it to a client", "description": "", "parameters": [], "operationId": "getClusters", "responses": { "default": { "description": "Default response" } } } }, "/client/cluster/{name}": { "get": { "summary": "Read cluster specified by its ID and return it to a client", "description": "", "parameters": [], "operationId": "getClusterById", "responses": { "default": { "description": "Default response" } } }, "post": { "summary": "Create a record with new cluster in a database. The updated list of all clusters is returned to client", "description": "", "parameters": [], "operationId": "newCluster", "responses": { "default": { "description": "Default response" } } }, "delete": { "summary": "Delete a cluster specified by its ID", "description": "", "parameters": [], "operationId": "deleteCluster", "responses": { "default": { "description": "Default response" } } } }, "/client/cluster/search": { "get": { "summary": "Search for a cluster specified by its ID or name", "description": "", "parameters": [ { "name": "id", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Cluster ID", "allowEmptyValue": true }, { "name": "name", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Cluster name", "allowEmptyValue": true } ], "operationId": "searchCluster", "responses": { "default": { "description": "Default response" } } } } }, "externalDocs": { "description": "Please see foo bar baz", "url": "https://godoc.org/..." }, "security": [] }
13. Dotazy nad obsahem souborů openapi.json
Nejdříve si můžeme celý vstupní soubor nechat přeformátovat:
$ jq . openapi.json
Získáme informaci o licenci, pod kterou je soubor vydán:
$ jq .info.license.name openapi.json "Apache 2.0"
Získáme souhrnné popisy všech endpointů s HTTP požadavkem typu GET:
$ jq ".paths[] | .get.summary" openapi.json "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):
$ jq ".paths[] | .delete.summary" openapi.json null null "Delete a cluster specified by its ID" null
Ve složitějších dotazech, kdy se například dotazujeme na uzel, jehož klíč obsahuje lomítka (nebo jiné speciální znaky) je nutné samotnou cestu umístit do uvozovek. Ovšem i samotný dotaz je v uvozovkách, proto je nutné před vnitřní uvozovky vložit znak \. Náhrada je provedena již shellem, nikoli nástrojem jq:
$ jq ".paths.\"/client/cluster/search\"" openapi.json { "get": { "summary": "Search for a cluster specified by its ID or name", "description": "", "parameters": [ { "name": "id", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Cluster ID", "allowEmptyValue": true }, { "name": "name", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Cluster name", "allowEmptyValue": true } ], "operationId": "searchCluster", "responses": { "default": { "description": "Default response" } } } }
Alternativou je použití jednoduchých lomítek:
$ jq '.paths."/client/cluster/search"' openapi.json ... ... ...
14. Složitější dotazy obsahující volání funkcí nástroje jq, popř. podmínky
Dotaz, zda endpointy podporují HTTP metodu GET:
$ jq '.paths[] | has("get")' openapi.json true true true true
Dotaz, zda endpointy podporují HTTP metodu DELETE:
$ jq '.paths[] | has("delete")' openapi.json false false true false
A konečně test, jestli popis endpointů neobsahuje prázdný řetězec:
$ jq '.paths[] | .get.summary | if (. | length) > 0 then "ok" else "empty" end ' openapi.json "ok" "ok" "ok" "ok"
Dotaz na popis parametrů endpointů podporujících HTTP metodu GET:
$ jq ".paths.\"/client/cluster/search\".get.parameters" openapi.json [ { "name": "id", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Cluster ID", "allowEmptyValue": true }, { "name": "name", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Cluster name", "allowEmptyValue": true } ]
15. Práce s rozsáhlejšími JSON soubory
Nástroj jq je relativně výkonný i při práci s rozsáhlejšími JSON soubory. Můžeme si to otestovat například na souboru countries.geojson s velikostí přibližně 674kB (což je, pravda, soubor s řekněme průměrnou délkou – existují totiž i JSONy o velikosti desítek megabajtů). Tento soubor lze stáhnout následujícím příkazem:
$ wget https://raw.github.com/datasets/geo-boundaries-world-110m/master/countries.geojson
Zjistíme, zda byl soubor skutečně stažen:
$ file countries.geojson countries.geojson: UTF-8 Unicode text, with very long lines
Přístup k atributu „type“:
$ jq .type countries.geojson "FeatureCollection"
Přístup k poli uloženém pod atributem „features“, získání druhého prvku z tohoto pole:
$ jq .features[1] countries.geojson ... ... ...
Přístup ke konkrétnímu bodu na Zeměkouli:
$ jq .features[0].geometry.coordinates[0][0] countries.geojson [ 61.210817091725744, 35.650072333309225 ]
Dtto, ale výsledkem nebude pole (reprezentované v JSONu), ale sekvence jednotlivých numerických hodnot:
$ jq .features[0].geometry.coordinates[0][0][] countries.geojson 61.210817091725744 35.650072333309225
Souřadnice je možné prohodit, pokud je to z nějakého důvodu nutné:
$ jq .features[0].geometry.coordinates[0][0][0,1] countries.geojson 61.210817091725744 35.650072333309225 $ jq .features[0].geometry.coordinates[0][0][1,0] countries.geojson 61.210817091725744 35.650072333309225
Dtto, ale výběr na nejvyšší úrovni:
$ jq .features[0,1].geometry.coordinates[0][0][0,1] countries.geojson 61.210817091725744 [ 16.326528354567046, -5.877470391466218 ] 35.650072333309225 [ 16.573179965896145, -6.622644545115094 ]
Dtto, ale výběr na nejvyšší úrovni:
$ jq .features[0,1,5,10].geometry.coordinates[0][0][0,1] countries.geojson 61.210817091725744 [ 16.326528354567046, -5.877470391466218 ] 43.58274580259273 [ 45.0019873390568, 39.7400035670496 ] 35.650072333309225 [ 16.573179965896145, -6.622644545115094 ] 41.09214325618257 [ 45.29814497252144, 39.471751207022436 ]
Výpis jmen všech států:
$ jq .features[].properties.name_long countries.geojson "Afghanistan" "Angola" "Albania" "United Arab Emirates" ... ... ... "Yemen" "South Africa" "Zambia" "Zimbabwe"
Výběr dvou atributů – kontinentu a názvu státu:
$ jq '.features[].properties | {continent, name_long}' countries.geojson { "continent": "Asia", "name_long": "Afghanistan" } { "continent": "Africa", "name_long": "Angola" } { "continent": "Europe", "name_long": "Albania" } ... ... ... { "continent": "Africa", "name_long": "South Africa" } { "continent": "Africa", "name_long": "Zambia" } { "continent": "Africa", "name_long": "Zimbabwe" }
16. Balíček jq určený pro programovací jazyk Python
Jak jsme si již řekli v úvodní kapitole, existuje pro programovací jazyk Python balíček pojmenovaný taktéž jq, který 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
Úspěšnost instalace tohoto balíčku lze snadno otestovat:
$ python3 Python 3.6.6 (default, Jul 19 2018, 16:29:00) [GCC 7.3.1 20180303 (Red Hat 7.3.1-5)] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import jq >>> help(jq)
Poslední příkaz by měl zobrazit nápovědu k balíčku jq:
Help on module jq: NAME jq FUNCTIONS all(...) compile(...) first(...) iter(...) jq(...) text(...) DATA __test__ = {} FILE
17. Základní použití balíčku jq
Balíček jq pro Python dokáže zpracovávat jak textový obsah (tedy řetězec obsahující data ve formátu JSON), tak i data předzpracovaná pomocí balíčku json. Podívejme se nejdříve na první případ, tedy zpracování textového obsahu:
import jq with open("nightwatch.json") as fin: content = fin.read() print(jq.compile(".links.self").input(text=content).text())
Obsah souboru je taktéž možné načíst a zpracovat balíčkem json. Potom vypadá volání funkcí z balíčku jq odlišně:
import jq import json with open("nightwatch.json") as fin: content = json.load(fin) print(jq.compile(".links.self").input(content).text())
V posledním demonstračním příkladu je ukázáno, že pokud nepoužijeme na konci volání metody text(), ale all(), vrátí se zpracovaný obsah ve formě pole nebo slovníku:
import jq import json with open("nightwatch.json") as fin: content = json.load(fin) print(jq.compile(".links").input(content).all())
18. Obsah navazujícího článku
Balíček jq pro programovací jazyk Python je stejně užitečný jako samotný nástroj jq, takže se mu budeme podrobněji věnovat v samostatném článku.
19. Odkazy na Internetu
- Repositář projektu jq (GitHub)
https://github.com/stedolan/jq - GitHub stránky projektu jq
https://stedolan.github.io/jq/ - 5 modern alternatives to essential Linux command-line tools
https://opensource.com/article/20/6/modern-linux-command-line-tools - Návod k nástroji jq
https://stedolan.github.io/jq/tutorial/ - jq Manual (development version)
https://stedolan.github.io/jq/manual/ - Introducing JSON
https://www.json.org/json-en.html - jq.py: a lightweight and flexible JSON processor
https://github.com/mwilliamson/jq.py - Discover how to use jq, a JSON manipulation command line, with GeoJSON
https://webgeodatavore.com/jq-json-manipulation-command-line-with-geojson.html - Reshaping JSON with jq
https://programminghistorian.org/en/lessons/json-and-jq - Python bindings for jq
https://pypi.org/project/jq/ - edn
https://github.com/edn-format/edn - Why use JSON over XML?
https://www.sitepoint.com/json-vs-xml/ - XML and XPath
https://www.w3schools.com/XML/xml_xpath.asp - XPath (Wikipedia)
https://en.wikipedia.org/wiki/XPath - RFC7159
https://www.ietf.org/rfc/rfc7159.txt