106 lines
3.7 KiB
Python
106 lines
3.7 KiB
Python
"""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)}
|