spaCy-Kandidaten-Skript verworfen, Dateien entfernt

This commit is contained in:
dschlueter 2026-09-10 01:30:44 +02:00
commit 3a8cab87c2

View file

@ -1,533 +0,0 @@
#!/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: 2N 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"(?<![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())