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.

5 min Niveau: Avanceret ✓ Kode testet

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:

EntitetPrimærnøgleHvad det er
postnummernrPostnumre
kommunekodeKommuner
navngivenvejidVeje med navn
vejstykkekommunekode + vejkodeVejens del i én kommune
adgangsadresseidBygningens indgang med husnummer og koordinat
adresseidDen 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å.)

JSON#seneste-response
{"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:

Python#udtraek-peek
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:

JSON#adresse-row
{"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:

Python#udtraek-sqlite
"""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:

Text#udtraek-output
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:

SQL#postgres
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)

Prøv det med en gratis nøgle

2 000 kald/måned på gratisplanen. Intet kreditkort.

Opret konto →