"""Textnormalisierung fuer Phase 2. Gemeinsame Vorstufe der Module. Jede Aenderung hier aendert die coder_version - siehe version.py. Grundsatz: die Normalisierung ist rein und ohne Zustand. Gleiche Eingabe, gleiche Ausgabe, auf jeder Maschine und in jedem Prozess. """ import html import re import unicodedata # Was hier steht, geht in den Versions-Hash ein. REGELWERK_VERSION = "norm-3" _TAGS = re.compile(r"<[^>]{1,200}>") _MEHRFACH_LEER = re.compile(r"\s+") # Anfuehrungszeichen und Striche vereinheitlichen. Verschiedene Haeuser setzen # fuer dieselbe Agenturmeldung verschiedene Zeichen; ohne das scheitert schon # die Trigrammbildung. _ZEICHEN = str.maketrans({ "„": '"', "“": '"', "”": '"', "»": '"', "«": '"', "‟": '"', "‚": "'", "‘": "'", "’": "'", "‹": "'", "›": "'", "´": "'", "`": "'", "–": "-", "—": "-", "‑": "-", "‒": "-", "−": "-", "…": "...", " ": " ", " ": " ", " ": " ", "": " ", }) # "+++ Newsblog +++" und Verwandtes: redaktioneller Aufmacher, der nichts # ueber den Inhalt sagt. Dieselbe Meldung traegt ihn bei einem Haus und beim # naechsten nicht. _PLUSKASTEN = re.compile(r"\+{2,}[^+]{0,120}?\+{2,}") # Satzzeichen fallen weg: sie sind der haeufigste Unterschied zwischen zwei # Fassungen derselben Meldung und tragen nichts zur Unterscheidung bei. _KEIN_WORT = re.compile(r"[^\w\s]", re.UNICODE) # Ressortkuerzel, das Haeuser ihren Titeln voranstellen: "Raumfahrt: Deutsche # Rakete ...", "Notfaelle: Fuenf Tote ...", "Fussball: FIFA-Praesident ...". # Dieselbe Agenturzeile bekommt bei jedem Haus ein anderes - oder gar keins. # # Bewusst eng gefasst: hoechstens vier Woerter vor dem Doppelpunkt, und was # danach kommt, muss fuer sich stehen koennen. Sonst frisst das Muster # Titel, bei denen der Teil vor dem Doppelpunkt die eigentliche Aussage # traegt ("Habeck: Wir haben uns geirrt"). _RESSORT = re.compile(r"^\s*(?P[^:]{2,34}):\s+(?P.+)$", re.DOTALL) _RESSORT_MAX_WOERTER = 4 _REST_MIN_WOERTER = 5 def _ressort_abtrennen(text): treffer = _RESSORT.match(text) if not treffer: return text kopf, rest = treffer.group("kopf"), treffer.group("rest") if len(kopf.split()) > _RESSORT_MAX_WOERTER: return text if len(rest.split()) < _REST_MIN_WOERTER: return text return rest def normalisiere(*teile, ressort_abtrennen=False): """Vergleichstext aus einem oder mehreren Feldern. Reihenfolge ist bedeutsam: Tags vor Entities, NFKC vor der Zeichentabelle, Kleinschreibung vor den Mustern, und das Ressortkuerzel vor dem Entfernen der Satzzeichen - danach gibt es keinen Doppelpunkt mehr, an dem es zu erkennen waere. ressort_abtrennen gilt nur fuer Titel. Im Teaser steht der Doppelpunkt fuer gewoehnlich mitten im Satz. """ text = " ".join(t for t in teile if t) if not text: return "" text = _TAGS.sub(" ", text) text = html.unescape(text) text = unicodedata.normalize("NFKC", text) text = text.translate(_ZEICHEN) text = text.casefold() text = _PLUSKASTEN.sub(" ", text) if ressort_abtrennen: text = _ressort_abtrennen(text) text = _KEIN_WORT.sub(" ", text) return _MEHRFACH_LEER.sub(" ", text).strip() def woerter(text): return text.split() def trigramme(text): """Wort-Trigramme als Menge. Menge, nicht Liste: der Jaccard-Wert braucht sie so, und Wiederholungen innerhalb einer kurzen Meldung sind kein Signal. """ w = woerter(text) if len(w) < 3: # Zu kurz fuer Trigramme. Das Wort selbst ist besser als nichts, # aber der Aufrufer prueft ohnehin auf Mindestlaenge. return {(x,) for x in w} return {tuple(w[i:i + 3]) for i in range(len(w) - 2)}