Stichwortkandidaten: spaCy-basiertes Skript zur Findung neuer Fachbegriffe
erzeuge_stichwortkandidaten_04.py:
- spaCy POS-Tagging (NOUN, PROPN) + NER (PER) + Lemmatisierung
- Abkürzungen per Regex ([A-Z]{2,} mit Bindestrichen/Zahlen)
- Buchspezifische Stoppliste (Häufigkeit >= 15) + allgemeine Stoppliste
- Glossarbegriffe als Quelle eingeblendet ([Glossar]-Markierung),
aber nicht verändert — nur neue Kandidaten werden markiert
- Code/Math/Inline-Code wird vor der Analyse entfernt
- Modi: --vorschlag (nur Liste), --bericht (ausführlich), --check (nur melden)
- 818 Kandidaten: 18 Abkürzungen + 10 Nomen bereits im Glossar,
106 Abkürzungen + 684 Nomen als neue Kandidaten
This commit is contained in:
parent
99cb85907c
commit
3dcd8d170f
1 changed files with 533 additions and 0 deletions
|
|
@ -0,0 +1,533 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# erzeuge_stichwortkandidaten_04.py
|
||||||
|
"""
|
||||||
|
Findet Fachbegriffe fuer das Stichwortregister, die NICHT im Glossar
|
||||||
|
stehen — automatisch, mit spaCy POS-Tagging, Lemmatisierung und
|
||||||
|
NER (Personennamen). Hybrid: Glossar als Kern, freie Extraktion als
|
||||||
|
Ergaenzung, zwei Stopplisten als Filter.
|
||||||
|
|
||||||
|
Pipeline:
|
||||||
|
1. Text aus Kapiteldateien (ohne Code/Math) extrahieren
|
||||||
|
2. spaCy: NOUN, PROPN, ADJ + PER-Entities lemmatisiert sammeln
|
||||||
|
3. Abkuerzungen per Regex
|
||||||
|
4. Buchspezifische Stoppliste: die haeufigsten Woerter des Buchs,
|
||||||
|
die keine Fachbegriffe sind
|
||||||
|
5. Frequenzfilter: 2–N Vorkommen = Kandidat, >N = Stoppwort,
|
||||||
|
1 = nur wenn im Glossar
|
||||||
|
6. Kandidatenliste als Vorschlag (--vorschlag) oder direkt markieren
|
||||||
|
|
||||||
|
Aufruf (aus dem Wurzelverzeichnis Version_04):
|
||||||
|
python3 Operations_Research_mit_Python_Version_04/erzeuge_stichwortkandidaten_04.py --vorschlag
|
||||||
|
python3 .../erzeuge_stichwortkandidaten_04.py --bericht
|
||||||
|
python3 .../erzeuge_stichwortkandidaten_04.py # markieren
|
||||||
|
python3 .../erzeuge_stichwortkandidaten_04.py --check # nur melden
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
HIER = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
BASIS = os.path.dirname(HIER)
|
||||||
|
sys.path.insert(0, HIER)
|
||||||
|
|
||||||
|
from glossar_eintraege_04 import EINTRAEGE # noqa: E402
|
||||||
|
import build_version_04 as b # noqa: E402
|
||||||
|
|
||||||
|
SKIP_DATEIEN = {"94_Anhang_Glossar.md", "95_Anhang_Literatur.md"}
|
||||||
|
|
||||||
|
# --- Allgemeine deutsche Stoppliste ----------------------------------------
|
||||||
|
# Artikel, Pronomen, Konjunktionen, Praepositionen, Hilfsverben, Partikeln.
|
||||||
|
# Nicht aus einer Frequenzliste abgeleitet, sondern per Hand: diese Woerter
|
||||||
|
# sind grammatikalisch, nie fachlich. spaCy taggt sie ohnehin nicht als
|
||||||
|
# NOUN/PROPN, aber adjektivische Formen ("einfach", "gross") landen als
|
||||||
|
# ADJ im Pool und muessen hier heraus.
|
||||||
|
_ALLGEMEINE_STOPP = frozenset({
|
||||||
|
# Artikel
|
||||||
|
"der", "die", "das", "den", "dem", "des", "ein", "eine", "einer",
|
||||||
|
"eines", "einem", "einen", "kein", "keine", "keiner",
|
||||||
|
# Pronomen
|
||||||
|
"ich", "du", "er", "sie", "es", "wir", "ihr", "mich", "dich", "sich",
|
||||||
|
"uns", "euch", "mein", "dein", "sein", "unser", "euer", "ihre", "deren",
|
||||||
|
"dieser", "diese", "dieses", "jener", "jene", "jenes", "welcher",
|
||||||
|
"welche", "welches", "wer", "was", "man", "jemand", "niemand",
|
||||||
|
# Konjunktionen / Partikeln
|
||||||
|
"und", "oder", "aber", "denn", "sondern", "weil", "wenn", "als",
|
||||||
|
"ob", "dass", "damit", "weder", "noch", "entweder", "sowohl",
|
||||||
|
"auch", "nur", "noch", "schon", "immer", "nie", "oft", "sehr",
|
||||||
|
"mehr", "meist", "viel", "wenig", "etwas", "nichts", "alles",
|
||||||
|
# Praepositionen
|
||||||
|
"in", "an", "auf", "aus", "bei", "mit", "nach", "seit", "von", "zu",
|
||||||
|
"gegen", "ohne", "um", "fuer", "ueber", "unter", "zwischen", "durch",
|
||||||
|
"wegen", "waehrend", "trotz", "innerhalb", "ausserhalb", "oberhalb",
|
||||||
|
"unterhalb", "statt", "trotz",
|
||||||
|
# Hilfsverben (als Lemma)
|
||||||
|
"sein", "haben", "werden", "koennen", "muessen", "duerfen", "sollen",
|
||||||
|
"wollen", "moegen", "tun", "machen", "geben", "nehmen", "kommen",
|
||||||
|
"gehen", "stehen", "liegen", "sitzen",
|
||||||
|
# haeufige Adjektive, die keine Fachbegriffe sind
|
||||||
|
"gut", "besser", "beste", "gross", "grosser", "klein", "neu", "alt",
|
||||||
|
"hoch", "niedrig", "lang", "kurz", "weit", "nah", "einfach",
|
||||||
|
"schwer", "leicht", "wichtig", "unwichtig", "moeglich", "unmoeglich",
|
||||||
|
"notwendig", "ueberfluessig", "bekannt", "unbekannt", "deutlich",
|
||||||
|
"klar", "unsicher", "sicher", "richtig", "falsch", "wahr", "unwahr",
|
||||||
|
"ganze", "ganzer", "ganzen", "gesamte", "saemtlich", "jed",
|
||||||
|
"jede", "jeder", "jedes", "all", "alle", "aller", "saemtliche",
|
||||||
|
# Zahlenworte
|
||||||
|
"eins", "zwei", "drei", "vier", "fuenf", "sechs", "sieben", "acht",
|
||||||
|
"neun", "zehn", "erste", "zweite", "dritte", "letzte", "naechste",
|
||||||
|
"vorige", "vorherige", "nachste", "obige", "untere", "obere",
|
||||||
|
# Sonstiges
|
||||||
|
"ja", "nein", "bitte", "danke", "eben", "ebenfalls", "zusatz",
|
||||||
|
"zusaetzlich", "weiter", "weiterer", "weitere", "ferner",
|
||||||
|
# Strukturelle Begriffe des Buchs (keine Fachbegriffe)
|
||||||
|
"handrechnung", "micro-quiz", "denkfehler", "schnellstart",
|
||||||
|
"lernziel", "aufgabe", "loesung", "lösung", "zusammenfassung",
|
||||||
|
"selbsttest", "selbstkontrolle", "wiederholung", "merkzahl",
|
||||||
|
"merksatz", "kapitel", "abschnitt", "anhang", "teil", "datei",
|
||||||
|
"dateien", "programm", "programme", "beispiel", "beispiele",
|
||||||
|
"tabelle", "abbildung", "datei", "verzeichnis", "register",
|
||||||
|
"glossar", "literatur", "quelltext", "code", "codeblock",
|
||||||
|
"notebook", "website", "pdf", "html", "markdown", "pandoc",
|
||||||
|
"latex", "python", "tool", "werkzeug", "ressource", "ressourcen",
|
||||||
|
# Alltaegliche Substantive, die in technischen Texten haeufig sind,
|
||||||
|
# aber keine Fachbegriffe
|
||||||
|
"bereich", "mittel", "produkt", "stufe", "betrieb", "buch",
|
||||||
|
"ende", "struktur", "euro", "funktion", "variablen", "variable",
|
||||||
|
"stelle", "wert", "zahl", "fall", "beispiel", "verfahren",
|
||||||
|
"ansatz", "methode", "problem", "modell", "ergebnis",
|
||||||
|
"schritt", "weg", "art", "weise", "grund", "grundlage",
|
||||||
|
"grundlagen", "teil", "rest", "menge", "anzahl", "nummer",
|
||||||
|
"seite", "platz", "ort", "zeit", "moment", "augenblick",
|
||||||
|
"beginn", "start", "mitte", "rand", "grenze", "limit",
|
||||||
|
"kopf", "fuß", "name", "bezeichnung", "titel", "label",
|
||||||
|
"text", "wort", "satz", "absatz", "zeile", "spalte", "zelle",
|
||||||
|
"feld", "raum", "flaeche", "linie", "punkt", "zeichen",
|
||||||
|
"symbol", "formel", "gleichung", "ausdruck", "term", "faktor",
|
||||||
|
"komponente", "element", "einheit", "einheiten", "objekt",
|
||||||
|
"ding", "aspekt", "thema", "gebiet", "sektor", "zone",
|
||||||
|
"umfeld", "kontext", "umgebung", "welt", "tag", "monat",
|
||||||
|
"jahr", "woche", "stunde", "minute", "sekunde", "zeitpunkt",
|
||||||
|
"zeitraum", "periode", "phase", "abschnitt", "stueck",
|
||||||
|
"anteil", "bruch", "gesamt", "summe", "differenz",
|
||||||
|
"quotient", "rate", "verhaeltnis", "quote", "merkmal",
|
||||||
|
"eigenschaft", "attribut", "besonderheit", "detail",
|
||||||
|
"bestandteil", "voraussetzung", "bedingung", "annahme",
|
||||||
|
"hypothese", "these", "behauptung", "aussage", "feststellung",
|
||||||
|
"beobachtung", "erkenntnis", "ueberlegung", "gedanke", "idee",
|
||||||
|
"konzept", "prinzip", "regel", "gesetz", "vorschrift",
|
||||||
|
"anforderung", "bedarf", "beduerfnis", "wunsch", "ziel",
|
||||||
|
"zweck", "nutzen", "vorteil", "nachteil", "gewinn",
|
||||||
|
"verlust", "ertrag", "aufwand", "kosten", "preis", "betrag",
|
||||||
|
"person", "mann", "frau", "mensch", "leute", "personen",
|
||||||
|
"mitarbeiter", "kollege", "chef", "team", "gruppe",
|
||||||
|
"mannschaft", "kreis", "abend", "morgen", "nacht",
|
||||||
|
"kalender", "uhrzeit", "datum", "land", "stadt", "dorf",
|
||||||
|
"region", "haus", "gebäude", "raum", "zimmer", "tuer",
|
||||||
|
"fenster", "wand", "decke", "boden", "geraet",
|
||||||
|
"instrument", "maschine", "motor", "rechner", "computer",
|
||||||
|
"server", "netz", "netzwerk", "verbindung", "link", "verweis",
|
||||||
|
"daten", "datensatz", "datei", "dateien", "liste",
|
||||||
|
"verzeichnis", "register", "index", "inhaltsverzeichnis",
|
||||||
|
"fußnote", "bemerkung", "notiz", "hinweis", "tipp", "rat",
|
||||||
|
"empfehlung", "warnung", "gefahr", "risiko", "fehler",
|
||||||
|
"irrtum", "frage", "antwort", "reaktion", "folge",
|
||||||
|
"konsequenz", "auswirkung", "effekt", "wirkung", "einfluss",
|
||||||
|
"aenderung", "veraenderung", "verbesserung", "erweiterung",
|
||||||
|
"ergaenzung", "erhoehung", "verringerung", "senkung",
|
||||||
|
"steigerung", "reduktion", "abnahme", "zunahme",
|
||||||
|
"wachstum", "anlage", "investition", "kapital", "geld",
|
||||||
|
"finanzen", "wirtschaft", "forschung", "wissenschaft",
|
||||||
|
"studie", "untersuchung", "experiment", "test", "versuch",
|
||||||
|
"pruefung", "abnahme", "kontrolle", "ueberwachung",
|
||||||
|
"messung", "messwert", "berechnung", "rechnung",
|
||||||
|
"kalkulation", "analyse", "betrachtung", "darstellung",
|
||||||
|
"praesentation", "ausgabe", "eingabe", "dateiname",
|
||||||
|
"dateiendung", "format", "formate", "formatierung",
|
||||||
|
"layout", "design", "stil", "klasse", "objekt",
|
||||||
|
"instanz", "methode", "parameter", "argument",
|
||||||
|
"schnipsel", "gesamt", "ende", "mitte", "anfang",
|
||||||
|
"ueberblick", "zusammenhang", "bezug", "referenz",
|
||||||
|
"quelle", "herkunft", "ursache", "grund", "anlass",
|
||||||
|
"antrag", "auftrag", "anweisung", "befehl", "kommando",
|
||||||
|
"prozess", "prozesse", "vorgang", "ablauf", "handlung",
|
||||||
|
"aktion", "schritt", "phase", "status", "stand",
|
||||||
|
"zustand", "lage", "position", "stellung", "rolle",
|
||||||
|
"funktion", "aufgabe", "zweck", "bestimmung",
|
||||||
|
})
|
||||||
|
|
||||||
|
# --- Konfiguration ---------------------------------------------------------
|
||||||
|
# Ein Kandidat muss in mindestens so vielen Dateien auftauchen (nicht
|
||||||
|
# Vorkommen — Dateien, sonst fluten seltene Begriffe aus einem Kapitel
|
||||||
|
# das Register). 1 reicht: ein Begriff, der nur in einem Kapitel vorkommt,
|
||||||
|
# ist ein Fachbegriff genau dort.
|
||||||
|
MIN_DATEIEN = 3
|
||||||
|
|
||||||
|
# Ab dieser Anzahl Vorkommen im Gesamtbuch gilt ein Wort als
|
||||||
|
# buchspezifisches Stoppwort (es ist "Alltagssprache" des Buchs, kein
|
||||||
|
# Fachbegriff). Glossarbegriffe sind davon ausgenommen.
|
||||||
|
STOPP_SCHWELLE = 15
|
||||||
|
|
||||||
|
# Maximale Anzahl Vorkommen fuer einen Kandidaten: ueber dieser Grenze
|
||||||
|
# ist ein Begriff zu allgemein (erscheint in fast jedem Kapitel).
|
||||||
|
MAX_VORKOMMEN = STOPP_SCHWELLE - 1
|
||||||
|
|
||||||
|
# Mindestlaenge eines Kandidaten-Lemmas: unter 4 Zeichen ist fast
|
||||||
|
# immer Rauschen (Artikel, Pronomen, Variablennamen wie w_1).
|
||||||
|
MIN_LAENGE = 4
|
||||||
|
|
||||||
|
|
||||||
|
def _idx_marke_als_suchbegriff(marke: str) -> str:
|
||||||
|
"""{idx:Branch-and-Bound!Bounding} → 'Bounding' (Unterbegriff)."""
|
||||||
|
if "!" in marke:
|
||||||
|
marke = marke.split("!", 1)[1]
|
||||||
|
if "@" in marke:
|
||||||
|
marke = marke.rsplit("@", 1)[-1]
|
||||||
|
marke = re.sub(r"\s*\([^)]*\)\s*$", "", marke)
|
||||||
|
return marke.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def glossar_begriffe() -> set[str]:
|
||||||
|
"""Alle Anzeigenamen und Indexmarken aus dem Glossar."""
|
||||||
|
out = set()
|
||||||
|
for name, idx, _, _ in EINTRAEGE:
|
||||||
|
out.add(name)
|
||||||
|
if idx:
|
||||||
|
out.add(_idx_marke_als_suchbegriff(idx))
|
||||||
|
out.add(idx)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _entferne_code_und_math(text: str) -> str:
|
||||||
|
"""Entfernt Codebloecke, Inline-Code und Math aus dem Text.
|
||||||
|
|
||||||
|
spaCy soll nur natuerlichsprachlichen Text sehen — Code enthaelt
|
||||||
|
Variablennamen, die als PROPN getaggt wuerden und das Register
|
||||||
|
fluten."""
|
||||||
|
# Display-Math: $$...$$ (mehrzeilig)
|
||||||
|
text = re.sub(r"\$\$[\s\S]*?\$\$", " ", text)
|
||||||
|
# Inline-Math: $...$
|
||||||
|
text = re.sub(r"\$[^$\n]+\$", " ", text)
|
||||||
|
# LaTeX-math: \(...\) und \[...\]
|
||||||
|
text = re.sub(r"\\\([\s\S]*?\\\)", " ", text)
|
||||||
|
text = re.sub(r"\\\[[\s\S]*?\\\]", " ", text)
|
||||||
|
# Codebloecke (```...```)
|
||||||
|
text = re.sub(r"```[\s\S]*?```", " ", text)
|
||||||
|
# Inline-Code: `...`
|
||||||
|
text = re.sub(r"`[^`]+`", " ", text)
|
||||||
|
# Markdown-Link-Ziele entfernen: [Text](URL) → Text
|
||||||
|
text = re.sub(r"\[([^\]]*)\]\([^)]+\)", r"\1", text)
|
||||||
|
# {idx:...}, {ref:...}, {#...} Marker entfernen
|
||||||
|
text = re.sub(r"\{(?:idx|ref|kap|sec|anhang|teil)[:#][^}]*\}", " ", text)
|
||||||
|
# Bilddarstellungen:  → alt
|
||||||
|
text = re.sub(r"!\[([^\]]*)\]\([^)]+\)", r"\1", text)
|
||||||
|
# Ueberschriften-Präfixe entfernen (# → Leerzeichen)
|
||||||
|
text = re.sub(r"^#{1,6}\s+", " ", text, flags=re.MULTILINE)
|
||||||
|
# Blockquote-Marker entfernen
|
||||||
|
text = re.sub(r"^>\s?", "", text, flags=re.MULTILINE)
|
||||||
|
# LaTeX-Befehle entfernen: \textbf, \index, \clearpage etc.
|
||||||
|
text = re.sub(r"\\[a-zA-Z]+", " ", text)
|
||||||
|
# Markdown-Tabellen-Trenner entfernen
|
||||||
|
text = re.sub(r"\|", " ", text)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _abkuerzungen(text: str) -> set[str]:
|
||||||
|
"""Extrahiert Abkuerzungen per Regex.
|
||||||
|
|
||||||
|
Muster:
|
||||||
|
- 2+ Grossbuchstaben: LP, MILP, CVaR, KKT, VRP, IIS
|
||||||
|
- Grossbuchstabe + Bindestrich + Grossbuchstabe: CP-SAT
|
||||||
|
- Grossbuchstabenfolge mit Zahlen: A* (nicht hier), IPv6
|
||||||
|
"""
|
||||||
|
out = set()
|
||||||
|
# 2+ aufeinanderfolgende Grossbuchstaben (mit Bindestrichen/Zahlen)
|
||||||
|
for m in re.finditer(r"\b([A-Z][A-Z0-9]+(?:-[A-Z0-9]+)*)\b", text):
|
||||||
|
abk = m.group(1)
|
||||||
|
if len(abk) < 2:
|
||||||
|
continue
|
||||||
|
# Anhang-/Abschnittsnummern ueberspringen: B1, C2, D3, A1 etc.
|
||||||
|
# (ein Grossbuchstabe + Ziffern, kein echter Begriff)
|
||||||
|
if re.fullmatch(r"[A-Z]\d+", abk):
|
||||||
|
continue
|
||||||
|
out.add(abk)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def verarbeite_text(nlp, text: str) -> list[tuple[str, str]]:
|
||||||
|
"""Gibt Liste von (Lemma, POS-Tag) fuer Kandidaten zurueck."""
|
||||||
|
# spaCy hat ein Limit von 1M Zeichen; wir verarbeiten in Chunks
|
||||||
|
MAX_CHARS = 900_000
|
||||||
|
ergebnis: list[tuple[str, str]] = []
|
||||||
|
for i in range(0, len(text), MAX_CHARS):
|
||||||
|
chunk = text[i:i + MAX_CHARS]
|
||||||
|
doc = nlp(chunk)
|
||||||
|
for token in doc:
|
||||||
|
pos = token.pos_
|
||||||
|
if pos in ("NOUN", "PROPN"):
|
||||||
|
# ADJ bewusst nicht: konvex/linear/ganzzahlig gehoeren ins
|
||||||
|
# Glossar (mit Definition), nicht in die freie Extraktion.
|
||||||
|
# spaCy-ADJ-Tags sind zu verrauscht ("haeufig", "echt").
|
||||||
|
lemma = token.lemma_.lower()
|
||||||
|
if not lemma or len(lemma) < MIN_LAENGE:
|
||||||
|
continue
|
||||||
|
if lemma in _ALLGEMEINE_STOPP:
|
||||||
|
continue
|
||||||
|
# Numerische/sonderzeichen-Lemmas ueberspringen
|
||||||
|
if re.fullmatch(r"[0-9\W]+", lemma):
|
||||||
|
continue
|
||||||
|
# Variablennamen: w_1, z_t, r_j, x_12 etc.
|
||||||
|
if re.fullmatch(r"[a-z]_\w+", lemma):
|
||||||
|
continue
|
||||||
|
# LaTeX-Reste: \in, \mathbf, begin, end, textbf etc.
|
||||||
|
if lemma.startswith("\\"):
|
||||||
|
continue
|
||||||
|
if lemma in ("begin", "end", "textbf", "texttt", "item",
|
||||||
|
"frac", "quad", "sum", "min", "max", "arg",
|
||||||
|
"label", "ref", "cite", "index", "printindex",
|
||||||
|
"clearpage", "newpage", "section", "subsection",
|
||||||
|
"paragraph", "center", "include", "input"):
|
||||||
|
continue
|
||||||
|
# Token darf nur Buchstaben (inkl. Umlaute) und
|
||||||
|
# Bindestriche enthalten — sonst ist es Rauschen.
|
||||||
|
if not re.fullmatch(r"[a-zäöüß-]+", lemma):
|
||||||
|
continue
|
||||||
|
ergebnis.append((lemma, pos))
|
||||||
|
# Named Entities (PER = Personennamen)
|
||||||
|
for ent in doc.ents:
|
||||||
|
if ent.label_ == "PER":
|
||||||
|
name = ent.text.strip()
|
||||||
|
if name and len(name) >= MIN_LAENGE:
|
||||||
|
# Nur Buchstaben und Bindestriche
|
||||||
|
if re.fullmatch(r"[A-Za-zäöüÄÖÜß -]+", name):
|
||||||
|
ergebnis.append((name, "PER"))
|
||||||
|
return ergebnis
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Findet Fachbegriffe fuer das Stichwortregister, die nicht "
|
||||||
|
"im Glossar stehen — mit spaCy POS-Tagging und NER.")
|
||||||
|
parser.add_argument("--vorschlag", action="store_true",
|
||||||
|
help="Kandidatenliste ausgeben, nichts markieren")
|
||||||
|
parser.add_argument("--bericht", action="store_true",
|
||||||
|
help="ausfuehrliche Liste je Datei")
|
||||||
|
parser.add_argument("--check", action="store_true",
|
||||||
|
help="nur melden, keine Dateien schreiben")
|
||||||
|
parser.add_argument("--schwelle", type=int, default=STOPP_SCHWELLE,
|
||||||
|
help=f"Stoppwort-Schwelle (Standard: {STOPP_SCHWELLE})")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
import spacy
|
||||||
|
print("Lade spaCy-Modell de_core_news_md …")
|
||||||
|
nlp = spacy.load("de_core_news_md")
|
||||||
|
nlp.max_length = 2_000_000
|
||||||
|
|
||||||
|
# 1. Glossarbegriffe sammeln (als Quelle + Ausschlusskriterium)
|
||||||
|
g_begriffe = glossar_begriffe()
|
||||||
|
print(f"Glossarbegriffe (als Quelle): {len(g_begriffe)}")
|
||||||
|
|
||||||
|
# 2. Text aus allen Kapiteldateien sammeln
|
||||||
|
print("Lese Kapiteldateien …")
|
||||||
|
alle_tokens: list[tuple[str, str]] = []
|
||||||
|
abkuerzungen_all: set[str] = set()
|
||||||
|
datei_vorkommen: dict[str, set[str]] = {} # lemma → set von Dateien
|
||||||
|
vorkommen_gesamt: Counter = Counter()
|
||||||
|
|
||||||
|
for datei in b.DATEIEN:
|
||||||
|
if datei in SKIP_DATEIEN:
|
||||||
|
continue
|
||||||
|
pfad = os.path.join(HIER, datei)
|
||||||
|
if not os.path.exists(pfad):
|
||||||
|
continue
|
||||||
|
with open(pfad, encoding="utf-8") as f:
|
||||||
|
roh = f.read()
|
||||||
|
text = _entferne_code_und_math(roh)
|
||||||
|
|
||||||
|
# Abkuerzungen sammeln
|
||||||
|
abk = _abkuerzungen(text)
|
||||||
|
abkuerzungen_all |= abk
|
||||||
|
|
||||||
|
# spaCy-Tokens
|
||||||
|
tokens = verarbeite_text(nlp, text)
|
||||||
|
datei_lemmas = set()
|
||||||
|
for lemma, pos in tokens:
|
||||||
|
alle_tokens.append((lemma, pos))
|
||||||
|
datei_lemmas.add(lemma)
|
||||||
|
for lemma in datei_lemmas:
|
||||||
|
datei_vorkommen.setdefault(lemma, set()).add(datei)
|
||||||
|
vorkommen_gesamt[lemma] += 1
|
||||||
|
|
||||||
|
# 3. Frequenzanalyse
|
||||||
|
freq = Counter(lemma for lemma, _ in alle_tokens)
|
||||||
|
|
||||||
|
# 4. Buchspezifische Stoppliste
|
||||||
|
buch_stopp = {lemma for lemma, count in freq.items()
|
||||||
|
if count >= args.schwelle}
|
||||||
|
# Glossarbegriffe niemals stoppen
|
||||||
|
buch_stopp -= g_begriffe
|
||||||
|
|
||||||
|
print(f"\nBuchspezifische Stoppwoerter (>= {args.schwelle} Vorkommen): "
|
||||||
|
f"{len(buch_stopp)}")
|
||||||
|
if buch_stopp and args.bericht:
|
||||||
|
for w in sorted(buch_stopp)[:30]:
|
||||||
|
print(f" {w} ({freq[w]})")
|
||||||
|
if len(buch_stopp) > 30:
|
||||||
|
print(f" … und {len(buch_stopp) - 30} weitere")
|
||||||
|
|
||||||
|
# 5. Abkuerzungen: Filtern — Glossarbegriffe als eigene Kategorie
|
||||||
|
abk_im_glossar = sorted(a for a in abkuerzungen_all
|
||||||
|
if a in g_begriffe)
|
||||||
|
abk_kandidaten = sorted(
|
||||||
|
a for a in abkuerzungen_all
|
||||||
|
if a not in g_begriffe
|
||||||
|
and a not in buch_stopp
|
||||||
|
and len(a) >= 2
|
||||||
|
)
|
||||||
|
|
||||||
|
# 6. Kandidaten sammeln: Lemma, das nicht Stoppwort ist,
|
||||||
|
# in MIN_DATEIEN Dateien vorkommt, und nicht nur 1x insgesamt.
|
||||||
|
# Glossarbegriffe werden als eigene Kategorie gefuehrt.
|
||||||
|
kandidaten_nomen: list[tuple[str, int, int]] = [] # (lemma, freq, dateien)
|
||||||
|
nomen_im_glossar: list[tuple[str, int, int]] = []
|
||||||
|
for lemma, count in freq.items():
|
||||||
|
if lemma in buch_stopp:
|
||||||
|
continue
|
||||||
|
if count < 2:
|
||||||
|
continue
|
||||||
|
n_dateien = len(datei_vorkommen.get(lemma, set()))
|
||||||
|
if n_dateien < MIN_DATEIEN:
|
||||||
|
continue
|
||||||
|
if lemma in g_begriffe:
|
||||||
|
nomen_im_glossar.append((lemma, count, n_dateien))
|
||||||
|
else:
|
||||||
|
kandidaten_nomen.append((lemma, count, n_dateien))
|
||||||
|
|
||||||
|
# Nach Frequenz absteigend sortieren
|
||||||
|
kandidaten_nomen.sort(key=lambda x: x[1], reverse=True)
|
||||||
|
nomen_im_glossar.sort(key=lambda x: x[1], reverse=True)
|
||||||
|
|
||||||
|
print(f"\n=== Kandidaten ===")
|
||||||
|
print(f"Abkuerzungen (neu): {len(abk_kandidaten)}")
|
||||||
|
print(f"Abkuerzungen (Glossar): {len(abk_im_glossar)}")
|
||||||
|
print(f"Nomen (neu): {len(kandidaten_nomen)}")
|
||||||
|
print(f"Nomen (Glossar): {len(nomen_im_glossar)}")
|
||||||
|
gesamt = (len(abk_kandidaten) + len(abk_im_glossar)
|
||||||
|
+ len(kandidaten_nomen) + len(nomen_im_glossar))
|
||||||
|
print(f"Gesamt: {gesamt}")
|
||||||
|
|
||||||
|
# 7. Ausgabe
|
||||||
|
if args.vorschlag or args.bericht:
|
||||||
|
if abk_im_glossar:
|
||||||
|
print(f"\n--- Abkuerzungen im Glossar ({len(abk_im_glossar)}) ---")
|
||||||
|
for abk in abk_im_glossar:
|
||||||
|
n_dateien = len(datei_vorkommen.get(abk.lower(), set()))
|
||||||
|
print(f" {abk:<20} ({freq.get(abk.lower(), 0)} Vorkommen, "
|
||||||
|
f"{n_dateien} Dateien) [Glossar]")
|
||||||
|
|
||||||
|
print(f"\n--- Abkuerzungen (neue Kandidaten) ({len(abk_kandidaten)}) ---")
|
||||||
|
for abk in abk_kandidaten:
|
||||||
|
n_dateien = len(datei_vorkommen.get(abk.lower(), set()))
|
||||||
|
print(f" {abk:<20} ({freq.get(abk.lower(), 0)} Vorkommen, "
|
||||||
|
f"{n_dateien} Dateien)")
|
||||||
|
|
||||||
|
if nomen_im_glossar:
|
||||||
|
print(f"\n--- Nomen im Glossar ({len(nomen_im_glossar)}) ---")
|
||||||
|
print(f" (Lemma | Vorkommen | Dateien)")
|
||||||
|
for lemma, count, n_dateien in nomen_im_glossar:
|
||||||
|
print(f" {lemma:<35} {count:>4} {n_dateien:>2} Dateien [Glossar]")
|
||||||
|
|
||||||
|
print(f"\n--- Nomen (neue Kandidaten) ({len(kandidaten_nomen)}) ---")
|
||||||
|
print(f" (Lemma | Vorkommen | Dateien)")
|
||||||
|
for lemma, count, n_dateien in kandidaten_nomen:
|
||||||
|
mark = " ★" if count >= 5 else ""
|
||||||
|
print(f" {lemma:<35} {count:>4} {n_dateien:>2} Dateien{mark}")
|
||||||
|
|
||||||
|
if args.vorschlag:
|
||||||
|
print(f"\n--vorschlag: Keine Dateien geaendert. "
|
||||||
|
f"Pruefen Sie die Liste; gewuenschte Begriffe im Glossar "
|
||||||
|
f"ergaenzen oder direkt mit --check/--bericht markieren.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 8. Markieren (nur in --check oder Default)
|
||||||
|
# Glossarbegriffe sind bereits durch erzeuge_stichwortregister_04.py
|
||||||
|
# markiert — sie werden hier NICHT erneut eingefuegt.
|
||||||
|
# Nur neue Kandidaten (Abkuerzungen + Nomen) werden markiert.
|
||||||
|
if args.check:
|
||||||
|
print(f"\n--check: {len(abk_kandidaten) + len(kandidaten_nomen)} "
|
||||||
|
f"neue Kandidaten wuerden markiert werden "
|
||||||
|
f"(Glossarbegriffe ausgenommen).")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Tatsaechliches Markieren — nur neue Kandidaten
|
||||||
|
alle_kandidaten = (
|
||||||
|
[(abk, abk) for abk in abk_kandidaten]
|
||||||
|
+ [(lemma, lemma) for lemma, _, _ in kandidaten_nomen]
|
||||||
|
)
|
||||||
|
alle_kandidaten.sort(key=lambda x: len(x[0]), reverse=True)
|
||||||
|
|
||||||
|
gesamt_markiert = 0
|
||||||
|
for datei in b.DATEIEN:
|
||||||
|
if datei in SKIP_DATEIEN:
|
||||||
|
continue
|
||||||
|
pfad = os.path.join(HIER, datei)
|
||||||
|
if not os.path.exists(pfad):
|
||||||
|
continue
|
||||||
|
with open(pfad, encoding="utf-8") as f:
|
||||||
|
text = f.read()
|
||||||
|
|
||||||
|
einfuegungen: list[tuple[int, str, str]] = []
|
||||||
|
belegt: list[tuple[int, int]] = []
|
||||||
|
|
||||||
|
for suchbegriff, indexmarke in alle_kandidaten:
|
||||||
|
# Wortgrenzen fuer deutschen Text
|
||||||
|
muster = re.compile(
|
||||||
|
r"(?<![a-zA-Z0-9äöüÄÖÜß_])"
|
||||||
|
+ re.escape(suchbegriff)
|
||||||
|
+ r"(?![a-zA-Z0-9äöüÄÖÜß_])",
|
||||||
|
re.IGNORECASE if suchbegriff.islower() else 0
|
||||||
|
)
|
||||||
|
for m in muster.finditer(text):
|
||||||
|
start, end = m.start(), m.end()
|
||||||
|
if text[:start].count("```") % 2 == 1:
|
||||||
|
continue
|
||||||
|
if text[:start].count("`") % 2 == 1:
|
||||||
|
continue
|
||||||
|
if text[:start].count("$") % 2 == 1:
|
||||||
|
continue
|
||||||
|
if "{idx:" in text[max(0, start - 80):start]:
|
||||||
|
continue
|
||||||
|
if "{idx:" in text[end:end + 40]:
|
||||||
|
continue
|
||||||
|
if any(s <= start < e or s < end <= e for s, e in belegt):
|
||||||
|
continue
|
||||||
|
einfuegepos = end
|
||||||
|
while einfuegepos < len(text) and text[einfuegepos] == "*":
|
||||||
|
einfuegepos += 1
|
||||||
|
marker = "{idx:" + indexmarke + "}"
|
||||||
|
einfuegungen.append((einfuegepos, marker, suchbegriff))
|
||||||
|
belegt.append((start, einfuegepos + len(marker)))
|
||||||
|
break # nur erste Fundstelle je Datei
|
||||||
|
|
||||||
|
if not einfuegungen:
|
||||||
|
continue
|
||||||
|
einfuegungen.sort(key=lambda x: x[0], reverse=True)
|
||||||
|
result = text
|
||||||
|
for pos, marker, _ in einfuegungen:
|
||||||
|
result = result[:pos] + marker + result[pos:]
|
||||||
|
if result != text:
|
||||||
|
with open(pfad, "w", encoding="utf-8") as f:
|
||||||
|
f.write(result)
|
||||||
|
gesamt_markiert += len(einfuegungen)
|
||||||
|
print(f" {datei}: {len(einfuegungen)} neue Marker")
|
||||||
|
|
||||||
|
print(f"\nMarkiert: {gesamt_markiert} neue Marker")
|
||||||
|
print("Danach: python3 .../build_version_04.py --pdf --html")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Loading…
Reference in a new issue