#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ sovp-benchmark-2026.py — erzeugt den veröffentlichten SOVP-Benchmark-Datensatz 2026. Aufruf: python3 -I sovp-benchmark-2026.py > sovp-benchmark-2026.json Das Skript öffnet die Datenbank ausschließlich lesend (mode=ro) und schreibt nichts. Es ist deterministisch: fester Stichtag, feste Listen, fester Tiebreak, nearest-rank- Perzentile. Zweimal aufgerufen liefert es dasselbe JSON, bis auf meta.erzeugt_am. Ausgegeben werden ausschließlich Aggregate. Es gibt im Datensatz keinen Domainnamen, keinen Einzelwert und keinen Höchstwert je Liste; der Selbsttest am Ende bricht ab, wenn doch ein Hostname im Ergebnis steht. Textwerte stehen mit echten Umlauten und ß in UTF-8 (ensure_ascii=False). Schlüsselnamen bleiben bewusst ASCII, damit sie sich ohne Normalisierung ansprechen lassen. Datensatz und Skript stehen unter CC BY 4.0. """ import hashlib import json import math import re import sqlite3 import sys from collections import Counter, defaultdict from datetime import datetime, timezone from pathlib import Path DB = "/var/data/sovp-benchmark.db" STICHTAG = "2026-09-05T23:59:59" SONDERLISTEN = ("PILOT-0902", "FI_MUSTER_2026_09_03") JUNI_BIS = "2026-07-01" # Listen, die nicht als eigener Sektorwert veröffentlicht werden. Die Musterliste hat # weniger als 10 Domains und war keine Markterhebung; ihre Domains zählen in allen # Gesamtwerten mit, erscheinen aber in keinem Listeneintrag. LISTEN_NICHT_VEROEFFENTLICHT = ("FI_MUSTER_2026_09_03",) # Reifegrade nach SOVP-Score. Die Linie bei 75 ordnet ein, sie ist keine Schwelle # des Protokolls (siehe regeln.hinweis_srs_75 im Datensatz). STUFEN = (("L0", None, 30), ("L1", 30, 45), ("L2", 45, 60), ("L3", 60, 75), ("L4", 75, None)) # Die vier gewichteten Bewertungsgruppen aus lib/sovp-scoring.mjs (GROUP_WEIGHTS). # Die nominalen Gewichte 25/35/22/8 summieren auf 90 % und werden durch 0,90 geteilt, # seit Authority am 01.08.2026 als Gewichtungsgruppe entfiel. BEWERTUNGSGRUPPEN = ( ("ki_readiness", "ai_readiness", 38.9), ("content_qualitaet", "content_quality", 27.8), ("infrastruktur", "infrastructure_score", 24.4), ("anchor", "anchor_score", 8.9), ) SIGNALGRUPPEN = ( ("meta_deklaration", "meta"), ("code_rendering", "code"), ("ueberschriften_struktur", "headings"), ("narrative_qualitaet", "narrative"), ("integration_apis", "integration"), ("dns_domain", "dns"), ("sicherheit_tls", "security"), ("alpn_protokolle", "alpn"), ("bilder_medien", "images"), ("links_navigation", "links"), ("ki_signale", "aiSignals"), ("semantisches_html", "semantic"), ("llms_txt_qualitaet", "llmsQuality"), ("agentische_bereitschaft", "agenticReadiness"), ) # Neutralwerte, die der Scanner einsetzte, wenn eine Gruppe gar nicht erhoben werden # konnte (lib/sovp-scoring.mjs, Kommentar vor WEIGHTED_GROUPS). NEUTRALWERTE = ( ("content_qualitaet", "content_quality", 50.0), ("authority", "authority_score", 50.0), ("ki_readiness", "ai_readiness", 25.0), ("infrastruktur", "infrastructure_score", 50.0), ("anchor", "anchor_score", 50.0), ) SQL_SNAPSHOT = """ WITH sonderlisten_domains AS ( SELECT DISTINCT domain FROM sovp_batch_scans WHERE scan_type = 'fullscan' AND list IN (?, ?) ), marktlisten_domains AS ( SELECT DISTINCT domain FROM sovp_batch_scans WHERE scan_type = 'fullscan' AND list NOT IN (?, ?) ), ausgeschlossen AS ( SELECT domain FROM sonderlisten_domains WHERE domain NOT IN (SELECT domain FROM marktlisten_domains) ), snapshot AS ( SELECT domain, list, sovereign_score, verdict, ai_readiness, content_quality, infrastructure_score, authority_score, anchor_score, raw_clusters, scanned_at, ROW_NUMBER() OVER ( PARTITION BY domain ORDER BY scanned_at DESC, id DESC ) AS rn FROM sovp_batch_scans WHERE scan_type = 'fullscan' AND scanned_at <= ? AND domain NOT IN (SELECT domain FROM ausgeschlossen) ) SELECT domain, list, sovereign_score, verdict, ai_readiness, content_quality, infrastructure_score, authority_score, anchor_score, raw_clusters, scanned_at FROM snapshot WHERE rn = 1 """ SQL_JUNI = """ SELECT domain, list, sovereign_score, verdict, ai_readiness, content_quality, infrastructure_score, authority_score, anchor_score, raw_clusters, scanned_at FROM sovp_batch_scans WHERE scan_type = 'fullscan' AND date(scanned_at) < ? """ SQL_TRANCO_LAUF = """ SELECT content_quality, authority_score, ai_readiness, infrastructure_score, anchor_score FROM sovp_batch_scans WHERE scan_type = 'fullscan' AND list = 'TRANCO1000' """ # ── Hilfsrechnungen ────────────────────────────────────────────────────────── def r2(x): """Auf zwei Nachkommastellen, als float. None bleibt None.""" return None if x is None else round(x + 0.0, 2) def mittel(werte): return (sum(werte) / len(werte)) if werte else None def stdabw(werte): """Standardabweichung der Grundgesamtheit (keine Stichprobenkorrektur: die Liste ist die Grundgesamtheit dieses Laufs, nicht eine Ziehung daraus).""" if len(werte) < 2: return None m = sum(werte) / len(werte) return math.sqrt(sum((w - m) ** 2 for w in werte) / len(werte)) def perzentil(werte, p): """nearest-rank, inklusiv, 1-basiert: Index = ceil(p/100 * n).""" if not werte: return None s = sorted(werte) return s[math.ceil(p / 100 * len(s)) - 1] def reifegrad(score): for name, unten, oben in STUFEN: if (unten is None or score >= unten) and (oben is None or score < oben): return name return None def gruppe_mittel(zeilen, feld): werte = [z[feld] for z in zeilen if z[feld] is not None] return {"n": len(werte), "durchschnitt": r2(mittel(werte))} def signalgruppen(zeilen): sammler = defaultdict(list) for z in zeilen: roh = z["raw_clusters"] if not roh: continue try: daten = json.loads(roh) except (ValueError, TypeError): continue if not isinstance(daten, dict): continue for schluessel, wert in daten.items(): if isinstance(wert, (int, float)) and not isinstance(wert, bool): sammler[schluessel].append(float(wert)) ergebnis = {} for name, schluessel in SIGNALGRUPPEN: werte = sammler.get(schluessel, []) ergebnis[name] = { "raw_clusters_key": schluessel, "n": len(werte), "durchschnitt": r2(mittel(werte)), } return ergebnis def je_liste(zeilen, je_messung=False, auslassen=()): """Aggregate je Liste. je_messung=True für den Juni-Lauf, in dem eine Domain in zwei Listen stehen und damit zweimal gemessen worden sein kann; dort werden die Zahl der Messungen und die Zahl der eindeutigen Domains getrennt ausgewiesen. Listen in `auslassen` erscheinen nicht im Ergebnis.""" gruppen = defaultdict(list) for z in zeilen: gruppen[z["list"]].append(z) ergebnis = {} for name in sorted(gruppen): if name in auslassen: continue g = gruppen[name] scores = [z["sovereign_score"] for z in g if z["sovereign_score"] is not None] ki_alle = [z["ai_readiness"] for z in g if z["ai_readiness"] is not None] ki_score = [z["ai_readiness"] for z in g if z["ai_readiness"] is not None and z["sovereign_score"] is not None] eintrag = {} if je_messung: eintrag["messungen"] = len(g) eintrag["domains"] = len(set(z["domain"] for z in g)) eintrag["mit_score"] = len(scores) eintrag["durchschnitt_srs"] = r2(mittel(scores)) eintrag["n_srs"] = len(scores) eintrag["durchschnitt_ki_readiness"] = r2(mittel(ki_alle)) eintrag["n_ki_readiness"] = len(ki_alle) # Wo nicht jede Zeile einen Score hat, beschreibt der SRS-Mittelwert eine andere # Teilmenge als der KI-Readiness-Mittelwert. Dann beide n ausweisen. if len(scores) != len(g): eintrag["durchschnitt_ki_readiness_nur_mit_score"] = r2(mittel(ki_score)) eintrag["n_ki_readiness_nur_mit_score"] = len(ki_score) eintrag["hinweis"] = ( f"Nur {len(scores)} von {len(g)} Einträgen haben einen Score " f"(Verdict INCOMPLETE bei den übrigen). Der SRS-Mittelwert beruht auf " f"diesen {len(scores)}, der KI-Readiness-Mittelwert auf allen {len(g)}; " f"der Wert nur für die Einträge mit Score steht daneben." ) ergebnis[name] = eintrag return ergebnis # ── Hauptteil ──────────────────────────────────────────────────────────────── def main(): skript = Path(__file__).resolve() skript_sha = hashlib.sha256(skript.read_bytes()).hexdigest() con = sqlite3.connect(f"file:{DB}?mode=ro", uri=True) con.row_factory = sqlite3.Row snap = [dict(z) for z in con.execute( SQL_SNAPSHOT, (*SONDERLISTEN, *SONDERLISTEN, STICHTAG))] juni = [dict(z) for z in con.execute(SQL_JUNI, (JUNI_BIS,))] tranco_lauf = [dict(z) for z in con.execute(SQL_TRANCO_LAUF)] db_mtime = datetime.fromtimestamp( Path(DB).stat().st_mtime, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") # ── September ──────────────────────────────────────────────────────────── sep_scores = [z["sovereign_score"] for z in snap if z["sovereign_score"] is not None] messdaten = Counter(z["scanned_at"][:10] for z in snap) verdicts_tag = defaultdict(Counter) for z in snap: verdicts_tag[z["scanned_at"][:10]][z["verdict"]] += 1 stufen = Counter(reifegrad(s) for s in sep_scores) september = { "stichtag": "2026-09-05", "methode": "v3.0", "domains": len(snap), "mit_score": len(sep_scores), "ohne_score": len(snap) - len(sep_scores), "durchschnitt_srs": r2(mittel(sep_scores)), "median_srs": r2(perzentil(sep_scores, 50)), "p25": r2(perzentil(sep_scores, 25)), "p50": r2(perzentil(sep_scores, 50)), "p75": r2(perzentil(sep_scores, 75)), "p90": r2(perzentil(sep_scores, 90)), "standardabweichung_srs": r2(stdabw(sep_scores)), "spanne_srs": [r2(min(sep_scores)), r2(max(sep_scores))], "anzahl_srs_ab_75": sum(1 for s in sep_scores if s >= 75), "anzahl_certified": sum(1 for z in snap if z["verdict"] == "CERTIFIED"), "messdaten": {tag: messdaten[tag] for tag in sorted(messdaten)}, "verdicts_nach_tag": {tag: dict(sorted(verdicts_tag[tag].items())) for tag in sorted(verdicts_tag)}, "verdicts_gesamt": dict(sorted(Counter(z["verdict"] for z in snap).items())), "reifegrade": {name: stufen.get(name, 0) for name, _, _ in STUFEN}, "hinweis_messdaten": ( "Der Name September bezeichnet den Stichtag, nicht den Messzeitraum: " "1.432 der 1.525 Domains wurden am 26. und 27.08.2026 gemessen, 92 zwischen " "dem 02. und 04.09.2026, eine bereits am 18.06.2026 unter Methode v2.0." ), "hinweis_verdicts": ( "Die Urteilsverteilung folgt dem Scandatum, nicht dem Markt. Das dreiwertige " "Urteil CERTIFIED / NOT_CONFORMANT / INCOMPLETE ging am 03.09.2026 live. Alle " "vorher gescannten Domains tragen den alten Wert NOT_READY, der im aktuellen " "Vokabular nicht mehr vorkommt. Wie diese Domains unter der neuen Logik " "eingestuft würden, ist nicht gemessen." ), } bewertungsgruppen = {} for name, feld, gewicht in BEWERTUNGSGRUPPEN: eintrag = gruppe_mittel(snap, feld) eintrag["gewicht_prozent"] = gewicht eintrag["im_score"] = True bewertungsgruppen[name] = eintrag authority = gruppe_mittel(snap, "authority_score") authority["gewicht_prozent"] = 0.0 authority["im_score"] = False authority["hinweis"] = ( "Authority ist seit dem 01.08.2026 keine Gewichtungsgruppe mehr. Der Wert wird " "weiter erhoben, geht aber nicht in den SRS ein." ) bewertungsgruppen["authority"] = authority # Neutralwerte im Tranco-Lauf neutral = {} for name, feld, wert in NEUTRALWERTE: neutral[name] = { "neutralwert": wert, "zeilen_mit_neutralwert": sum(1 for z in tranco_lauf if z[feld] == wert), } neutralwerte = { "lauf": "TRANCO1000, gescannt am 27.08.2026", "zeilen_im_lauf": len(tranco_lauf), "je_gruppe": neutral, "hinweis": ( "Wo eine Bewertungsgruppe gar nicht erhoben werden konnte, setzte der Scanner " "einen Neutralwert ein (50, bei KI-Readiness 25). Diese Platzhalter sind in den " "Daten von echten Messwerten nicht unterscheidbar. Seit dem 03.09.2026 führen " "solche Fälle zum Urteil INCOMPLETE; der Tranco-Lauf vom 27.08.2026 liegt davor. " "Die Mittelwerte für Content-Qualität und Authority sind dadurch nach oben " "verzerrt." ), } # ── Listen September, ohne die Musterliste ─────────────────────────────── listen_sep = je_liste(snap, auslassen=LISTEN_NICHT_VEROEFFENTLICHT) domains_in_listen = sum(e["domains"] for e in listen_sep.values()) ohne_liste = len(snap) - domains_in_listen def tausender(n): """Deutsche Schreibweise im Fließtext: 1.522 statt 1522.""" return f"{n:,}".replace(",", ".") listen_september = { "hinweis_ohne_liste": ( f"{ohne_liste} Domains zählen in allen Gesamtwerten mit, erscheinen aber in " f"keiner Liste: Ihr neuester Scan vor dem Stichtag lief über die Musterliste " f"aus sechs Finanzinstituten, die als eigener Sektorwert nicht veröffentlicht " f"wird (weniger als 10 Domains, keine Markterhebung). Die Listen ergeben " f"deshalb zusammen {tausender(domains_in_listen)} statt " f"{tausender(len(snap))} Domains." ), "domains_in_listen": domains_in_listen, "domains_ohne_liste": ohne_liste, "listen": listen_sep, } # ── Listenwechsel Juni → September ─────────────────────────────────────── snap_liste = {z["domain"]: z["list"] for z in snap} juni_listen = defaultdict(set) for z in juni: juni_listen[z["list"]].add(z["domain"]) wechsel = {} for name in sorted(juni_listen): ziel = Counter() for domain in juni_listen[name]: ziel[snap_liste.get(domain, "nicht im Snapshot")] += 1 umgehaengt = {k: v for k, v in sorted(ziel.items()) if k != name} wechsel[name] = { "domains_juni": len(juni_listen[name]), "davon_september_gleiche_liste": ziel.get(name, 0), "umgehaengt_nach": umgehaengt, } listenwechsel = { "je_liste": wechsel, "hinweis": ( "Keine Domain ist zwischen den beiden Messreihen ausgefallen. Der Snapshot ordnet " "jede Domain der Liste zu, unter der ihr neuester Scan lief; viele Domains stehen " "in mehreren Listen. Wo eine Sektorliste im September kleiner wirkt, wurden ihre " "Domains unter einer anderen Liste gezählt. Sektormittelwerte aus Juni und " "September beschreiben deshalb nicht dieselbe Domainmenge." ), } # ── Juni ───────────────────────────────────────────────────────────────── juni_scores = [z["sovereign_score"] for z in juni if z["sovereign_score"] is not None] juni_stufen = Counter(reifegrad(s) for s in juni_scores) juni_block = { "methode": "v2.0", "messzeitraum": "2026-06-17 bis 2026-06-18", "messungen": len(juni), "domains": len(set(z["domain"] for z in juni)), "hinweis_messungen": ( "559 Messungen auf 540 Domains: 19 Domains stehen in zwei Sektorlisten und " "wurden zweimal gemessen. Beide Messungen gehen in die Mittelwerte ein. Die " "ursprüngliche Veröffentlichung sprach von 559 Domains." ), "durchschnitt_srs": r2(mittel(juni_scores)), "durchschnitt_srs_urspruenglich_veroeffentlicht": 45.1, "median_srs": r2(perzentil(juni_scores, 50)), "spanne_srs": [r2(min(juni_scores)), r2(max(juni_scores))], "standardabweichung_srs": r2(stdabw(juni_scores)), "durchschnitt_ki_readiness": gruppe_mittel(juni, "ai_readiness")["durchschnitt"], "durchschnitt_infrastruktur": gruppe_mittel(juni, "infrastructure_score")["durchschnitt"], "durchschnitt_content_qualitaet": gruppe_mittel(juni, "content_quality")["durchschnitt"], "n_teilscores": len(juni), "verdicts_gesamt": dict(sorted(Counter(z["verdict"] for z in juni).items())), "reifegrade": {name: juni_stufen.get(name, 0) for name, _, _ in STUFEN}, "signalgruppen": signalgruppen(juni), "listen": je_liste(juni, je_messung=True), } # ── Zusammenbau ────────────────────────────────────────────────────────── datensatz = { "meta": { "titel": "SOVP-Benchmark 2026 – Maschinenlesbarkeit von 1.525 Domains für KI-Agenten", "version": "1.1", "erzeugt_am": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "autor": "Thorsten Litzki", "herausgeber": "Litzki Systems LLC", "lizenz": "CC BY 4.0", "lizenz_url": "https://creativecommons.org/licenses/by/4.0/", "zitiervorschlag": ( "Litzki, T. (2026): SOVP-Benchmark 2026 – Maschinenlesbarkeit von 1.525 " "Domains für KI-Agenten. Fassung 1.1, Litzki Systems LLC, 29. Oktober 2026. " "https://litzki-systems.com/de/sovp-benchmark/ – Veröffentlicht unter " "CC BY 4.0." ), "hinweis": ( "Archivdokument, wird nicht fortgeschrieben. Weitere Messreihen sind nicht " "geplant; der SOVP-Benchmark ist seit Oktober 2026 kein Angebot mehr." ), "offenlegung": ( "Das gemessene Protokoll (SOVP) stammt vom Autor. Bis Oktober 2026 wurden auf " "Basis dieser Daten kostenpflichtige Sektorberichte angeboten; das Angebot ist " "eingestellt." ), "inhalt": ( "Ausschließlich Aggregate. Der Datensatz enthält keine Domainnamen, keine " "Einzelwerte und keinen Höchstwert je Liste." ), "vorversion": ( "benchmark-snapshot-2026-09.json (intern, 29.10.2026). Gegenüber jener Fassung " "entfallen die namentlich genannten Domains über der Linie 75 und die Angabe " "schwelle_sovp_ready; hinzugekommen sind Streuung, Messdaten, Verdicts nach Tag, " "Bewertungsgruppen mit Gewicht, Neutralwerte, Listenwechsel und der Juni-Teil." ), }, "regeln": { "stichtag": "2026-09-05T23:59:59", "stichtag_zeitzone": "UTC (scanned_at wird als datetime('now') in UTC geschrieben)", "auswahl_je_domain": "neuester FullScan bis zum Stichtag (scanned_at DESC, id DESC)", "scan_typ": "fullscan", "ausgeschlossene_listen": list(SONDERLISTEN), "ausschluss_regel": ( "Eine Domain fällt nur heraus, wenn sie ausschließlich über PILOT-0902 oder " "FI_MUSTER_2026_09_03 im Bestand ist. Steht sie zusätzlich in einer Marktliste, " "zählt sie mit. PILOT-0902 ist ein interner Testeintrag auf der Domain des " "Autors, FI_MUSTER_2026_09_03 eine Musterliste aus sechs Finanzinstituten." ), "null_scores": ( "Verdict INCOMPLETE hat sovereign_score NULL. Diese Domains zählen in 'domains', " "aber in keinem SRS-Mittelwert, keinem Perzentil und keiner Reifegradstufe. In den " "Teilscores (KI-Readiness, Content-Qualität, Infrastruktur, Anchor, Authority) " "zählen sie mit, weil diese Werte auch dort vorliegen." ), "perzentil_verfahren": "nearest-rank, inklusiv, 1-basiert (Index = ceil(p/100 * n))", "median": "p50 nach demselben Rangverfahren, kein Mittelwert zweier Werte", "standardabweichung": ( "Grundgesamtheit, ohne Stichprobenkorrektur: die Liste ist die Grundgesamtheit " "dieses Laufs, nicht eine Ziehung daraus." ), "reifegrade": {"L0": "< 30", "L1": "30-44", "L2": "45-59", "L3": "60-74", "L4": ">= 75"}, "hinweis_srs_75": ( "SRS 75 ist eine Linie zur Einordnung der Punktzahl, keine Schwelle des Protokolls. " "Ob eine Domain CERTIFIED ist, entscheidet allein die Conformance: veröffentlichte " "SOVP-Artefakte plus ein Betriebsmindestmaß. Die früheren Absolutschwellen sind am " "01.08.2026 ersatzlos entfallen. Im September-Snapshot trägt keine Domain das Urteil " "CERTIFIED; die beiden Domains mit SRS 78 tragen das alte Urteil NOT_READY." ), "rundung": "alle Mittelwerte auf zwei Nachkommastellen", }, "september_2026": september, "bewertungsgruppen_september": { "basis": "alle 1.525 Domains des Snapshots", "hinweis": ( "Vier Gruppen gehen gewichtet in den SRS ein; die nominalen Gewichte 25/35/22/8 " "summieren auf 90 % und werden auf 100 % normiert. Authority steht getrennt und " "ist nicht Teil des Scores." ), "gruppen": bewertungsgruppen, }, "signalgruppen_september": { "basis": ( "Snapshot, ohne internen Testeintrag und ohne Musterliste. Die inhaltlichen Gruppen " "decken nur rund 60 % der Domains ab, die Infrastrukturgruppen rund 100 % — deshalb " "trägt jeder Wert sein n." ), "gruppen": signalgruppen(snap), }, "listen_september": listen_september, "neutralwerte_tranco_lauf": neutralwerte, "listenwechsel_juni_september": listenwechsel, "juni_2026": juni_block, "herkunft": { "datenbank": DB, "datenbank_mtime_utc": db_mtime, "zugriff": "nur lesend (sqlite3 file:...?mode=ro)", "skript": skript.name, "skript_sha256": skript_sha, "sql_snapshot": " ".join(SQL_SNAPSHOT.split()), "sql_juni": " ".join(SQL_JUNI.split()), "sql_tranco_lauf": " ".join(SQL_TRANCO_LAUF.split()), }, } # ── Selbsttest: kein Hostname im Ergebnis ──────────────────────────────── text = json.dumps(datensatz, ensure_ascii=False) erlaubt = { "litzki-systems.com", "creativecommons.org", "sovp-benchmark.db", "sovp-benchmark-2026.py", "benchmark-snapshot-2026-09.json", } # Hostname = mindestens ein Label, dann ein Punkt, dann eine rein alphabetische # Endung. Reine Zahlenfolgen mit Punkt (Messwerte, Datumsangaben, "v2.0") fallen # dadurch heraus, Hostnamen jeder Länge nicht. muster = re.compile( r"\b(?:xn--)?[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?" r"(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*" r"\.[a-z]{2,24}\b", re.IGNORECASE, ) treffer = sorted({t.lower() for t in muster.findall(text)} - erlaubt) if treffer: print("ABBRUCH: mögliche Hostnamen im Datensatz: " + ", ".join(treffer), file=sys.stderr) return 1 json.dump(datensatz, sys.stdout, ensure_ascii=False, indent=2, sort_keys=False) sys.stdout.write("\n") print(f"OK: {len(snap)} Domains September ({domains_in_listen} in Listen, " f"{ohne_liste} ohne Liste), {len(juni)} Messungen Juni, " f"keine Hostnamen im Datensatz.", file=sys.stderr) return 0 if __name__ == "__main__": sys.exit(main())