Zpracování dat reprezentovaných ve formátu JSON nástrojem jq

6. 8. 2020
Doba čtení: 22 minut

Sdílet

 Autor: Pixabay
V dalším článku o užitečných utilitách určených pro příkazovou řádku se seznámíme s nástrojem nazvaným jq. Tento překvapivě mocný nástroj slouží k provádění různých operací nad daty uloženými ve formátu JSON.

Obsah

1. Zpracování dat reprezentovaných ve formátu JSON nástrojem jq

2. Instalace nástroje 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

8. Indexování prvků v polích

9. Řez polem

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

18. Obsah navazujícího článku

19. Odkazy na Internetu

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á.

Poznámka: pro jq vznikl i balíček určený pro programovací jazyk Python, který operace (a dotazovací jazyk) zpřístupňuje i skriptům naprogramovaným v Pythonu. I tímto balíčkem se budeme zabývat v závěrečné části dnešního článku.

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
Poznámka: tato starší verze Fedory obsahuje jq verze 1.5, kdežto nejnovější stabilní verze je 1.6.

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
Poznámka: přepínač -j8 si samozřejmě můžete upravit podle počtu dostupných jader, ovšem u tak (relativně) malého nástroje, jakým jq je, je překlad a slinkování záležitostí několika sekund i v případě použití starších mikroprocesorů.

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
Poznámka: výraz „tečka“ lze zapsat přímo, ovšem mnohé komplikovanější výrazy již obsahují znaky, které by byly expandovány vlastním shellem. Proto je vhodné složitější výrazy zapisovat do jednoduchých uvozovek, v nichž k žádné expanzi nedochází:
$ 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://programminghistori­an.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"]}
Poznámka: ve skutečnosti jsme takto ušetřili 129 znaků (bajtů).

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"
}
Poznámka: povšimněte si, že v tomto případě jq stále pracuje jako klasický filtr, protože vstupem byla data ve formátu JSON a výstupem jsou opět data ve formátu JSON.

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
Poznámka: výstup asi není zapotřebí znovu opisovat.

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ě:

bitcoin_skoleni

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

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

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.