Replikering, del 1: hent et fuldt udtræk ind i din database
Hent alle postnumre, veje og adresser som stream, gem dem i SQLite eller Postgres, og gem sekvensnummeret, du skal synkronisere videre fra.
Har du brug for alle danske adresser i din egen database, fx til joins, batchjobs eller offline brug, så bruger du replikerings-API'et. Det sker i to trin: først et fuldt udtræk af hver entitet, derefter løbende hændelser med ændringerne. Denne guide dækker første trin. Del 2 dækker hændelserne.
Endpoints er de samme som i DAWA's replikering: /replikering/senestesekvensnummer, /replikering/udtraek?entitet=… og /replikering/haendelser?entitet=…. Vi leverer dem selv fra vores egen kopi af adresseregistret. Alle kald undtagen senestesekvensnummer kræver en nøgle, og et udtræk tæller som ét kald, uanset hvor mange rækker det indeholder.
Du starter altid med et fuldt udtræk. Vores hændelseslog går kun tilbage til 2026, og kilden skiftede fra DAWA til Datafordeleren, da DAWA lukkede 1. oktober 2026. Du kan altså ikke bygge en kopi op af hændelser alene eller fortsætte fra en gammel DAWA-cursor.
1. Vælg entiteter og rækkefølge
Udtrækket findes for disse entiteter: postnummer, kommune, region, sogn, retskreds, politikreds, opstillingskreds, afstemningsomraade, supplerendebynavn, navngivenvej, vejstykke, adgangsadresse, adresse og bygning. De fleste skal kun bruge seks:
| Entitet | Primærnøgle | Hvad det er |
|---|---|---|
postnummer | nr | Postnumre |
kommune | kode | Kommuner |
navngivenvej | id | Veje med navn |
vejstykke | kommunekode + vejkode | Vejens del i én kommune |
adgangsadresse | id | Bygningens indgang med husnummer og koordinat |
adresse | id | Den fulde adresse med etage og dør |
Hent dem i den rækkefølge, så det, der bliver refereret til, er på plads først. En adresse peger på sin adgangsadresse via adgangsadresseid, og en adgangsadresse peger på postnr, kommunekode og navngivenvej_id. Se Adresse eller adgangsadresse for datamodellen.
2. Gem sekvensnummeret, før du starter
Kald /replikering/senestesekvensnummer før det første udtræk og gem tallet. Det er dit startpunkt for hændelser bagefter: du henter hændelser fra det tal + 1. (/replikering/senesteSekvensnummer med stort S virker også.)
{"sekvensnummer": 207940624, "tidspunkt": "2026-11-02T12:23:10.000Z"}Rækkefølgen er vigtig. Udtrækket af adresse kan tage et kvarter, og data ændrer sig imens. Tager du sekvensnummeret før udtrækket, henter du bagefter alle ændringer fra det tidspunkt. Nogle af dem står allerede i udtrækket, men det gør ikke noget, fordi du gemmer med upserts. Tager du sekvensnummeret efter udtrækket, kan du miste ændringer, der skete undervejs.
3. Se formatet
Som standard er udtrækket ét JSON-array, sendt som en stream, ligesom hos DAWA. Sætter du &ndjson på URL'en, får du NDJSON i stedet: én JSON-række pr. linje (application/x-ndjson). NDJSON er nemmest at læse række for række, så eksemplerne her bruger det. Hent de første tre postnumre:
import json import os import requests BASE = "https://api.danadresse.dk" H = {"X-Api-Key": os.environ["DANADRESSE_API_KEY"]} seneste = requests.get(f"{BASE}/replikering/senestesekvensnummer", headers=H, timeout=10).json() print("seneste sekvensnummer:", seneste["sekvensnummer"]) # ndjson: én række pr. linje. Uden den får du ét stort JSON-array. with requests.get(f"{BASE}/replikering/udtraek", params={"entitet": "postnummer", "ndjson": ""}, headers=H, stream=True, timeout=(5, 30)) as r: r.raise_for_status() for i, linje in enumerate(r.iter_lines()): print(json.loads(linje)) if i == 2: break
Rækkerne er kolonnerne i vores egne tabeller, så feltnavnene er ikke præcis DAWA's attributnavne fra replikeringen. /replikering/datamodel viser felterne og primærnøglen for hver entitet; læs den i stedet for at gætte på navne. En adresse-række ser sådan ud:
{"id": "000021c5-e9ee-411d-b2d8-ec9161780ccd", "status": 1, "adgangsadresseid": "0a3f5086-e9cd-32b8-e044-0003ba298018", "etage": "2", "doer": "tv", "oprettet": "2018-08-02T12:47:31", "aendret": "2018-08-02T12:47:31"}Bemærk, at udtrækket bruger feltnavne uden æ, ø og å (doer, aendret), og at adgangsadresse har både lat/lon (WGS84) og etrs89_x/etrs89_y (UTM32). Udtrækket indeholder alle statusser: 1 gældende, 3 foreløbig, 2 nedlagt og 4 henlagt. Filtrér på status, hvis du kun vil vise aktive adresser.
4. Stream udtrækket ind i SQLite
Scriptet herunder henter hver entitet som stream og skriver i batches af 5.000 rækker. Hele rækken gemmes som JSON, så du ikke skal vedligeholde et skema for hvert felt, og primærnøglen læses fra /replikering/datamodel:
"""foerste_udtraek.py: fuldt udtræk af adressedata ind i SQLite. python foerste_udtraek.py # alle entiteter i ENTITETER python foerste_udtraek.py postnummer kommune # kun disse """ import json import os import sqlite3 import sys import time import requests BASE_URL = os.environ.get("DANADRESSE_BASE_URL", "https://api.danadresse.dk") HEADERS = {"X-Api-Key": os.environ["DANADRESSE_API_KEY"]} # Rækkefølge: det, der bliver refereret til, før det, der refererer ENTITETER = ["postnummer", "kommune", "navngivenvej", "vejstykke", "adgangsadresse", "adresse"] BATCH = 5000 def opret_tabeller(db: sqlite3.Connection, entiteter: list[str]) -> None: for e in entiteter: # pk = primærnøglen som tekst; data = hele rækken som JSON db.execute(f"CREATE TABLE IF NOT EXISTS {e} (pk TEXT PRIMARY KEY, status INTEGER, data TEXT NOT NULL)") db.execute("CREATE TABLE IF NOT EXISTS replikering_cursor " "(entitet TEXT PRIMARY KEY, sekvensnummer INTEGER NOT NULL)") db.commit() def hent_json(path: str) -> dict: r = requests.get(BASE_URL + path, headers=HEADERS, timeout=(5, 30)) r.raise_for_status() return r.json() def hent_udtraek(db: sqlite3.Connection, entitet: str, noegle: list[str]) -> int: sql = (f"INSERT INTO {entitet} (pk, status, data) VALUES (?, ?, ?) " "ON CONFLICT(pk) DO UPDATE SET status = excluded.status, data = excluded.data") antal = 0 rows: list[tuple] = [] # stream=True: rækkerne skrives, mens de kommer, i stedet for at ligge i hukommelsen. # Read-timeout gælder mellem to bidder af svaret, ikke hele overførslen. # ndjson: én række pr. linje, så iter_lines() kan læse rækkerne én ad gangen. with requests.get(f"{BASE_URL}/replikering/udtraek", params={"entitet": entitet, "ndjson": ""}, headers=HEADERS, stream=True, timeout=(5, 120)) as r: r.raise_for_status() for linje in r.iter_lines(): if not linje: continue raekke = json.loads(linje) pk = "-".join(str(raekke[k]) for k in noegle) rows.append((pk, raekke.get("status"), json.dumps(raekke, ensure_ascii=False))) if len(rows) >= BATCH: db.executemany(sql, rows) db.commit() antal += len(rows) rows.clear() if rows: db.executemany(sql, rows) db.commit() antal += len(rows) return antal def main(entiteter: list[str]) -> None: db = sqlite3.connect("adresser.db") opret_tabeller(db, entiteter) model = hent_json("/replikering/datamodel") # primærnøgle pr. entitet # 1. Gem sekvensnummeret FØR udtrækket. Ændringer under udtrækket hentes # bagefter som hændelser og overskriver idempotent. start = hent_json("/replikering/senestesekvensnummer")["sekvensnummer"] print("start-sekvensnummer:", start) for entitet in entiteter: t0 = time.monotonic() antal = hent_udtraek(db, entitet, model[entitet]["key"]) # 2. Cursor pr. entitet: herfra skal hændelser hentes næste gang db.execute("INSERT INTO replikering_cursor (entitet, sekvensnummer) VALUES (?, ?) " "ON CONFLICT(entitet) DO UPDATE SET sekvensnummer = excluded.sekvensnummer", (entitet, start)) db.commit() print(f"{entitet}: {antal} rækker på {time.monotonic() - t0:.0f} s") if __name__ == "__main__": main(sys.argv[1:] or ENTITETER)
Typisk output for de små entiteter:
start-sekvensnummer: 207940624 postnummer: 1108 rækker på 0 s kommune: 99 rækker på 1 s navngivenvej: 112484 rækker på 31 s vejstykke: 113877 rækker på 29 s
adgangsadresse og adresse har millioner af rækker. Regn med 10 til 30 minutter for hver, afhængigt af din forbindelse og din database. Afbrydes forbindelsen undervejs, så kør scriptet igen for den entitet. Upserts gør, at rækker, du allerede har, bare bliver overskrevet.
Hvorfor stream? requests.get(...).json() på hele adresse-udtrækket ville læse flere gigabyte ind i hukommelsen, før den første række blev gemt. Med &ndjson behandler iter_lines() én linje ad gangen. Foretrækker du standardformatet med JSON-array, så læs det med en streamende JSON-parser som ijson (ijson.items(r.raw, "item")), aldrig med .json().
5. Postgres i stedet for SQLite
I Postgres er jsonb og ON CONFLICT det naturlige valg. Brug || i opdateringen, så felter fra senere hændelser bliver flettet ind i stedet for at erstatte hele rækken:
CREATE TABLE adresse ( pk text PRIMARY KEY, status smallint, data jsonb NOT NULL ); CREATE INDEX adresse_adgangsadresseid ON adresse ((data->>'adgangsadresseid')); -- Én række pr. linje i udtrækket (psycopg: cur.executemany(sql, rows)) INSERT INTO adresse (pk, status, data) VALUES (%s, %s, %s::jsonb) ON CONFLICT (pk) DO UPDATE SET status = EXCLUDED.status, data = adresse.data || EXCLUDED.data; CREATE TABLE replikering_cursor (entitet text PRIMARY KEY, sekvensnummer bigint NOT NULL);
Til meget store mængder er COPY til en midlertidig tabel efterfulgt af én INSERT … SELECT … ON CONFLICT hurtigere end executemany.
6. Hold kopien opdateret
Når udtrækket er færdigt, står start-sekvensnummeret i replikering_cursor for hver entitet. Derfra henter du ændringer med /replikering/haendelser?entitet=…. Det er beskrevet i Replikering, del 2: hændelser. Vil du have besked, når der er nyt, i stedet for at spørge regelmæssigt, så se Webhooks.
Entiteter, der sjældent ændrer sig og ikke har løbende hændelser (fx kommune, region og sogn), kan du hente igen med et nyt udtræk, fx en gang om måneden.
Data kommer fra Klimadatastyrelsen via Datafordeleren. Gemmer du en kopi, så husk kildeangivelsen "Indeholder data fra Klimadatastyrelsen (CC BY 4.0)".
Relaterede guides
Markdown-kilde Fejl i guiden? Skriv til os API-reference (Swagger)