"""Kanonische Medien-Identitaet je Domain. Hintergrund: Quellennamen kommen aus drei Richtungen in den Bestand, aus dem Feed-Namen (RSS), aus dem Hostnamen (staan-Treffer im EU-Weg) und aus der freien Benennung durch das Modell in der Abschlussauswahl. Dadurch fuehrt derselbe Verlag mehrere Identitaeten, etwa "Guardian UK", "Guardian World" und "The Guardian" oder "tagesschau" und "tagesschau.de". Jede Aussage ueber Quellenvielfalt oder Belegtiefe wird damit falsch, weil dieselbe Redaktion mehrfach gezaehlt wird. Dieses Modul bestimmt die Identitaet ueber die Domain, nicht ueber den Namen. Der Anzeigename wird datengetrieben gewaehlt: der haeufigste Name, der fuer diese Domain vorkommt. Die Tabelle unten deckt nur die Faelle ab, in denen gar kein brauchbarer Name vorliegt oder der Hostname als Name durchschlaegt. """ from collections import Counter from urllib.parse import urlparse # Zusammengesetzte Endungen, bei denen die registrierbare Domain drei Labels # hat (bbc.co.uk statt co.uk). _MEHRTEILIGE_ENDUNGEN = frozenset({ "co.uk", "org.uk", "ac.uk", "gov.uk", "me.uk", "net.uk", "com.au", "net.au", "org.au", "gov.au", "co.jp", "or.jp", "ne.jp", "go.jp", "com.br", "com.mx", "com.ar", "com.tr", "com.cn", "com.hk", "co.za", "co.nz", "co.kr", "co.in", "co.il", "or.at", "co.at", "gv.at", }) # Praefixe, die dieselbe Redaktion nur anders ausliefern. _HOST_PRAEFIXE = ("www.", "m.", "amp.", "rss.", "feeds.", "news.", "english.", "de.", "en.") # Anzeigename, wenn kein brauchbarer Name mitgeliefert wurde. Bewusst kurz # gehalten, die Liste ist keine Pflegepflicht: unbekannte Domains bekommen # ihren ersten Domain-Bestandteil in Grossschreibung. _NAMEN_JE_DOMAIN = { "tagesschau.de": "tagesschau", "zdfheute.de": "ZDF heute", "zdf.de": "ZDF heute", "faz.net": "FAZ", "sueddeutsche.de": "Süddeutsche Zeitung", "spiegel.de": "Spiegel", "zeit.de": "Zeit", "welt.de": "Welt", "n-tv.de": "n-tv", "dw.com": "Deutsche Welle", "deutschlandfunk.de": "Deutschlandfunk", "bundesregierung.de": "Bundesregierung", "nzz.ch": "NZZ", "srf.ch": "SRF", "kurier.at": "Kurier", "diepresse.com": "Die Presse", "theguardian.com": "The Guardian", "nytimes.com": "New York Times", "ft.com": "Financial Times", "bbc.co.uk": "BBC", "bbc.com": "BBC", "aljazeera.com": "Al Jazeera", "france24.com": "France24", "reuters.com": "Reuters", "apnews.com": "AP News", "elpais.com": "El País", "elmundo.es": "El Mundo", "abc.es": "ABC", "infobae.com": "Infobae", "politico.eu": "Politico Europe", "euronews.com": "Euronews", } # Weiterleitungs- und Sammelportale. Sie sind kein eigenstaendiges Medium und # duerfen in keiner Vielfaltsstatistik als Redaktion durchgehen. AGGREGATOR_DOMAINS = frozenset({ "news.google.com", "google.com", "msn.com", "headtopics.com", "newsbreak.com", "yahoo.com", "flipboard.com", "smartnews.com", "reutersconnect.com", "pressreader.com", "onvista.de", "finanznachrichten.de", "boersennews.de", "aol.com", }) def registrable_domain(url: str) -> str: """Registrierbare Domain einer URL, klein geschrieben. 'https://www.theguardian.com/world/...' -> 'theguardian.com' 'https://www.bbc.co.uk/news/...' -> 'bbc.co.uk' Leere oder unparsebare Eingaben ergeben einen leeren String. """ host = "" text = (url or "").strip() if not text: return "" try: host = (urlparse(text if "//" in text else "//" + text).hostname or "").lower() except ValueError: return "" if not host: return "" for praefix in _HOST_PRAEFIXE: if host.startswith(praefix) and host.count(".") >= 2: host = host[len(praefix):] teile = host.split(".") if len(teile) <= 2: return host if ".".join(teile[-2:]) in _MEHRTEILIGE_ENDUNGEN: return ".".join(teile[-3:]) return ".".join(teile[-2:]) def ist_aggregator(url: str) -> bool: """True, wenn die URL auf ein Weiterleitungs- oder Sammelportal zeigt.""" return registrable_domain(url) in AGGREGATOR_DOMAINS def name_aus_domain(domain: str) -> str: """Anzeigename allein aus der Domain, wenn kein Name vorliegt.""" if not domain: return "Unbekannt" if domain in _NAMEN_JE_DOMAIN: return _NAMEN_JE_DOMAIN[domain] erstes = domain.split(".")[0] return erstes if any(c.isupper() for c in erstes) else erstes.capitalize() def _ist_hostname_als_name(name: str, domain: str) -> bool: """True, wenn der Name nur der Hostname ist ('www.france24.com').""" schlicht = (name or "").strip().lower() return bool(schlicht) and (schlicht == domain or schlicht.endswith(domain)) def namen_je_domain(eintraege: list[tuple[str, str]]) -> dict[str, str]: """Waehlt je Domain einen Anzeigenamen aus allen gelieferten Varianten. eintraege ist eine Liste aus (url, name). Gewinner ist der haeufigste Name, bei Gleichstand der kuerzeste. Hostnamen und leere Namen zaehlen nicht mit, fuer sie greift die Tabelle beziehungsweise die Domain selbst. """ kandidaten: dict[str, Counter] = {} for url, name in eintraege: domain = registrable_domain(url) if not domain: continue zaehler = kandidaten.setdefault(domain, Counter()) sauber = (name or "").strip() if sauber and not _ist_hostname_als_name(sauber, domain): zaehler[sauber] += 1 ergebnis: dict[str, str] = {} for domain, zaehler in kandidaten.items(): if not zaehler: ergebnis[domain] = name_aus_domain(domain) continue hoechste = max(zaehler.values()) beste = sorted((n for n, c in zaehler.items() if c == hoechste), key=lambda n: (len(n), n)) ergebnis[domain] = beste[0] return ergebnis def sprache_aus_url(url: str) -> str: """Sprachkennung, die sich eindeutig aus der Adresse ergibt, sonst ''. Deckt die Faelle ab, in denen das Modell die Sprache falsch angibt, etwa 'english.elpais.com' als DE. Nur eindeutige Muster, keine Raterei. """ text = (url or "").strip().lower() if not text: return "" try: zerlegt = urlparse(text if "//" in text else "//" + text) except ValueError: return "" host = zerlegt.hostname or "" pfad = zerlegt.path or "" if host.startswith("english.") or pfad.startswith("/en/") or "/english/" in pfad: return "EN" if host.startswith("de.") or pfad.startswith("/de/"): return "DE" if host.startswith("fr.") or pfad.startswith("/fr/"): return "FR" if host.startswith("es.") or pfad.startswith("/es/"): return "ES" return ""