Files
swp-02-aufbereitung/normalisierung.py
T
2026-09-07 14:30:02 +02:00

106 lines
3.7 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)}