library(httr2)
lykill <- Sys.getenv("GITHUB_TOKEN") # tómt ef hann er ekki settur
gh_auth <- if (nzchar(lykill)) paste("Bearer", lykill) else NULLAð sækja gögn
Beiðni byggð, svar lesið, villur meðhöndlaðar og allar síður sóttar — sömu skref í R (httr2) og Python (requests).
Hér er sama verkið unnið í tveimur málum. Veldu flipann sem á við þig — hugtökin eru þau sömu, aðeins rithátturinn er ólíkur.
install.packages(c("httr2", "jsonlite", "dplyr"))httr2 er arftaki eldri pakkans httr, sem er ekki lengur í virkri þróun. Mörg dæmi á netinu nota httr — rithátturinn er annar og þeir blandast ekki.
pip install requests pandasrequests er staðalvalið fyrir HTTP í Python. urllib fylgir með Python en er óþægilegra.
Af hverju ekki bara að lesa slóðina beint
Bæði mál bjóða styttri leið — jsonlite::fromJSON(url) í R, pandas.read_json(url) í Python. Hún virkar til að kíkja á gögn, en felur allt sem skiptir máli þegar eitthvað fer úrskeiðis: þú sérð ekki stöðukóðann, ekki hausana, og færð óskiljanlega villu í stað 404.
Í skýrslum sem aðrir eiga að treysta notarðu réttu tólin.
Álagsmörk GitHub
GitHub leyfir 60 beiðnir á klukkustund án auðkenningar, en 5.000 með aðgangslykli. Dæmin hér að neðan eru keyrð í hvert sinn sem bókin er byggð, svo þau nota lykil þegar hann er til staðar í umhverfinu:
req_headers() endurstillir haus á sjálfgefið gildi ef þú gefur NULL. Fyrir Authorization þýðir það að enginn auðkenningarhaus er sendur, svo sama kóðadæmi virkar hvort sem lykillinn er til eða ekki — bara með ólíkum álagsmörkum. Sjá góða starfshætti um það hvernig lykillinn er geymdur.
Beiðni í skrefum
library(httr2)
svar <- request("https://api.github.com") |>
req_url_path("repos", "HI-IDN", "IDN302G") |>
req_headers("Accept" = "application/vnd.github+json",
Authorization = gh_auth) |>
req_user_agent("IDN302G-nemandi (nafn@hi.is)") |>
req_perform()
resp_status(svar)[1] 200
resp_content_type(svar)[1] "application/json"
Ekkert er sent fyrr en req_perform() er kallað, svo þú getur skoðað beiðnina fyrst:
request("https://api.github.com") |>
req_url_path("search", "repositories") |>
req_url_query(q = "quarto", per_page = 5) |>
req_dry_run()GET /search/repositories HTTP/1.1
accept: */*
accept-encoding: deflate, gzip, br, zstd
host: api.github.com
user-agent: httr2/1.3.0 r-curl/8.0.0 libcurl/8.5.0
req_dry_run() sýnir aðferðina, slóðina og alla hausa — en sendir ekkert.
Taktu eftir að ?q=quarto&per_page=5 vantar í úttakið að ofan. req_dry_run() prentar slóðina án fyrirspurnarstrengs, svo hún dugar til að skoða hausa en ekki til að staðfesta að færibreyturnar hafi ratað rétt. Til þess skoðarðu slóðina sjálfa:
req <- request("https://api.github.com") |>
req_url_path("search", "repositories") |>
req_url_query(q = "quarto", per_page = 5)
req$url[1] "https://api.github.com/search/repositories?q=quarto&per_page=5"
Þetta samsvarar print(svar.url) í Python.
import requests
import os
hausar = {
"Accept": "application/vnd.github+json",
"User-Agent": "IDN302G-nemandi (nafn@hi.is)",
}
if os.environ.get("GITHUB_TOKEN"): # sama og gh_auth í R
hausar["Authorization"] = "Bearer " + os.environ["GITHUB_TOKEN"]
svar = requests.get(
"https://api.github.com/repos/HI-IDN/IDN302G",
headers=hausar,
timeout=30,
)
svar.status_code200
svar.headers["Content-Type"]'application/json; charset=utf-8'
Til að sjá hvernig slóðin varð eftir að færibreytur bættust við:
svar = requests.get(
"https://api.github.com/search/repositories",
params={"q": "quarto", "per_page": 5},
timeout=30,
)
print(svar.url)https://api.github.com/search/repositories?q=quarto&per_page=5
Notaðu req_url_query() (R) eða params= (Python) frekar en að líma saman streng. Þeir sjá um URL-kóðun sérstafa, svo "jarðskjálfti á Reykjanesi" virkar án þess að þú hugsir út í það.
timeout
Í Python bíður requests að eilífu ef þjónustan svarar ekki — skýrslan hangir án villu. Gefðu alltaf timeout=. httr2 hefur sjálfgefið tímamark, svo þar er þetta valfrjálst.
Frá svari yfir í töflu
library(dplyr)
leit <- request("https://api.github.com") |>
req_url_path("search", "repositories") |>
req_url_query(q = "quarto language:R", sort = "stars", per_page = 10) |>
req_user_agent("IDN302G-nemandi") |>
req_headers(Authorization = gh_auth) |>
req_perform() |>
resp_body_json(simplifyVector = TRUE)
leit$items |>
select(full_name, stargazers_count, language, updated_at) |>
mutate(updated_at = as.Date(updated_at)) |> # ISO-tímastimpill -> dagsetning
arrange(desc(stargazers_count)) full_name stargazers_count language updated_at
1 benmarwick/rrtools 721 R 2026-08-06
2 gadenbuie/epoxy 217 R 2026-08-05
3 surveydown-dev/surveydown 180 R 2026-08-20
4 quarto-dev/quarto-r 161 R 2026-07-01
5 andrewheiss/quarto-wordcount 151 R 2026-08-24
6 kazuyanagimoto/quarto-awesomecv-typst 98 R 2026-08-21
7 nrennie/30DayChartChallenge 76 R 2026-04-24
8 ropensci-review-tools/babelquarto 62 R 2026-08-11
9 matt-dray/quartostamp 62 R 2026-08-12
10 ekholme/gcp_quarto_api 53 R 2025-10-06
import requests
import pandas as pd
svar = requests.get(
"https://api.github.com/search/repositories",
params={"q": "quarto language:R", "sort": "stars", "per_page": 10},
headers=hausar,
timeout=30,
)
svar.raise_for_status()
d = svar.json()
df = pd.json_normalize(d["items"])
df["updated_at"] = pd.to_datetime(df["updated_at"]).dt.date # -> dagsetning
df[["full_name", "stargazers_count", "language", "updated_at"]] \
.sort_values("stargazers_count", ascending=False) full_name stargazers_count language updated_at
0 benmarwick/rrtools 721 R 2026-08-06
1 gadenbuie/epoxy 217 R 2026-08-05
2 surveydown-dev/surveydown 180 R 2026-08-20
3 quarto-dev/quarto-r 161 R 2026-07-01
4 andrewheiss/quarto-wordcount 151 R 2026-08-24
5 kazuyanagimoto/quarto-awesomecv-typst 98 R 2026-08-21
6 nrennie/30DayChartChallenge 76 R 2026-04-24
7 ropensci-review-tools/babelquarto 62 R 2026-08-11
8 matt-dray/quartostamp 62 R 2026-08-12
9 ekholme/gcp_quarto_api 53 R 2025-10-06
pd.json_normalize() flettir líka út hreiðruð svæði: owner.login verður eigin dálkur.
Taflan að ofan var sótt þegar þessi síða var síðast byggð. Stjörnur og updated_at breytast stöðugt, svo þínar tölur verða aðrar — og röðin líka. Það er einmitt eðli gagna sem sótt eru um vefþjónustu: þau eru ekki föst, og þess vegna skiptir máli að skrá hvenær var sótt (sjá góða starfshætti).
Villur
httr2 kastar sjálfgefið villu ef stöðukóði er 4xx eða 5xx. Það er rétt hegðun.
Þurfirðu að bregðast við sjálf/ur:
svar <- request("https://api.github.com/repos/HI-IDN/ekki-til") |>
req_headers(Authorization = gh_auth) |>
req_error(is_error = \(resp) FALSE) |> # ekki kasta, ég sé um þetta
req_perform()
resp_status(svar)[1] 404
if (resp_status(svar) == 404) {
message("Geymslan fannst ekki — athugaðu nafnið.")
} else {
d <- resp_body_json(svar, simplifyVector = TRUE)
}Geymslan fannst ekki — athugaðu nafnið.
requests kastar ekki villu sjálfkrafa — svar.status_code getur verið 404 og forritið haldið glaðlega áfram. Þú verður að kalla á raise_for_status():
svar = requests.get("https://api.github.com/repos/HI-IDN/IDN302G",
headers=hausar, timeout=30)
svar.raise_for_status() # kastar HTTPError ef 4xx/5xx
d = svar.json()Þessi tekst (200) og heldur áfram þegjandi. Kalliðu á slóð sem er ekki til fellur það svona:
requests.exceptions.HTTPError: 404 Client Error: Not Found for url:
https://api.github.com/repos/HI-IDN/ekki-til
Eða meðhöndla sjálf/ur:
svar = requests.get("https://api.github.com/repos/HI-IDN/ekki-til",
headers=hausar, timeout=30)
print(svar.status_code)404
print(svar.json()["message"])Not Found
if svar.status_code == 404:
print("Geymslan fannst ekki — athugaðu nafnið.")
else:
svar.raise_for_status()
d = svar.json()Geymslan fannst ekki — athugaðu nafnið.
Freistingin er að vefja allt í try() / try: svo skýrslan brotni ekki. Ekki gera það. Skýrsla sem klárast með röngum eða tómum gögnum er verri en skýrsla sem stoppar — því enginn tekur eftir villunni. Láttu hana falla og lagaðu ástæðuna.
Þetta á sérstaklega við í Python, þar sem 200 er ekki sjálfgefið athugað.
Endurtilraunir og álagsmörk
Sé þjónustan niðri (5xx) eða þú kominn yfir álagsmörk (429) er rétt viðbragð að bíða og reyna aftur.
svar <- request("https://api.github.com/repos/HI-IDN/IDN302G") |>
req_headers(Authorization = gh_auth) |>
req_retry(max_tries = 3) |>
req_throttle(rate = 30 / 60) |> # í mesta lagi 30 beiðnir á mínútu
req_perform()req_retry() virðir Retry-After-hausinn og bíður sjálfkrafa réttan tíma.
import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
retry = Retry(
total=3,
backoff_factor=1, # 1s, 2s, 4s …
status_forcelist=[429, 500, 502, 503, 504],
respect_retry_after_header=True,
)
s = requests.Session()
s.headers.update(hausar) # auðkenning fylgir öllum beiðnum
s.mount("https://", HTTPAdapter(max_retries=retry))
svar = s.get("https://api.github.com/repos/HI-IDN/IDN302G", timeout=30)Session heldur líka tengingunni opinni milli beiðna, sem er hraðvirkara þegar þú sækir margt.
Allar síður
svor <- request("https://api.github.com") |>
req_url_path("repos", "HI-IDN", "IDN302G", "commits") |>
req_url_query(per_page = 100) |>
req_headers(Authorization = gh_auth) |>
req_perform_iterative(
next_req = iterate_with_link_url(rel = "next"),
max_reqs = 10
)
commits <- svor |>
lapply(resp_body_json, simplifyVector = TRUE) |>
bind_rows()
nrow(commits)[1] 118
requests les Link-hausinn sjálfkrafa í svar.links:
url = "https://api.github.com/repos/HI-IDN/IDN302G/commits"
params = {"per_page": 100}
allt = []
for _ in range(10): # öryggisventill
svar = s.get(url, params=params, timeout=30)
svar.raise_for_status()
allt.extend(svar.json())
if "next" not in svar.links:
break
url = svar.links["next"]["url"]
params = None # slóðin ber þær nú þegar
df = pd.json_normalize(allt)
len(df)118
Alls 118 commit: fyrsta síðan skilaði 100 og sú næsta 18. Link-hausinn sagði hvenær var hætt. Hefðirðu ekki farið gegnum allar síður hefðirðu misst af 18 commitum án þess að nokkuð benti til þess.
max_reqs (R) og range(10) (Python) eru ekki skraut. Án þeirra getur ein villa sótt gögn þar til þjónustan lokar á þig — eða lykkjan hættir aldrei.
Að geyma svarið
Sæktu ekki sömu gögnin í hvert sinn sem skýrslan er þýdd. Tvær leiðir: láta pakkann halda skyndiminni, eða vista svarið sjálf/ur í skrá.
library(jsonlite)
# 1) skyndiminni: endurnýtir svar sem þjónustan segir óbreytt
req <- request("https://api.github.com/repos/HI-IDN/IDN302G") |>
req_cache(tempdir())
# 2) vista svarið sjálf/ur, ásamt tímastimpli
d <- resp_body_json(svar, simplifyVector = TRUE)
dir.create("data", showWarnings = FALSE)
write_json(list(gogn = d, sott = format(Sys.time(), "%Y-%m-%dT%H:%M:%SZ", tz = "UTC")),
"data/github_raw.json", auto_unbox = TRUE, pretty = TRUE)Lesið til baka með read_json("data/github_raw.json", simplifyVector = TRUE).
import json, datetime, pathlib
# vista svarið, ásamt tímastimpli
raw = {
"gogn": svar.json(),
"sott": datetime.datetime.now(datetime.timezone.utc).isoformat(),
}
pathlib.Path("data").mkdir(exist_ok=True)
pathlib.Path("data/github_raw.json").write_text(
json.dumps(raw, ensure_ascii=False, indent=2), encoding="utf-8"
)Lesið til baka með json.loads(pathlib.Path("data/github_raw.json").read_text("utf-8")).
Þarftu skyndiminni sjálfkrafa er pip install requests-cache einfaldasta leiðin.
Bæði málin eiga eigið snið sem geymir hlutinn nákvæmlega eins og hann er: .rds í R (saveRDS() / readRDS()) og pickle í Python (pickle.dumps() / pickle.loads()).
| JSON | .rds / pickle |
|
|---|---|---|
| Læsilegt í textaritli | já | nei |
git diff sýnir breytinguna |
já | nei, bara að skráin breyttist |
| Annað mál getur lesið | já | nei |
| Geymir gerðir nákvæmlega | nei | já |
Innfæddu sniðin geyma datetime, set, data.frame, eigin klasa — allt sem JSON ræður ekki við. Þess vegna þurfti .isoformat() (Python) og format(...) (R) á tímastimpilinn hér að ofan: JSON á enga dagsetningagerð.
Notaðu samt JSON þegar skráin á að fara í Git, til samstarfsfólks eða inn í skýrslu sem einhver annar keyrir. Innfædda sniðið er fyrir þitt eigið millistig, þegar hluturinn er flókinn og þú ætlar að lesa hann aftur í sama máli.
pickle.loads() á skrá sem þú treystir ekki
pickle getur keyrt kóða þegar hlutur er lesinn, svo skrá frá ókunnugum getur tekið yfir vélina þína. readRDS() í R er ekki alveg jafn varasamt en á sömu varúð skilið.
Þetta er önnur ástæða fyrir því að JSON og CSV eru sjálfgefna valið þegar gögn fara á milli manna.
Seinni leiðin — að vista svarið — er sú sem hentar skýrslum best: greiningin verður endurtakanleg því hún er ekki lengur háð því að þjónustan sé uppi eða hafi ekki breyst.
Þurfirðu ekki hráu gögnin sjálf, heldur bara að skýrslan endurbyggist án þess að sækja aftur, þá býður Quarto upp á execute: freeze: auto. Þá eru niðurstöður kóðabútanna frystar og búturinn keyrður aftur aðeins ef hann breytist — sem þýðir líka að CI þarf ekki að setja upp R eða Python. Sjá birtingu með GitHub Pages.
Þessi bók frystir ekki: GitHub Actions setur upp R og Python og keyrir bútana frá grunni, svo netbeiðnirnar hér eru raunverulega sendar í hverri byggingu. Liggi þjónusta niðri fellur byggingin með tímamörkum og villuboðum — þá keyrir maður hana einfaldlega aftur.
Undantekningin er beiðni sem krefst persónulegs aðgangslykils, eins og TMDB-dæmið í góðum starfsháttum. Slíkur lykill á ekki heima í opinni geymslu, svo sá bútur er eval: false.
GitHub-beiðnirnar eru annað mál: GitHub Actions býr sjálfkrafa til tímabundinn GITHUB_TOKEN sem má senda með í env:. Hann lyftir álagsmörkunum úr 60 í 5.000 beiðnir á klukkustund og rennur út þegar keyrslunni lýkur, svo hann er óhættur í opinni geymslu.
Hvað á heima í geymslunni?
Hér þarf dómgreind. Hrátt svar frá vefþjónustu er oftast margfalt stærra en það sem þú notar — GitHub skilar 86 lyklum þegar þú vildir einn. Að henda öllu inn í Git-geymsluna þýðir að þú ert að útgáfustýra rusli: skránum fjölgar, git clone hægist, og enginn sér hvað breyttist því hávaðinn drekkir merkingunni.
Vinnureglan er í þremur skrefum:
1. Vistaðu hráa svarið — utan Git. Þú þarft það meðan þú ert að átta þig á gögnunum, en það á ekki heima í sögunni. Settu möppuna í .gitignore strax:
# .gitignore
data/raw/2. Skrifaðu skriftu sem sækir og strípar. Ein skrá sem sækir svarið og skilar aðeins því sem greiningin þarf. Skriftan sjálf fer í Git — hún er uppskriftin:
# scripts/saekja_geymslur.R
library(httr2); library(dplyr); library(readr); library(jsonlite)
d <- request("https://api.github.com/search/repositories") |>
req_url_query(q = "quarto language:R", per_page = 100) |>
req_user_agent("IDN302G-nemandi (nafn@hi.is)") |>
req_perform() |>
resp_body_json(simplifyVector = TRUE)
# hrátt: utan Git, til vara meðan unnið er
dir.create("data/raw", recursive = TRUE, showWarnings = FALSE)
write_json(d, "data/raw/geymslur.json", auto_unbox = TRUE)
# strípað: aðeins dálkarnir sem greiningin notar
d$items |>
select(full_name, stargazers_count, language, updated_at) |>
mutate(sott = Sys.Date()) |>
write_csv("data/geymslur.csv")# scripts/saekja_geymslur.py
import datetime, json, pathlib
import requests, pandas as pd
svar = requests.get(
"https://api.github.com/search/repositories",
params={"q": "quarto language:R", "per_page": 100},
headers={"User-Agent": "IDN302G-nemandi (nafn@hi.is)"},
timeout=30,
)
svar.raise_for_status()
d = svar.json()
# hrátt: utan Git, til vara meðan unnið er
pathlib.Path("data/raw").mkdir(parents=True, exist_ok=True)
pathlib.Path("data/raw/geymslur.json").write_text(
json.dumps(d, ensure_ascii=False), encoding="utf-8"
)
# strípað: aðeins dálkarnir sem greiningin notar
(pd.json_normalize(d["items"])
[["full_name", "stargazers_count", "language", "updated_at"]]
.assign(sott=datetime.date.today())
.to_csv("data/geymslur.csv", index=False))3. Committaðu aðeins strípuðu skrána. data/geymslur.csv er lítil, læsileg og git diff sýnir raunverulega breytingu milli útgáfna. Hráa JSON-skráin er í .gitignore og enginn saknar hennar — skriftan endurskapar hana hvenær sem er.
CSV er textasnið, svo Git getur borið saman útgáfur línu fyrir línu. Tvíundarsnið (.rds, .xlsx, .parquet) geymast sem einn ógegnsær kubbur — þú sérð að skráin breyttist en ekki hverju. Fyrir hóflega stór gögn í Git-geymslu er CSV nær alltaf rétta valið.
Vaxi gögnin — margar skrár, tengsl milli þeirra, endurteknar fyrirspurnir — er næsta skref að setja þau í einfaldan gagnagrunn í stað þess að hlaða CSV-skrám í minni í hvert sinn. Það er efni SQL-lotanna síðar í áfanganum, þar sem við skoðum hvernig CSV-skrár verða að töflum sem hægt er að spyrja.
Óháð stærð eiga þessi gögn aldrei heima í geymslunni: aðgangslyklar, persónugreinanleg gögn, og allt sem skilmálar þjónustunnar banna þér að dreifa. Sjá góða starfshætti.
Æfing
- Sæktu upplýsingar um geymsluna þína á GitHub og prentaðu
updated_at. - Staðfestu að slóðin sé eins og þú bjóst við —
req_dry_run()(R) eðaprint(svar.url)(Python). - Sæktu síðustu 100 commits og teldu hversu margir eru frá þér.
- Kallaðu á slóð sem er ekki til og staðfestu að þú fáir
404— ekki þögult tómt svar. Í Python: hvað gerist ef þú gleymirraise_for_status()?