JSON-Dokumente sind heute allgegenwärtig, aber sie sind selten so strukturiert, wie Sie es möchten. Sie enthalten oft zu viele Daten, haben seltsam benannte Felder oder platzieren die Daten in unnötigen verschachtelten Objekten. Graph-Relational Object Queries (GROQ) ist eine Abfragesprache (ähnlich wie SQL, aber anders), die für die direkte Arbeit mit JSON-Dokumenten entwickelt wurde. Im Grunde können Sie damit Abfragen schreiben, um JSON-Dokumente schnell zu filtern und neu zu formatieren, um sie in die bequemste Form zu bringen.
GROQ wurde von Sanity.io entwickelt (wo es als primäre Abfragesprache verwendet wird). Es ist Open Source und bietet uns integrierte Möglichkeiten, es in JavaScript und der Kommandozeile für jede JSON-Quelle zu verwenden. Gemeinsam werden wir GROQ zum Terminal-Toolkit hinzufügen, was Ihnen Zeit spart, wann immer Sie JSON-Daten in einem Projekt bearbeiten müssen.
Lassen Sie uns GROQ installieren
Wie bei den meisten Dingen müssen wir das GROQ CLI-Tool installieren, was wir mit npm (oder Yarn) im Terminal tun können.
$ npm install -g groq-cli
Um damit zu spielen, benötigen wir eine JSON-Datei. Wir verwenden curl, um einen Beispieldatensatz von To-Do-Daten herunterzuladen.
$ curl -o todos.json https://jsonplaceholder.typicode.com/todos
Werfen wir einen kurzen Blick auf ein Beispielobjekt in den Daten.
{
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
},
Ziemlich unkompliziert. Wir haben eine Benutzer-ID, eine To-Do-Element-ID, einen To-Do-Titel und einen booleschen Wert, der angibt, ob das To-Do-Element abgeschlossen ist oder nicht.
Lassen Sie uns nun eine grundlegende GROQ-Abfrage ausführen: Finden Sie alle abgeschlossenen To-Dos, aber geben Sie nur die To-Do-Titel und Benutzer-IDs zurück. Es ist in Ordnung, diese Zeile zu kopieren/einzufügen, da wir sie gleich im Detail besprechen werden.
$ cat todos.json | groq '*[completed == true]{title, userId}' --pretty
Das groq-Kommandozeilen-Tool akzeptiert ein JSON-Dokument über Standardeingabe. Dies funktioniert sehr gut mit der Unix-Philosophie, "eine Sache zu tun und auf einem Textstrom zu arbeiten". Um JSON aus einer Datei zu lesen, verwenden wir den cat-Befehl. Beachten Sie auch, dass groq standardmäßig ein minimales JSON in einer einzelnen Zeile ausgibt, aber durch Übergabe von --pretty erhalten wir eine schön eingerückte und syntaktisch hervorgehobene Ausgabe.
Um das Ergebnis zu speichern, können wir es mit > in eine neue Datei umleiten.
$ cat todos.json | groq '*[completed == true]{title, userId}' > result.json
Die Abfrage selbst besteht aus drei Teilen.
*bezieht sich auf den Datensatz (d.h. die Daten in der JSON-Datei).[completed == true]ist ein Filter, der Elemente entfernt, die als unvollständig markiert sind.{title, userId}ist eine Projektion, die bewirkt, dass die Abfrage nur die Eigenschaften"title"und"userId"zurückgibt.
Lassen Sie uns mit einigen Übungen aufwärmen
Sie dachten wahrscheinlich nicht, dass Sie Übungen machen müssten, um diesen Beitrag zu verstehen! Nun, die gute Nachricht ist, dass wir nur den Verstand mit ein paar Dingen trainieren, die Sie mit GROQ ausprobieren können, bevor wir auf weitere Details eingehen.
- Was passiert, wenn Sie
[completed == true]und/oder{title, userId}entfernen? - Wie können Sie die Abfrage ändern, um alle To-Dos für den Benutzer mit der ID 2 zu finden?
- Wie können Sie die Abfrage ändern, um unvollständige To-Dos für den Benutzer mit der ID 2 zu finden?
- Was passiert, wenn der Filter in der ursprünglichen Abfragebeispiel die Plätze mit der Projektion tauscht?
- Wie würden Sie einen einzigen Befehl (mit Pipes) schreiben, der die JSON-Daten herunterlädt und mit GROQ verarbeitet?
Die Antworten werden wir am Ende des Beitrags für Sie bereitstellen, damit Sie sie nachschlagen können.
Abfrage der Nobelpreisträger
Die To-Do-Daten sind gut für ein Aufwärmen, aber seien wir ehrlich: Es ist nicht sehr motivierend, eine Liste mit lateinischem Platzhalterinhalt anzusehen. Der Nobelpreis bietet jedoch einen Datensatz aller bisherigen Preisträger, der öffentlich zugänglich ist.
Hier ist ein Beispiel der Rückgabe.
{
"laureates": [
{
"id": "1",
"firstname": "Wilhelm Conrad",
"surname": "Röntgen",
"born": "1845-03-27",
"died": "1923-02-10",
"bornCountry": "Prussia (now Germany)",
"bornCountryCode": "DE",
"bornCity": "Lennep (now Remscheid)",
"diedCountry": "Germany",
"diedCountryCode": "DE",
"diedCity": "Munich",
"gender": "male",
"prizes": [...],
},
// ...
]
}
Ah! Das ist schon viel interessanter! Laden wir den Datensatz herunter und suchen den Vornamen aller norwegischen Preisträger. Hier verwenden wir das --output-Flag für curl, um die Daten in einer Datei zu speichern.
$ curl --output laureate.json http://api.nobelprize.org/v1/laureate.json
$ cat laureate.json | groq '*.laureates[bornCountryCode == "NO"]{firstname}' --pretty
Was erhalten Sie zurück? Ich habe 12 norwegische Nobelpreisträger erhalten. Nicht schlecht!
Beachten Sie, dass diese Abfrage nicht ganz wie die erste Abfrage ist, die wir geschrieben haben. Wir haben hier ein zusätzliches .laureates. Als wir * im To-Do-Datensatz verwendeten, repräsentierte es das gesamte JSON-Dokument, das sich in einem Array auf der obersten Ebene des To-Do-Datensatzes befand. Auf der anderen Seite verwendet die Preisträgerdatei ein Objekt auf der obersten Ebene, in dem die Liste der Preisträger in der "laureates"-Eigenschaft gespeichert ist.
Um auf ein bestimmtes Element zuzugreifen, können wir den Filter [0] verwenden und nur den Vornamen zurückgeben. Das sollte uns sagen, wer der erste Norweger war, der einen Nobelpreis gewonnen hat.
$ cat laureate.json | groq '*.laureates[bornCountryCode == "NO"]{firstname}[0]' --pretty
// Returned object
{
"firstname": "Ivar"
}
Weitere Übungen!
Wir wären nachlässig, wenn wir nicht ein wenig mit diesem neuen Datensatz spielen würden, um zu sehen, wie die Abfragen funktionieren.
- Schreiben Sie eine Abfrage, um alle Nobelpreisträger aus Ihrem eigenen Land zu finden.
- Schreiben Sie eine Abfrage, um den letzten norwegischen Preisträger zurückzugeben. Tipp:
-1bezieht sich auf das letzte Element. - Was passiert, wenn Sie versuchen, direkt auf das Root-Objekt zu filtern?
*[bornCountryCode == "NO"]? - Was ist der Unterschied zwischen
*.laureates\[bornCountryCode == "NO"\][0]und*.laureates\[0\][bornCountryCode == "NO"]?
Wie beim letzten Mal finden Sie die Antworten am Ende dieses Beitrags.
Arbeiten mit Filtern
Wir wissen nun, dass es insgesamt 12 norwegische Nobelpreisträger gab, wie viele von ihnen wurden nach 1950 geboren? Das lässt sich mit GROQ problemlos herausfinden.
$ cat laureate.json | groq '*.laureates[bornCountryCode == "NO" && born >= "1950-01-01"]{firstname}' --pretty
// Sample return
[
{
"firstname": "May-Britt"
},
{
"firstname": "Edvard I."
}
]
Tatsächlich verfügt GROQ über eine reichhaltige Auswahl an Operatoren, die wir innerhalb eines Filters verwenden können. Wir können Zahlen und Zeichenketten mit gleich (==), ungleich (!=), größer als (>), größer oder gleich (>=), kleiner als (<) und kleiner oder gleich (<=) vergleichen. Außerdem können Vergleiche mit UND (&&), ODER (||) und NICHT (!) kombiniert werden. Der Operator in ermöglicht sogar die Überprüfung auf viele Fälle gleichzeitig (z.B. bornCountryCode in ["NO", "SE", "DK"]). Und die Funktion defined ermöglicht es uns zu prüfen, ob ein Feld existiert (z.B. defined(diedCountry)).
Noch mehr Übungen!
Sie kennen das Spiel: Probieren Sie ein wenig mit Filtern herum, um zu sehen, wie sie mit dem Datensatz funktionieren. Antworten gibt es natürlich am Ende.
- Schreiben Sie eine Abfrage, die lebende Preisträger zurückgibt.
- Gibt es einen Unterschied zwischen den Filtern
[bornCountryCode == "NO"][born >= "1950-01-01"]und[bornCountryCode == "NO" && born >= "1950-01-01"]? - Können Sie alle Preisträger finden, die 1973 einen Preis gewonnen haben?
Arbeiten mit Projektionen
Der Datensatz des Nobelpreises trennt den Vornamen und den Nachnamen für jeden Preisträger, aber was, wenn wir sie zu einem Feld zusammenführen möchten? Projektionen in GROQ können genau das tun!
*.laureates[bornCountryCode == "NO" && born >= "1950-01-01"]{
"name": firstname + " " + surname,
born,
"prizeCount": count(prizes),
}
Die Ausführung dieser Abfrage verrät uns, dass May-Britt Moser und Edvard Moser jeweils einen Preis erhalten haben (der tatsächlich derselbe Preis war).
[
{
"name": "May-Britt Moser",
"born": "1963-01-04",
"prizeCount": 1
},
{
"name": "Edvard I. Moser",
"born": "1962-04-27",
"prizeCount": 1
}
]
Was ist hier passiert? Nun, wenn wir eine Projektion in GROQ schreiben, schreiben wir eigentlich ein JSON-Objekt. Zuvor hatten wir einfache Projektionen (wie {firstname}), aber dies ist eine Kurzform für {"firstname": firstname}. Durch die Verwendung der erweiterten Objekt-Syntax können wir sowohl Schlüssel umbenennen als auch Werte transformieren.
GROQ verfügt über eine reichhaltige Auswahl an Operatoren und Funktionen zur Transformation von Daten, darunter Zeichenkettenverkettung, arithmetische Operatoren (+, -, *, /, %, **), Zählen von Arrays (count(prizes)) und Runden von Zahlen (round(num, <amount of decimals>).
Übungen
Hoffentlich bekommen Sie inzwischen ein gutes Gefühl dafür, aber hier sind noch weitere Möglichkeiten, mit Projektionen zu üben.
- Finden Sie alle Preisträger, die zwei oder mehr Preise gewonnen haben.
- Finden Sie heraus, wie viele Preise von Frauen gewonnen wurden.
- Formatiere einen
fullname-Schlüssel, derlastnameundfirstnameim Ergebnis kombiniert.
Mehr auf einmal erledigen
Schau dir das an.
$ cat laureate.json | groq --pretty '
{
"count": count(*.laureates),
"norwegians": *.laureates[bornCountryCode == "NO"]{firstname},
}
'
Das Ergebnis
{
"count": 928,
"norwegians": [
{
"firstname": "Ivar"
},
{
"firstname": "Lars"
},
…
]
}
Hast du es gesehen? Eine GROQ-Abfrage muss nicht mit * beginnen. In dieser Abfrage erstellen wir ein JSON-Objekt, bei dem die Werte Ergebnisse aus separaten Abfragen sind. Dies bietet eine hohe Flexibilität bei dem, was wir mit GROQ produzieren können. Vielleicht möchten Sie die Gesamtzahl der unvollständigen To-Dos zusammen mit einer Liste der fünf letzten. Oder vielleicht möchten Sie die To-Dos in zwei separate Listen aufteilen: eine für abgeschlossene und eine für unvollständige. Oder vielleicht müssen Sie alles in ein Objekt verpacken, weil das von einem anderen Werkzeug/einer anderen Bibliothek/einem anderen Framework erwartet wird. Was auch immer der Fall ist, GROQ hat Sie abgedeckt.
Versuchen wir eine letzte Übung. Können Sie ein Objekt projizieren, bei dem laureates ein Array mit einem gerundeten Prozentsatz der Gesamtzahl der Preise enthält, die jeder Preisträger erhalten hat, und den Vornamen des Preisträgers zurückgeben? Versuchen Sie dann, die Gesamtzahl der vergebenen Preise auszugeben.
Zusammenfassung
Es gibt nicht viel, was Sie lernen müssen, bevor Sie GROQ gut nutzen können. Wenn Sie die Übungen befolgt haben, sind Sie auf dem besten Weg, ein GROQ-Guru zu werden. Selbstverständlich behandelt diese Einführung nicht alle verschiedenen Funktionen und Aspekte von GROQ. Fühlen Sie sich frei, die Spezifikation und das Projekt selbst auf GitHub zu erkunden. Und zögern Sie nicht, sich an das Team von Sanity.io zu wenden, wenn Sie Fragen zur Datenmanipulation mit GROQ haben.
Übungsantworten
Übung 1
Frage 1
Wenn Sie [completed == true] entfernen, erhalten Sie alle To-Dos, nicht nur die abgeschlossenen. Wenn Sie {title, userId} entfernen, erhalten Sie alle Eigenschaften.
Frage 2
*[userId == 2]
Frage 3
*[userId == 2 && completed == false] or *[userId == 2 && !completed]
Frage 4
Wenn Sie die Reihenfolge des Filters und der Projektion ändern, führen Sie zuerst die Projektion aus und wenden dann den Filter an. Das bedeutet, dass Sie eine Liste von To-Dos filtern, die nur title und userId enthalten, und completed == true niemals wahr sein kann.
Frage 5
curl https://jsonplaceholder.typicode.com/todos | groq '*[completed == true]{title, userId}' > result.json
Übung 2
Frage 1
*.laureates[bornCountryCode == "INSERT-YOUR-COUNTRY-HERE"]
Frage 2
*.laureates\[bornCountryCode == "NO"\][-1]
Frage 3
*[bornCountryCode == "NO"] versucht, ein Objekt zu filtern. Das ergibt keinen Sinn, daher erhalten Sie null als Ergebnis.
Frage 4
*.laureates\[0\][bornCountryCode == "NO"] funktioniert nicht, wie Sie vielleicht denken. Dies findet zuerst den ersten Preisträger (der zufällig Wilhelm Conrad ist) und versucht dann, das Objekt zu "filtern". Das ergibt keinen Sinn, daher ist die Antwort null.
Übung 3
Frage 1
*.laureates[died == "0000-00-00"]
Frage 2
Es gibt keinen Unterschied zwischen den Filtern \[bornCountryCode == "NO"\][born >= "1950-01-01"] und [bornCountryCode == "NO" && born >= "1950-01-01"]. Der erste führt das Filtern in zwei "Durchgängen" durch, aber das Endergebnis ist dasselbe.
Frage 3
*.laureates["1973" in prizes[].year]
Übung 4
Frage 1
*.laureates[count(prizes) >= 2]
Frage 2
count(*.laureates[gender == "female"])
Frage 3
*.laureates{"fullname": surname + ", " + firstname}
Übung 5
*.laureates{"laureates": {firstname, "percentage": round(count(prizes) / count(*.laureates[].prizes), 3) * 100}, "total": count(*.laureates[].prizes)}
Für diejenigen, die das Aufblähen des Dateisystems und die Sicherheitsschwierigkeiten bei der Verwendung von NPM vermeiden möchten, ist es auch möglich, JSON in der Kommandozeile mit jq zu parsen.
Die meisten Benutzer sollten es direkt über den Paketmanager ihres Betriebssystems installieren können (
apt-get install jq,brew install jqusw.).Meiner Meinung nach
jq, aber mit einem seltsameren Format und etwas schwieriger zu lesen von der Eingabe bis zur Ausgabe.…
Pipes sind immer gut.
Wenn Sie serverseitige Dinge schreiben, empfehle ich dafür TypeScript oder Retyping.
Zum Beispiel arbeite ich in
serde_mangadex(https://gitlab.io/zeen3/serde_mangadex/) undjs-mangadex-json(https://gitlab.io/zeen3/js-mangadex-json/) mit ordentlich definierten, aber schlecht organisierten Daten. Ich verwende das, um eine bessere Schnittstelle und eine bessere Menge von ausgehenden Daten zu schaffen, aber ansonsten ist es mir wirklich egal.Tief verschachtelte Daten sind ärgerlich, aber so ist auch
json.parse, selbst wenn Sie genau wissen, wie diese Daten aussehen werden. Besonders die Arbeit mit TypeScript, da es immer zu `any` typisiert und es keine Möglichkeit gibt, Transformer daraus zu erstellen.Wirklich, ich bin nur verärgert, dass JSON oft so tief ist.
Ups... gitlab.io verwendet
Korrektur: https://gitlab.com/zeen3/serde_mangadex/ https://gitlab.com/zeen3/js-mangadex-json/
http://porkmail.org/era/unix/award.html#uucaletter
Herzlichen Glückwunsch! Sie haben den „Useless Use of
catAward“ gewonnen.… aber ich benutze
batund verwende Pipes nur injqfür z. B. die Zuordnung lästiger Werte.catverwende ich nie.… oh, Sie haben an die falsche Person geantwortet, Fehler.