Sådan crawler Domæneskanner.dk registrerede .dk domæner

9 Oct 2025

Hos Domæneskanner.dk arbejder vi løbende på at kortlægge og analysere danske domæner. For at holde databasen opdateret crawler vi løbende allerede registrerede .dk-domæner for at finde nye aktive hjemmesider, afdække relationer mellem domæner og indsamle data til analyser. Her gennemgår vi, hvordan vores crawler fungerer i praksis.

Automatiseret domænescanning med Python og Selenium

For at kunne gennemgå store mængder hjemmesider effektivt bruger vi Python kombineret med Selenium WebDriver. Det gør det muligt at indlæse sider i en headless browser (uden grafik) og analysere links, som fører videre til andre danske domæner.

Vores crawler henter løbende tilfældige, registrerede domæner fra API’et på Domæneskanner.dk og starter derfra en scanning. For hver side analyseres alle links for at finde nye .dk-domæner. Disse sendes derefter tilbage til Domæneskanner.dk, hvor systemet registrerer dem og eventuelt føjer dem til databasen.

Sådan fungerer scriptet

Herunder ses et udsnit af det Python-script, der står for selve crawl-processen. Scriptet bruger Selenium i headless Firefox-mode, kombineret med tldextract til domæneanalyse og requests til API-kommunikation:

from selenium import webdriver
from selenium.common.exceptions import WebDriverException, TimeoutException
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.common.by import By
import tldextract
from urllib.parse import urljoin, urlparse
import psutil
import requests
import signal
import time
import os
from dotenv import load_dotenv

# Load .env
load_dotenv()
BEARER_TOKEN = os.getenv("BEARER")

def get_random_taken_domain():
    """Fetch a random taken domain from your Laravel API using Bearer auth"""
    headers = {"Authorization": f"Bearer {BEARER_TOKEN}"} if BEARER_TOKEN else {}
    try:
        response = requests.get(API_RANDOM_DOMAIN, headers=headers, timeout=10)
        response.raise_for_status()
        data = response.json()
        return data['data']['domain']
    except requests.RequestException as e:
        print(f"Error fetching random domain: {e}")
        return None

def kill_existing_crawlers():
    current_pid = os.getpid()
    parent_pid = os.getppid()
    for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
        try:
            pid = proc.info['pid']
            cmdline = proc.info['cmdline'] or []
            # Skip self and parent (xvfb-run)
            if pid in (current_pid, parent_pid):
                continue
            # Check if 'crawler.py' is in cmdline
            if any('crawler.py' in arg for arg in cmdline):
                print(f"Killing existing crawler process {pid}")
                os.kill(pid, signal.SIGTERM)
        except (psutil.NoSuchProcess, psutil.AccessDenied):
            continue

kill_existing_crawlers()

def is_dk_domain(url):
    ext = tldextract.extract(url)
    return ext.suffix == "dk"


def should_skip(url):
    parsed = urlparse(url)
    return parsed.path.lower().endswith(SKIP_EXTENSIONS)


def notify_new_domain(domain):
    """Send GET request to domæneskanner.dk"""
    url = f"https://domæneskanner.dk/domæne/{domain}"
    try:
        response = requests.get(url, timeout=10)
        if response.status_code == 200:
            print(f"Notified domæneskanner.dk for {domain}")
        else:
            print(f"Failed to notify for {domain}, status code: {response.status_code}")
    except requests.RequestException as e:
        print(f"Error notifying for {domain}: {e}")


def extract_links(url):
    links = set()
    try:
        driver.get(url)
        time.sleep(delay)  # Wait for JS to load
        a_tags = driver.find_elements(By.TAG_NAME, "a")
        for a_tag in a_tags:
            link = a_tag.get_attribute("href")
            if link and urlparse(link).scheme in ["http", "https"]:
                if is_dk_domain(link) and not should_skip(link):
                    links.add(urljoin(url, link))
    except (WebDriverException, TimeoutException) as e:
        # Gracefully handle pages that fail to load
        print(f"Skipped {url}: could not load page ({e.__class__.__name__})")
    except Exception as e:
        print(f"Unexpected error accessing {url}: {e}")
    return links


def crawl(url, start_domain=None):
    if len(visited_urls) >= max_pages or url in visited_urls:
        return

    visited_urls.add(url)

    links = extract_links(url)

    # Extract and store .dk domains
    for link in links:
        domain = tldextract.extract(link).fqdn
        if domain not in found_domains:
            found_domains.add(domain)
            print(f"Found new domain: {domain}")

            # Skip start domain
            if start_domain and domain == start_domain:
                print(f"Skipping notify for start domain: {domain}")
                continue

            # Skip domains in SKIP_DOMAINS
            if domain in SKIP_DOMAINS:
                print(f"Skipping domain in skip list: {domain}")
                continue

            # Stop if domain is in STOP_DOMAINS
            if domain in STOP_DOMAINS:
                print(f"Domain {domain} is in the stop list. Exiting crawl.")
                driver.quit()
                exit(0)

            # Notify normally
            notify_new_domain(domain)
            time.sleep(4)

    # Recursively crawl only .dk links that are not files and not in SKIP_DOMAINS
    for link in links:
        domain = tldextract.extract(link).fqdn
        if link not in visited_urls and domain not in SKIP_DOMAINS:
            time.sleep(delay)
            crawl(link, start_domain=start_domain)

# Set of found .dk domains
found_domains = set()
visited_urls = set()

# Crawl settings
max_pages = 100  # Limit number of pages to crawl
delay = 3  # Delay between requests (seconds)

# Setup Firefox in headless mode
options = Options()
options.headless = True
options.set_preference("layers.acceleration.disabled", True)
options.set_preference("gfx.webrender.all", False)
options.set_preference("gfx.x11-egl.force-enabled", False)
driver = webdriver.Firefox(options=options)
driver.set_page_load_timeout(15)

# List of domains that should stop the crawl
STOP_DOMAINS = {"dandomain.dk", "scannet.com", "www.dandomain.dk"}

# List of domains to skip (will not notify or crawl deeper, but continue)
SKIP_DOMAINS = {'datatilsynet.dk', 'punktum.dk', 'webhosting.dk', 'www.datatilsynet.dk'}

# File extensions to skip
SKIP_EXTENSIONS = (
    ".pdf", ".jpg", ".jpeg", ".png", ".gif", ".zip", ".rar",
    ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
    ".mp4", ".mp3", ".svg", ".ico"
)

API_RANDOM_DOMAIN = "https://domæneskanner.dk/api/domain/random-taken"


if __name__ == "__main__":
    start_domain = get_random_taken_domain()
    if start_domain:
        start_url = f"https://{start_domain}"
        print(f"Starting crawl from: {start_url}")
        crawl(start_url, start_domain=start_domain)
    else:
        print("Could not fetch a starting domain from the API.")

    driver.quit()

    print("All found .dk domains:")
    for domain in sorted(found_domains):
        print(domain)

Crawleren kører i et loop, hvor den automatisk afslutter gamle processer, hvis der opstår fejl eller overlap, så systemet altid kun har én aktiv scanning i gang ad gangen. Samtidig er der indbygget forsinkelser mellem forespørgsler, så vi respekterer servere og undgår overbelastning.

Formålet med crawlingen

Formålet er ikke at indeksere indhold som en søgemaskine, men derimod at kortlægge relationer mellem registrerede danske domæner. På den måde kan Domæneskanner.dk give brugerne bedre indblik i, hvilke domæner der er aktive, hvordan de hænger sammen, og hvilke trends der præger det danske domænemarked.

Et levende billede af det danske internet

Med denne teknologi opbygger Domæneskanner.dk et dynamisk billede af danske hjemmesider og deres udvikling over tid. Det giver mulighed for at følge nye trends, identificere sammenhænge mellem virksomheder og se, hvordan domænelandskabet ændrer sig.

Vidste du? Du kan selv søge i registrerede .dk-domæner direkte på Domæneskanner.dk og se, hvilke domæner der er aktive lige nu.

Relateret indhold

Sådan crawler jeg samtlige danske domænenavne

Sådan crawler jeg samtlige danske domænenavne

Udgivet 2025-09-24

Find og overvåg allerede registrerede .dk-domæner. Få historik og whois-data, når domæner bliver ledige.

Læs indlægget →
Privatlivspolitik

Privatlivspolitik

Udgivet 2024-05-08 — Opdateret 2024-05-09

Privatlivspolitikken for hjemmesiden ppsa.dk. Bliv klogere på hvordan vi bruger dine data og hvad dine rettigheder er.

Læs indlægget →
Hvordan vil din investering gro?

Hvordan vil din investering gro?

Udgivet 2024-05-02 — Opdateret 2024-05-09

Få et bedre indblik i hvordan og hvor meget din investering vil gro henover en lang årrække. Ved at investere din opsparing får du et langt større afkast.

Læs indlægget →