From 3dcd8d170f25653146b25fc8b1ebe46981181352 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Thu, 10 Sep 2026 01:08:32 +0200 Subject: [PATCH] Stichwortkandidaten: spaCy-basiertes Skript zur Findung neuer Fachbegriffe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../erzeuge_stichwortkandidaten_04.py | 533 ++++++++++++++++++ 1 file changed, 533 insertions(+) create mode 100644 Operations_Research_mit_Python_Version_04/erzeuge_stichwortkandidaten_04.py diff --git a/Operations_Research_mit_Python_Version_04/erzeuge_stichwortkandidaten_04.py b/Operations_Research_mit_Python_Version_04/erzeuge_stichwortkandidaten_04.py new file mode 100644 index 0000000..186c624 --- /dev/null +++ b/Operations_Research_mit_Python_Version_04/erzeuge_stichwortkandidaten_04.py @@ -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](pfad) → 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"(?