Dublettenprüfung

This commit is contained in:
irrlicht
2026-09-07 14:30:02 +02:00
parent 861caae57c
commit 50d497925f
11 changed files with 1648 additions and 17 deletions
+105
View File
@@ -0,0 +1,105 @@
"""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<kopf>[^:]{2,34}):\s+(?P<rest>.+)$", 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)}