JSON

Sniðið sem nær öll API skila: uppbygging JSON, hvernig það kortleggst í R og Python, og hvernig hreiðruð gögn verða að töflu.

Hvað er JSON?

JSON (JavaScript Object Notation) er textasnið til að lýsa gögnum. Nær allar vefþjónustur skila því, svo þú þarft að geta lesið það.

JSON er byggt úr tveimur samsettum gerðum, sem geta geymt aðrar gerðir inni í sér:

Tákn Heitir Í R Í Python
{ } hlutur (e. object) — lyklar og gildi nefndur listi dict
[ ] fylki (e. array) — raðaður listi vigur eða listi list

Grunngerðirnar eru svo fjórar: texti í "gæsalöppum", tölur, true/false og null.

{
  "namskeid": "IÐN302G",
  "einingar": 6,
  "skyldunamskeid": true,
  "kennarar": ["Helga", "Máni"],
  "misseri": { "ar": 2026, "onn": "haust" }
}

Lestu þetta svona: ysta {} er hlutur með fimm lykla. kennarar er fylki með tveimur strengjum. misseri er hlutur inni í hlut — það kallast hreiðrun (e. nesting) og er algengasta ástæða þess að fólk lendir í vandræðum með JSON.

AðvörunAlgengustu villurnar

JSON er ströng um form:

  • lyklar verða að vera í tvöföldum gæsalöppum — {'a': 1} er ekki gilt JSON
  • ekkert komma má vera á eftir síðasta atriði — [1, 2, 3,] er ógilt
  • true/false eru lágstafa — ekki True/False eins og í Python
  • NA og None eru ekki til; það heitir null

Frá JSON yfir í gagnahluti

jsonlite þýðir á milli. fromJSON() les JSON og skilar R-hlut:

library(jsonlite)

txt <- '{
  "namskeid": "IÐN302G",
  "einingar": 6,
  "skyldunamskeid": true,
  "kennarar": ["Helga", "Máni"],
  "misseri": { "ar": 2026, "onn": "haust" }
}'

nam <- fromJSON(txt)
str(nam)
List of 5
 $ namskeid      : chr "IÐN302G"
 $ einingar      : int 6
 $ skyldunamskeid: logi TRUE
 $ kennarar      : chr [1:2] "Helga" "Máni"
 $ misseri       :List of 2
  ..$ ar : int 2026
  ..$ onn: chr "haust"

Niðurstaðan er venjulegur nefndur listi og þú nálgast hann með $:

nam$namskeid
[1] "IÐN302G"
nam$kennarar
[1] "Helga" "Máni" 
nam$misseri$ar
[1] 2026

json fylgir Python — það þarf ekki að setja upp neinn pakka:

import json

txt = '''{
  "namskeid": "IÐN302G",
  "einingar": 6,
  "skyldunamskeid": true,
  "kennarar": ["Helga", "Máni"],
  "misseri": { "ar": 2026, "onn": "haust" }
}'''

nam = json.loads(txt)
type(nam)
<class 'dict'>

Niðurstaðan er dict og þú nálgast hana með hornklofum:

nam["namskeid"]
'IÐN302G'
nam["kennarar"]
['Helga', 'Máni']
nam["misseri"]["ar"]
2026

Ólíkt R, þar sem hreiðraði hlutinn misseri verður listi inni í lista, er hann einfaldlega dict inni í dict — sami hlutur alla leið niður.

Í requests þarftu ekki json.loads()svar.json() gerir þetta beint.

Athugasemdtrue verður TRUE og True

JSON-gildið true verður TRUE í R og True í Python — bæði mál kortleggja það á sína eigin sannindagerð. Á leiðinni til baka gerist hið gagnstæða, og þess vegna er json.dumps() og toJSON() rétta leiðin frekar en að byggja strenginn sjálf/ur.

Listi af hlutum verður tafla

Þetta er mikilvægasta tilfellið í reynd: þegar API skilar lista af hlutum sem hafa sömu lykla liggur tafla beint við.

svar <- '[
  {"nafn": "Quarto",  "stjornur": 4200, "mal": "TypeScript"},
  {"nafn": "knitr",   "stjornur": 2500, "mal": "R"},
  {"nafn": "jsonlite","stjornur":  400, "mal": "C"}
]'

tafla <- fromJSON(svar)
tafla
      nafn stjornur        mal
1   Quarto     4200 TypeScript
2    knitr     2500          R
3 jsonlite      400          C
class(tafla)
[1] "data.frame"

jsonlite býr til data.frame sjálfkrafa. Þaðan vinnur dplyr með þetta eins og hverja aðra töflu:

library(dplyr)

tafla |>
  filter(stjornur > 1000) |>
  arrange(desc(stjornur))
    nafn stjornur        mal
1 Quarto     4200 TypeScript
2  knitr     2500          R
import json
import pandas as pd

svar = '''[
  {"nafn": "Quarto",  "stjornur": 4200, "mal": "TypeScript"},
  {"nafn": "knitr",   "stjornur": 2500, "mal": "R"},
  {"nafn": "jsonlite","stjornur":  400, "mal": "C"}
]'''

tafla = pd.DataFrame(json.loads(svar))
tafla
       nafn  stjornur         mal
0    Quarto      4200  TypeScript
1     knitr      2500           R
2  jsonlite       400           C

Og síun:

tafla[tafla["stjornur"] > 1000].sort_values("stjornur", ascending=False)
     nafn  stjornur         mal
0  Quarto      4200  TypeScript
1   knitr      2500           R

Sama niðurstaða og í R-flipanum. Munurinn er að pandas sýnir vísitölu (0, 1) í fyrsta dálki þar sem R sýnir raðnúmer.

Þegar gögnin eru hreiðruð

Raunveruleg svör eru sjaldan svona snyrtileg. Oftast eru gögnin sem þú vilt grafin inni í umbúðum, og einstakir dálkar geta sjálfir verið hlutir:

{
  "total_count": 3,
  "items": [
    {"nafn": "Quarto", "eigandi": {"login": "quarto-dev", "type": "Organization"}},
    {"nafn": "knitr",  "eigandi": {"login": "yihui",      "type": "User"}}
  ]
}
svar <- '{
  "total_count": 3,
  "items": [
    {"nafn": "Quarto", "eigandi": {"login": "quarto-dev", "type": "Organization"}},
    {"nafn": "knitr",  "eigandi": {"login": "yihui",      "type": "User"}}
  ]
}'

d <- fromJSON(svar)
items <- d$items
str(items)
'data.frame':   2 obs. of  2 variables:
 $ nafn   : chr  "Quarto" "knitr"
 $ eigandi:'data.frame':    2 obs. of  2 variables:
  ..$ login: chr  "quarto-dev" "yihui"
  ..$ type : chr  "Organization" "User"

eigandi er data.frame inni í data.frame. Það kemur á óvart og brýtur dplyr-keðjur. Lausnin er flatten():

flatten(items)
    nafn eigandi.login eigandi.type
1 Quarto    quarto-dev Organization
2  knitr         yihui         User
svar = '''{
  "total_count": 2,
  "items": [
    {"nafn": "Quarto", "eigandi": {"login": "quarto-dev", "type": "Organization"}},
    {"nafn": "knitr",  "eigandi": {"login": "yihui",      "type": "User"}}
  ]
}'''

d = json.loads(svar)

# rangt: eigandi verður dálkur með dict-um í
pd.DataFrame(d["items"])
     nafn                                          eigandi
0  Quarto  {'login': 'quarto-dev', 'type': 'Organization'}
1   knitr               {'login': 'yihui', 'type': 'User'}

Dálkurinn eigandi ber heila dict-hluti — ónothæft til greiningar. json_normalize() flettir hreiðrunina út:

pd.json_normalize(d["items"])
     nafn eigandi.login  eigandi.type
0  Quarto    quarto-dev  Organization
1   knitr         yihui          User

Dálkarnir eigandi.login og eigandi.type — sama niðurstaða og flatten() skilar í R.

Sé listinn grafinn dýpra má benda á hann beint:

pd.json_normalize(d, record_path="items")
     nafn eigandi.login  eigandi.type
0  Quarto    quarto-dev  Organization
1   knitr         yihui          User

Sama tafla — munurinn er aðeins að þú bendir á items innan úr svarinu í stað þess að taka það út fyrst.

ÁbendingVinnuregla

Ekki reyna að giska á uppbygginguna. Skoðaðu svarið fyrststr(x, max.level = 2) í R, eða print(json.dumps(d, indent=2, ensure_ascii=False)[:500]) í Python — og flettu svo út.

Að skoða ókunnugt svar

slod <- "https://api.vedur.is/weather/stations?station_id=1"
d <- fromJSON(slod)

dim(d)                    # hversu stórt er svarið?
[1]  1 11
names(d)                  # hvaða reitir eru í boði?
 [1] "station" "name"    "abbr"    "type"    "lat"     "lon"     "ele"    
 [8] "wigos"   "owner"   "start"   "ending" 

Svarið ber fleiri reiti en þú þarft. Taktu sneið af því sem skiptir máli:

str(d[c("station", "name", "lat", "lon", "ele")])
'data.frame':   1 obs. of  5 variables:
 $ station: int 1
 $ name   : chr "Reykjavík"
 $ lat    : num 64.1
 $ lon    : num -21.9
 $ ele    : num 60.2
import requests

slod = "https://api.vedur.is/weather/stations?station_id=1"
d = requests.get(slod, timeout=30).json()

len(d)                    # hversu margar færslur?
1
list(d[0])                # hvaða reitir eru í boði?
['station', 'name', 'abbr', 'type', 'lat', 'lon', 'ele', 'wigos', 'owner', 'start', 'ending']

Svarið ber fleiri reiti en þú þarft. Taktu sneið af því sem skiptir máli:

{k: d[0][k] for k in ["station", "name", "lat", "lon", "ele"]}
{'station': 1, 'name': 'Reykjavík', 'lat': 64.1288833618, 'lon': -21.9081897736, 'ele': 60.2000007629}
AthugasemdAf hverju ekki GitHub hér?

Stuttu dæmin hér að ofan senda enga hausafromJSON(slod) tekur ekki við þeim, og þótt requests.get() styðji headers= er það ekki notað þegar slóðin ein er gefin. Beiðnin fer því óauðkennd af stað.

GitHub leyfir aðeins 60 óauðkenndar beiðnir á klukkustund, svo dæmi sem byggist á þeim er brothætt. Veðurstofan krefst engrar auðkenningar og hentar því betur til að sýna stuttu leiðina.

Þurfirðu auðkenningu — eða yfirleitt að stjórna hausum, stöðukóðum og villum — notarðu httr2 / requests að fullu. Sjá næsta kafla.

ÁbendingRaunveruleg svör eru stór

Langur lyklalisti fyrir eina geymslu er ekkert einsdæmi — þjónustur skila öllu sem þær eiga, ekki því sem þú baðst um. Fyrsta verkið er alltaf að finna út hvað er í boði, og annað verkið að henda öllu hinu.

Þetta er fínt til að skoða, en fyrir alvöru vinnu áttu að nota httr2 / requests með villumeðhöndlun — þau gefa þér stöðukóða og hausa sem stuttu leiðirnar fela. Það er efni næsta kafla.

Hin áttin

Stundum þarftu að senda JSON, t.d. þegar þjónusta krefst POST — eins og Hagstofan gerir.

listi <- list(nafn = "Máni", lotur = c(1, 2, 3))
toJSON(listi, auto_unbox = TRUE, pretty = TRUE)
{
  "nafn": "Máni",
  "lotur": [1, 2, 3]
} 

Íslenski stafurinn skilar sér óbreyttur — jsonlite skrifar UTF-8 sjálfgefið og hefur ekkert viðfang til að slökkva á því. Python hagar sér öfugt; sjá Python-flipann.

AthugasemdHvað auto_unbox gerir

R á enga stærð sem er “ein tala” — 5 er í raun vigur af lengd 1. Án auto_unbox = TRUE skrifar toJSON() því ["Helga"] þar sem þjónustan bjóst við "Helga". Þetta veldur 400 sem er erfitt að rekja, svo hafðu þetta á.

Í httr2 sendirðu listann beint og pakkinn sér um umbreytinguna:

slod <- "https://example.com/api"  # slóð sem tekur við JSON með POST

request(slod) |>
  req_body_json(listi, auto_unbox = TRUE) |>
  req_perform()
listi = {"nafn": "Máni", "lotur": [1, 2, 3]}
print(json.dumps(listi, ensure_ascii=False, indent=2))
{
  "nafn": "Máni",
  "lotur": [
    1,
    2,
    3
  ]
}

ensure_ascii=False er nauðsyn fyrir íslenska stafi. Sjálfgefið flýr json.dumps() alla stafi utan ASCII:

json.dumps({"nafn": "Máni", "stadur": "Þingvellir"})
'{"nafn": "M\\u00e1ni", "stadur": "\\u00deingvellir"}'
json.dumps({"nafn": "Máni", "stadur": "Þingvellir"}, ensure_ascii=False)
'{"nafn": "Máni", "stadur": "Þingvellir"}'

Hvort tveggja er gilt JSON og þjónustan skilur bæði — en það fyrra er ólæsilegt þegar þú skoðar skrána sjálf/ur eða vistar hana í geymsluna.

Þetta er ólíkt R: jsonlite::toJSON() hefur ekkert ensure_ascii-viðfang og skrifar UTF-8 sjálfgefið. Gildran er því aðeins Python-megin.

Í requests sendirðu hlutinn beint og pakkinn sér um umbreytinguna:

slod = "https://example.com/api"  # slóð sem tekur við JSON með POST

requests.post(slod, json=listi, timeout=30)