Problem mit verschachtelten JSON Daten

Für allgemeine Fragen zur Programmierung mit PureBasic.
Benutzeravatar
Kurzer
Beiträge: 1614
Registriert: 25.04.2006 17:29
Wohnort: Nähe Hamburg

Problem mit verschachtelten JSON Daten

Beitrag von Kurzer »

Hallo zusammen,

ich wollte mir "mal kurz" ein Tool zur Extraktion von Tankstellenpreisen aus einer JSON-Datei zusammenschreiben, aber dieses JSON Format macht mich echt fertig. Bis vor 1 Stunde habe ich noch nie was von JSON gehört, geschweige denn etwas damit gemacht.

Soweit ich das verstanden habe liegt mir hier eine JSON Datei vor, die in der äußeren "Schicht" aus einem Array mit 10 Einträgen besteht. Jeder Arrayeintrag entspricht einer Tankstelle und besteht selbst wieder aus diversen Untereinträgen. Viele dieser Untereinträge sind "flat", das heißt ich kann sie relativ einfach mit GetJSONMember() extrahieren - entsprechenden Code habe ich mir auf die Schnelle aus der PB-Hilfe zusammengestoppelt. Einige Untereinträge sind selbst aber wieder Arrays und genau da habe ich es bisher nicht geschafft dran zu kommen. Ich weiß nicht mit welchen JSON-Funktionen ich mich zu dem betreffenden Eintrag "durch hangeln" muss. Die Hilfe liefert leider keine Beispiele für Zugriff auf verschachtelte Arrays in einem JSON String.

Ich liefere hier mal nur den ersten Arrayeintrag aus der Datei, sonst wird es zu lang. Ein Eintrag reicht ja auch völlig aus.
Folgende Daten müsstet ihr als Textdatei "spritpreis-json-stripped.txt" speichern:

Code: Alles auswählen

{"id":41188,"name":"Diskont Tankstelle","location":{"address":"Neue Landstraße 74 (\"Hofer-Parkplatz\")","postalCode":"4655","city":"Vorchdorf","latitude":48.008358,"longitude":13.920214},"contact":{"telephone":"43800202055","mail":"office@fe-trading.at","website":"http://www.diskonttanken.at/"},"openingHours":[{"day":"MO","label":"Montag","order":1,"from":"00:00","to":"24:00"},{"day":"DI","label":"Dienstag","order":2,"from":"00:00","to":"24:00"},{"day":"MI","label":"Mittwoch","order":3,"from":"00:00","to":"24:00"},{"day":"DO","label":"Donnerstag","order":4,"from":"00:00","to":"24:00"},{"day":"FR","label":"Freitag","order":5,"from":"00:00","to":"24:00"},{"day":"SA","label":"Samstag","order":6,"from":"00:00","to":"24:00"},{"day":"SO","label":"Sonntag","order":7,"from":"00:00","to":"24:00"},{"day":"FE","label":"Feiertag","order":8,"from":"00:00","to":"24:00"}],"offerInformation":{"service":false,"selfService":true,"unattended":true},"paymentMethods":{"cash":false,"debitCard":true,"creditCard":true,"others":"DKV-Card"},"paymentArrangements":{"cooperative":false,"clubCard":false},"position":1,"open":true,"distance":12.111741463692725,"prices":[{"fuelType":"DIE","amount":1.146,"label":"Diesel"}]}
Hier nun der Code mit dem ich erfolgreich auf das Member, das Element, den Eintrag (oder wie auch immer es in JSON richtig benannt wird) "id" und "name" in der ersten Zeile zu greifen kann. Jedoch ist mir der Versuch auf den Eintrag "address" innerhalb des Arrays "location" zuzugreifen bisher nicht geglückt. Ihr müsstet den Laufwerksbuchstaben und ggf. den Dateinamen in der ersten Zeile anpassen:

Code: Alles auswählen

If ReadFile(0, "D:\spritpreis-json-stripped.txt")
	sJsonFile.s = ReadString(0)
	CloseFile(0)
Else
	Debug "ReadFile Error"
EndIf

If ParseJSON(0, sJSONFile)
	
	For i = 0 To JSONArraySize(JSONValue(0)) - 1
		Debug Str( GetJSONInteger(GetJSONMember(GetJSONElement(JSONValue(0), i), "id")) )
		Debug GetJSONString(GetJSONMember(GetJSONElement(JSONValue(0), i), "name"))
		Debug GetJSONString(GetJSONMember(GetJSONElement(GetJSONMember(GetJSONElement(JSONValue(0), 0), "address"), i), "location"))
		;[{"id":41188,"name":"Diskont Tankstelle","location":{"address":"Neue Landstraße 74 
	Next i
	FreeJSON(0)
Else
	Debug "JSON Parse Error"
EndIf
Debugoutput sagt:
[23:30:04] [Debug] 41188
[23:30:04] [Debug] Diskont Tankstelle
[23:30:04] [ERROR] Zeile: 13
[23:30:04] [ERROR] Ungültiger JSON Wert. (0)
Wenn mich hierzu jemand in die richtige Richtung schubsen könnte, wär das super. Vermutlich habe ich einfach nur das Konzept JSON noch nicht ganz verstanden. Es soll ja lt. Beschreibung ein "ganz einfaches" Datenaustauschformat sein, was sehr leicht von Mensch und Maschine zu lesen ist.

Gruß Kurzer

Edit: JSON-Datei nach Hinweis von Bisonte korrigiert.
Zuletzt geändert von Kurzer am 11.04.2019 09:32, insgesamt 1-mal geändert.
"Never run a changing system!" | "Unterhalten sich zwei Alleinunterhalter... Paradox, oder?"
PB 6.02 x64, OS: Win 7 Pro x64 & Win 11 x64, Desktopscaling: 125%, CPU: I7 6500, RAM: 16 GB, GPU: Intel Graphics HD 520
Useralter in 2023: 56 Jahre.
Benutzeravatar
Bisonte
Beiträge: 2427
Registriert: 01.04.2007 20:18

Re: Problem mit verschachtelten JSON Daten

Beitrag von Bisonte »

Ich benutze in so einem Fall (es sind ja extrem viele Werte die da aufschlagen) ExtractJSONStructure.

Dein Beispiel JSON ist leider nicht korrekt. die Eckige Klammer am Anfang und am Ende muss entfernt werden,
damit es in deinem "Einzelbeispiel" funktioniert. (Ich nehme an, im Original ist es eine Liste...)

Soweit so gut. Es ist etwas Vorarbeit nötig, um die Daten in die richtigen Strukturen zu pressen.
Ich hab da mal was vorbereitet :

Code: Alles auswählen

Structure s_location
  address.s
  postalCode.s
  city.s
  latitude.f
  longitude.f
EndStructure
Structure s_contact
  telephone.s
  mail.s
  website.s
EndStructure
Structure s_openingItem
  Day.s
  Label.s
  order.i
  from.s
  To.s
EndStructure
Structure s_offerinfo
  service.i
  selfservice.i
  unattended.i
EndStructure
Structure s_paymentMethods
  cash.i
  debitcard.i
  creditcard.i
  others.s
EndStructure
Structure s_paymentArrangements
  cooperative.i
  clubcard.i
EndStructure
Structure s_prices
  fueltype.s
  amount.f
  label.s
EndStructure

Structure json_tankstelle
  ID.i
  Name.s
  Location.s_location
  Contact.s_contact
  List openingHours.s_openingItem()
  offerInformation.s_offerinfo
  paymentMethods.s_paymentMethods
  paymentArrangements.s_paymentArrangements
  position.i
  open.i
  distance.f
  List prices.s_prices()
EndStructure

Tank.json_tankstelle

jSon = LoadJSON(#PB_Any, "D:\spritpreis-json-stripped.txt", #PB_JSON_NoCase)

If jSon
  
  ExtractJSONStructure(JSONValue(jSon), @Tank, json_tankstelle)
  FreeJSON(jSon)
  
EndIf

Debug Tank\ID
Debug Tank\Name
Debug Tank\Location\address
Debug Tank\Location\postalCode
Debug Tank\Location\city
Debug StrF(Tank\Location\latitude, 6)
Debug StrF(Tank\Location\longitude, 6)

Debug Tank\Contact\telephone
Debug Tank\Contact\mail
Debug Tank\Contact\website

ForEach Tank\openingHours()
  With Tank\openingHours()
    Debug \Day
    Debug \Label
    Debug \order
    Debug \from
    Debug \To
  EndWith
Next

ForEach Tank\prices()
  With Tank\prices()
    Debug \fueltype
    Debug StrF(\amount, 3)
    Debug \label
  EndWith
Next
Sieht auf den ersten Blick wie die Hölle aus, ist aber beim Benutzen eine echte Wohltat, weil alles da ist wo man es braucht ;)
PureBasic 6.04 LTS (Windows x86/x64) | Windows10 Pro x64 | Asus TUF X570 Gaming Plus | R9 5900X | 64GB RAM | GeForce RTX 3080 TI iChill X4 | HAF XF Evo | build by vannicom​​
Benutzeravatar
Kurzer
Beiträge: 1614
Registriert: 25.04.2006 17:29
Wohnort: Nähe Hamburg

Re: Problem mit verschachtelten JSON Daten

Beitrag von Kurzer »

Wow! Vielen Dank Bisonte. :allright: Nee, sieht nicht wie die Hölle aus, ist extrem strukturiert und ich kann das sofort leicht nachvollziehen. Den ganzen "Drecksjob" macht ja ExtractJSONStructure(). :lol:

ExtractJSONStructure() ist ja echt ein mächtiger Befehl. Dein Code klappt hervorragend. Vielen Dank für die ganze Tipparbeit, die du dir gemacht hast. Ein virtuelles Bier/Kaffee/Cola/... von mir ist dir sicher. :praise:

Zwei Fragen habe ich dazu noch (nur am Rande):
1) Werden JSON Objekte in einer Struktur immer als Liste repräsentiert? Ich habe dazu keinen Hinweis in der Hilfe gefunden und wenn ich versuche aus...

Code: Alles auswählen

List prices.s_prices()
das hier zu machen:

Code: Alles auswählen

prices.s_prices[1]
(weil es wird eh immer nur eine Sorte mit einem Preis in der JSON Datei vorkommen), dann wird das statische Array von ExtractJSONStructure() nicht gefüllt.

2) Wenn man den Wert "address" zu Fuß ermitteln wollte ohne die ganze Struktur zu extrahieren, wie müsste das dann korrekt aussehen? Das ist der Teil an dem ich nicht weiter kam.

Gruß Kurzer

Edit: Punkt 2) habe ich mir dank JSONTYpe() (zum Untersuchen was für ein Typ zurück kommt) nun selbst beantwortet.
Die Zeile muss wie folgt lauten:

Code: Alles auswählen

Debug GetJSONString(GetJSONMember(GetJSONMember(GetJSONElement(JSONValue(0), i), "location"), "address"))
"Never run a changing system!" | "Unterhalten sich zwei Alleinunterhalter... Paradox, oder?"
PB 6.02 x64, OS: Win 7 Pro x64 & Win 11 x64, Desktopscaling: 125%, CPU: I7 6500, RAM: 16 GB, GPU: Intel Graphics HD 520
Useralter in 2023: 56 Jahre.
Benutzeravatar
Bisonte
Beiträge: 2427
Registriert: 01.04.2007 20:18

Re: Problem mit verschachtelten JSON Daten

Beitrag von Bisonte »

Kurzer hat geschrieben:Zwei Fragen habe ich dazu noch (nur am Rande):
1) Werden JSON Objekte in einer Struktur immer als Liste repräsentiert? Ich habe dazu keinen Hinweis in der Hilfe gefunden und wenn ich versuche aus...

Code: Alles auswählen

List prices.s_prices()
das hier zu machen:

Code: Alles auswählen

prices.s_prices[1]
(weil es wird eh immer nur eine Sorte mit einem Preis in der JSON Datei vorkommen), dann wird das statische Array von ExtractJSONStructure() nicht gefüllt.
Arrays sind in JSON auch als solche zu erkennen.
In deinem Beispiel ist ein Array nicht vorhanden. Bei Dir sind aber Listeneinträge vorhanden (oder solche die es werden könnten, wenn mehr Einträge da wären)
Kurzer hat geschrieben:2) Wenn man den Wert "address" zu Fuß ermitteln wollte ohne die ganze Struktur zu extrahieren, wie müsste das dann korrekt aussehen? Das ist der Teil an dem ich nicht weiter kam.
Gruß Kurzer
Das ist beinahe wie bei XML.
Root\location\adress
Root\location\postalcode
usw.
Zu Fuss wäre das in etwa so :

Code: Alles auswählen

jSon = LoadJSON(#PB_Any, "D:\spritpreis-json-stripped.txt", #PB_JSON_NoCase)

If jSon
  jV = JSONValue(jSon)
  If jV
    Node_Location = GetJSONMember(jV, "location")
    
    If Node_Location
      Debug GetJSONString(GetJSONMember(Node_Location, "address"))
      Debug GetJSONString(GetJSONMember(Node_Location, "postalcode"))
      ; usw...
    EndIf
    
  EndIf
    
  FreeJSON(jSon)
 
EndIf
PureBasic 6.04 LTS (Windows x86/x64) | Windows10 Pro x64 | Asus TUF X570 Gaming Plus | R9 5900X | 64GB RAM | GeForce RTX 3080 TI iChill X4 | HAF XF Evo | build by vannicom​​
Andesdaf
Moderator
Beiträge: 2658
Registriert: 15.06.2008 18:22
Wohnort: Dresden

Re: Problem mit verschachtelten JSON Daten

Beitrag von Andesdaf »

Zu 2.: Für einfachen Zugriff auf JSON-Daten wie bei XMLNodeFromPath() kann ich außerdem empfehlen:
(angepasstes Original von Little John hier)

Code: Alles auswählen

; #author Little John, derivated by hgzh
; #url    http://www.purebasic.fr/english/viewtopic.php?p=467733#p467733

DeclareModule JSON
   Declare.s Get (jn.i, path$, quote$="")
   Declare.i Set (jn.i, path$, value$, quote$="'")
EndDeclareModule

Module JSON
   EnableExplicit
   
   Procedure.i _GetJSONValue (jn.i, path$)
      ; -- internal function
      Protected jv.i, depth.i, level.i, index.i, field$
     
      jv = JSONValue(jn)
     
      If jv <> 0 And path$ <> ""
         depth = CountString(path$, "\") + 1
         For level = 1 To depth
            field$ = StringField(path$, level, "\")
            If Left(field$, 1) = "[" And Right(field$, 1) = "]"
               index = Val(Mid(field$, 2, Len(field$)-2))
               If JSONType(jv) = #PB_JSON_Array And index >= 0 And index < JSONArraySize(jv)
                  jv = GetJSONElement(jv, index)
                  If jv = 0
                    ProcedureReturn 0
                  EndIf
               Else
                  jv = 0
               EndIf
            Else   
               If JSONType(jv) = #PB_JSON_Object
                  jv = GetJSONMember(jv, field$)   ; 0 if the given 'field$' does not exist in the object
                  If jv = 0
                    ProcedureReturn 0
                  EndIf
               Else
                  jv = 0
               EndIf
            EndIf
         Next   
      EndIf
     
      ProcedureReturn jv
   EndProcedure
   
   
   Procedure.s Get (jn.i, path$, quote$="")
      ; in : jn    : JSON number, e.g. generated by ParseJSON()
      ;      path$ : path to the JSON element that is to be retrieved
      ;      quote$: character that is regarded as quote
      ; out: value of the desired element
      Protected jv.i, ret$=""
     
      If jn
         jv = _GetJSONValue(jn, path$)
         If jv
            Select JSONType(jv)
               Case #PB_JSON_String
                  ret$ = quote$ + GetJSONString(jv) + quote$
               Case #PB_JSON_Boolean
                  If GetJSONBoolean(jv) = #True
                     ret$ = "true"
                  Else
                     ret$ = "false"
                  EndIf   
               Case #PB_JSON_Null
                  ret$ = "null"
               Case #PB_JSON_Number
                  ret$ = StrD(GetJSONDouble(jv))
               Case #PB_JSON_Array
                  ret$ = Str(jv)
            EndSelect     
         EndIf
      EndIf
     
      ProcedureReturn ret$
   EndProcedure
   
   
   Macro _IsNumber (_string_)
      ; -- internal macro
      Bool(_string_ <> "" And (Val(_string_) <> Val(_string_+"1") Or ValF(_string_) <> ValD(_string_+"1")))
   EndMacro
   
   Procedure.i Set (jn.i, path$, value$, quote$="'")
      ; in : jn    : JSON number, e.g. generated by ParseJSON()
      ;      path$ : path to the JSON element that is to be changed
      ;      value$: new value of the desired element
      ;      quote$: character that is regarded as quote
      ; out: #True on success, #False on error
      Protected jv.i, ret.i=#False
     
      If jn
         jv = _GetJSONValue(jn, path$)
         If jv
            If Left(value$, 1) = quote$ And Right(value$, 1) = quote$
               SetJSONString(jv, Mid(value$, 2, Len(value$)-2))
               ret = #True
            ElseIf value$ = "true"
               SetJSONBoolean(jv, #True)
               ret = #True
            ElseIf value$ = "false"
               SetJSONBoolean(jv, #False)
               ret = #True
            ElseIf value$ = "null"
               SetJSONNull(jv)
               ret = #True
            ElseIf _IsNumber(value$)
               SetJSONDouble(jv, ValD(value$))
               ret = #True
            EndIf 
         EndIf
      EndIf
     
      ProcedureReturn ret
   EndProcedure
EndModule
Win11 x64 | PB 6.00 (x64)
Benutzeravatar
Kurzer
Beiträge: 1614
Registriert: 25.04.2006 17:29
Wohnort: Nähe Hamburg

Re: Problem mit verschachtelten JSON Daten

Beitrag von Kurzer »

Besten Dank für Eure Hinweise. Die JSON-Extraktion verstehe ich nun besser.

Allerdigs stolpere ich bei der weiteren Programmierung über eine komische Sache im Bereich ReceiveHTTPMemory().
Ich weiß nicht, ob es ein Problem der Webseite ist von der ich Daten lese oder ein Problem von PB. Das geschilderte Verhalten tritt mit PB 5.62 und 5.70 auf. Andere Versionen habe ich nicht probiert.

Folgender Testcode greift von e-control.at per ReceiveHTTPMemory() eine JSON Datei mit Tankstellen und deren Benzinpreisen ab.
Es werden zwei identische Abfragen hintereinander ausgeführt. Bei den Abfragen sind nur die URLs verschieden. Die eine URL enthält das Token DIE für Diesel, das andere SUP für Super. Beide Abfragen kann man mit dem "If 1=1" entweder aktivieren oder deaktivieren.

Das Ergebnis der letzten Abfrage wird in die Zwischenablage kopiert, damit man sich den JSON String zur Ansicht in einen Texteditor einfügen kann.

Folgendes Problem:

Bei der unten dargestellten Konstellation (beide Abfragen ausführen, Diesel zuerst) wird beim Empfang des zweiten JSON Strings (also den für Super) am ende Müll mit übertragen. Das Ende der JSON-Datei sieht so aus:

Code: Alles auswählen

"distance":12.812155275720695,"prices":[]}]se,"clubCard
Es müsste aber so aussehen:

Code: Alles auswählen

"distance":12.812155275720695,"prices":[]}]
Dies tritt nur auf, wenn ich beide Abfragen ausführe und Diesel zuerst abgefragt wird. Frage ich Super zuerst ab und dann Diesel, dann sind beide JSON Strings in Ordnung. Ebenso, wenn ich Super oder Diesel einzeln abfrage.

Ich vermute ja, dass die Webseite der Verursacher ist, bin mir aber nicht sicher. Oder habe ich was in meinem Testcode übersehen?

Code: Alles auswählen

InitNetwork()

; Diesel
If 1=1
	;sUrl.s = "https://api.e-control.at/sprit/1.0/search/gas-stations/by-address?latitude=47.902266&longitude=13.956367&fuelType=SUP&includeClosed=false"
	sUrl.s = "https://api.e-control.at/sprit/1.0/search/gas-stations/by-address?latitude=47.902266&longitude=13.956367&fuelType=DIE&includeClosed=false"
	*ReceiveBuffer = ReceiveHTTPMemory(sUrl)
	If *ReceiveBuffer
		sResult.s = PeekS(*ReceiveBuffer, MemorySize(*ReceiveBuffer), #PB_UTF8)
		FreeMemory(*ReceiveBuffer)
	Else
		Debug "Error"
	EndIf
EndIf

sResult = ""

; Super
If 1=1
	sUrl.s = "https://api.e-control.at/sprit/1.0/search/gas-stations/by-address?latitude=47.902266&longitude=13.956367&fuelType=SUP&includeClosed=false"
	;sUrl.s = "https://api.e-control.at/sprit/1.0/search/gas-stations/by-address?latitude=47.902266&longitude=13.956367&fuelType=DIE&includeClosed=false"
	*ReceiveBuffer = ReceiveHTTPMemory(sUrl)
	If *ReceiveBuffer
		sResult + PeekS(*ReceiveBuffer, MemorySize(*ReceiveBuffer), #PB_UTF8)
		FreeMemory(*ReceiveBuffer)
	Else
		Debug "Error"
	EndIf
EndIf

SetClipboardText(sResult)
Gruß Kurzer

Edit: Wenn ich #PB-Ascii statt #PB_UTF8 beim Peek nutze, dann klappts. Sehr ominös.

Code: Alles auswählen

PeekS(*ReceiveBuffer, MemorySize(*ReceiveBuffer), #PB_Ascii)
Edit2: So jetzt aber: Es fehlte das |#PB_ByteLength. Steht ja auch so als Beispiel in der Hilfe, ich hätte nur mal reingucken müssen. Na ja, ich tippe offenbar gern Monologe... aber wenn's hilft ist's ja ok. :lol:

Code: Alles auswählen

PeekS(*ReceiveBuffer, MemorySize(*ReceiveBuffer), #PB_UTF8|#PB_ByteLength)
"Never run a changing system!" | "Unterhalten sich zwei Alleinunterhalter... Paradox, oder?"
PB 6.02 x64, OS: Win 7 Pro x64 & Win 11 x64, Desktopscaling: 125%, CPU: I7 6500, RAM: 16 GB, GPU: Intel Graphics HD 520
Useralter in 2023: 56 Jahre.
Antworten