Titeltext: eine Quelle fuer PDF-Titelseite und Startseite

Die Startseite zeigte einen anderen Titeltext als das PDF. Der Text stand
VIERMAL im Repository: NEUER_TITEL.md, die Konstanten in
bilder_04/erzeuge_titelseite.py, der Titelblock des Vorworts - und noch einmal
fest einprogrammiert im Hero-Block von baue_landingpage().

Die Ursache ist messbar: Die drei Commits, die den Titel ueberarbeitet haben
(ca5621f, 199dd3b, 1659fb9 im Ursprungs-Repository), fassten jedes Mal
dieselben drei Dateien an. build_version_04.py war in keinem davon, und
OR_HTML_04/index.html taucht in ihren Dateilisten gar nicht auf. Online stand
deshalb:

  Unterzeile  "Von den mathematischen Grundlagen zur quantitativen
              Portfolio-Optimierung an den Finanzmaerkten."
  Kompendium  "Ein vollstaendiges Kompendium mit Algorithmen ..."
  Anwendungen fehlte ganz

Behandlung wie bei den neun Nummern-Funden zuvor: nicht die Kopie korrigieren,
sondern die Kopie abschaffen. Das neue titeltexte_04.py zerlegt NEUER_TITEL.md
in Titel, Untertitel, Kompendium- und Anwendungen-Absatz; PDF-Titelseite und
Startseite lesen dieselbe Funktion. Der Autor bleibt bewusst Konstante im
Generator - die Vorlage beschreibt das Buch, nicht sein Impressum.

Der entscheidende Beleg: Die neu erzeugte titelseite.svg ist BYTE-IDENTISCH
(97fb40d7...) zur vorherigen - der Umbau hat am PDF nachweislich nichts
veraendert. Auch nicht am Umbruch: Die frueher handgesetzten drei Zeilen
entstehen jetzt automatisch aus _umbrechen(), und zwar exakt so. Ein
Textvergleich ueber das SVG war uebrigens nicht moeglich - es enthaelt kein
einziges <text>-Element, matplotlib zeichnet alle 726 Buchstaben als Kurven.

Das Vorwort bleibt von Hand gepflegter Buchtext, wird aber von
pruefe_titeltexte() bei jedem --check gegen die Quelle gestellt. Gleich beim
ersten Lauf fand es einen echten Unterschied: Das Vorwort setzte einen
Geviertstrich, die Vorlage einen Halbgeviertstrich. Im Deutschen ist der
Halbgeviertstrich der richtige - das Vorwort folgt jetzt der Quelle.

Veroeffentlicht: 5 Dateien uebertragen, alle vierzehn HTTP-Proben richtig.
Gegenprobe vom Server: alle drei Absaetze der Startseite sind zeichengenau
identisch mit NEUER_TITEL.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
dschlueter 2026-09-08 19:39:49 +02:00
commit 022bd4eaca
15 changed files with 250 additions and 37 deletions

View file

@ -34,7 +34,8 @@ OR_HTML_04/ generiert: Mehrseiten-Website
Operations_Research_mit_Python_Version_04_Programme/ generiert: Beispielprogramme Operations_Research_mit_Python_Version_04_Programme/ generiert: Beispielprogramme
Notebooks_04/ generiert: ein .ipynb je Kapitel Notebooks_04/ generiert: ein .ipynb je Kapitel
Kritik_und_Verbesserungsvorschlaege/ Quelle: Rezension, Verbesserungsvorschläge, Kritik_und_Verbesserungsvorschlaege/ Quelle: Rezension, Verbesserungsvorschläge,
NEUER_TITEL.md (Vorlage des Titelblatts) NEUER_TITEL.md - die EINE Quelle des
Titeltexts (PDF-Titelseite + Startseite)
pyproject.toml Quelle: Abhängigkeiten in Gruppen pyproject.toml Quelle: Abhängigkeiten in Gruppen
Dockerfile, .dockerignore Quelle: Kurs-Image (Programme + JupyterLab) Dockerfile, .dockerignore Quelle: Kurs-Image (Programme + JupyterLab)
LICENSE, LICENSE-TEXT.md MIT fuer Code, CC BY-SA 4.0 fuer den Text LICENSE, LICENSE-TEXT.md MIT fuer Code, CC BY-SA 4.0 fuer den Text
@ -87,10 +88,11 @@ sonst fälschlich als sichtbaren Umbruch dar.
## Die wichtigsten Konventionen ## Die wichtigsten Konventionen
**Keine abgeleiteten Zahlen im Quelltext.** Das ist die Regel, an der dieses Projekt am **Keine abgeleiteten Zahlen im Quelltext.** Das ist die Regel, an der dieses Projekt am
häufigsten gescheitert ist — **neun** Mal an verschiedenen Stellen verletzt gefunden häufigsten gescheitert ist — **zehn** Mal an verschiedenen Stellen verletzt gefunden
(Anhang A, 66 Programm-Docstrings, die Übersichtstabellen der Anhänge, `CLAUDE.md` selbst, (Anhang A, 66 Programm-Docstrings, die Übersichtstabellen der Anhänge, `CLAUDE.md` selbst,
eine Tabellenzelle, die 98 Lösungsmarken des Anhangs, neun Denkfehler-Verweise, sieben eine Tabellenzelle, die 98 Lösungsmarken des Anhangs, neun Denkfehler-Verweise, sieben
„Übung N.N"-Verweise in alter Zählung, zuletzt die Dateiübersicht des Leser-Wegweisers). „Übung N.N"-Verweise in alter Zählung, die Dateiübersicht des Leser-Wegweisers,
zuletzt der Titeltext der Startseite).
Seit dem letzten Fund trägt **keine** Marke mehr eine handgeschriebene Nummer, und `--check` Seit dem letzten Fund trägt **keine** Marke mehr eine handgeschriebene Nummer, und `--check`
bewacht jede Familie. Konkret: bewacht jede Familie. Konkret:
@ -107,6 +109,12 @@ bewacht jede Familie. Konkret:
einer bereits nummerierten Überschrift. Verwiesen wird auf einer bereits nummerierten Überschrift. Verwiesen wird auf
`{ref:sec:<kapitel>-denkfehler}`. `{ref:sec:<kapitel>-denkfehler}`.
* Im Fließtext `{ref:<label>}`, nie eine Literalzahl. * Im Fließtext `{ref:<label>}`, nie eine Literalzahl.
* **Der Titeltext steht in `Kritik_und_Verbesserungsvorschlaege/NEUER_TITEL.md`** und
nirgends sonst. `titeltexte_04.py` liest ihn; die PDF-Titelseite
(`bilder_04/erzeuge_titelseite.py`) und der Hero-Block der Startseite
(`baue_landingpage()`) speisen sich daraus. Eine Titeländerung wird **einmal** dort
gemacht. Das Vorwort trägt denselben Text als Buchprosa — `pruefe_titeltexte()`
vergleicht ihn bei jedem `--check` mit der Quelle.
* **Die Dateiübersicht in `Operations_Research_mit_Python_Version_04/README.md`** steht * **Die Dateiübersicht in `Operations_Research_mit_Python_Version_04/README.md`** steht
zwischen `<!-- KAPITELTABELLE:ANFANG -->` und `<!-- KAPITELTABELLE:ENDE -->` und wird von zwischen `<!-- KAPITELTABELLE:ANFANG -->` und `<!-- KAPITELTABELLE:ENDE -->` und wird von
`schreibe_kapiteltabelle()` aus derselben Struktur erzeugt wie die Nummern im Buch. Nicht `schreibe_kapiteltabelle()` aus derselben Struktur erzeugt wie die Nummern im Buch. Nicht

View file

@ -309,6 +309,7 @@ article li { margin: .3em 0; }
.hero-untertitel { color: var(--muted); font-size: 1.05rem; } .hero-untertitel { color: var(--muted); font-size: 1.05rem; }
.hero-badges { display: flex; gap: .6rem; margin-top: 1rem; flex-wrap: wrap; } .hero-badges { display: flex; gap: .6rem; margin-top: 1rem; flex-wrap: wrap; }
.hero-kompendium { color: var(--muted); margin-top: 1.4rem; max-width: 640px; } .hero-kompendium { color: var(--muted); margin-top: 1.4rem; max-width: 640px; }
.hero-anwendungen { color: var(--muted); margin-top: .8rem; max-width: 640px; }
.hero-autor { color: var(--muted); font-size: .92rem; line-height: 1.5; } .hero-autor { color: var(--muted); font-size: .92rem; line-height: 1.5; }
.badge { padding: .35em .9em; border-radius: 20px; color: #fff; font-weight: 600; font-size: .85rem; } .badge { padding: .35em .9em; border-radius: 20px; color: #fff; font-weight: 600; font-size: .85rem; }
.badge-indigo { background: var(--accent); } .badge-indigo { background: var(--accent); }

View file

@ -378,7 +378,7 @@
<h1 class="unnumbered" id="optimierte-entscheidungsfindung-mit-python">Optimierte Entscheidungsfindung mit Python</h1> <h1 class="unnumbered" id="optimierte-entscheidungsfindung-mit-python">Optimierte Entscheidungsfindung mit Python</h1>
<h2 class="unnumbered" id="von-der-mathematischen-modellierung-zu-praktischen-intelligenten-lösungen-für-betrieb-technik-und-finanzmärkte">Von der mathematischen Modellierung zu praktischen intelligenten Lösungen für Betrieb, Technik und Finanzmärkte</h2> <h2 class="unnumbered" id="von-der-mathematischen-modellierung-zu-praktischen-intelligenten-lösungen-für-betrieb-technik-und-finanzmärkte">Von der mathematischen Modellierung zu praktischen intelligenten Lösungen für Betrieb, Technik und Finanzmärkte</h2>
<p><strong>Ein praxisorientiertes Kompendium für Entscheider</strong> mit Constraint Programming, Vektor- und Matrixmethoden sowie HiGHS, SciPy, Google OR-Tools und CVXPY.</p> <p><strong>Ein praxisorientiertes Kompendium für Entscheider</strong> mit Constraint Programming, Vektor- und Matrixmethoden sowie HiGHS, SciPy, Google OR-Tools und CVXPY.</p>
<p>Von <strong>Personaleinsatz-, Schicht- und Vertretungsplänen, Fahrtrouten, Logistik und Maschinen- und Netzwerkauslastung</strong> über <strong>Energieverteilung und Ressourcenplanung</strong> bis zur <strong>Portfoliooptimierung und besseren Entscheidungen an den Finanzmärkten</strong> mit Python modellieren, optimieren und belegbar fundierte Entscheidungen treffen.</p> <p>Von <strong>Personaleinsatz-, Schicht- und Vertretungsplänen, Fahrtrouten, Logistik und Maschinen- und Netzwerkauslastung</strong> über <strong>Energieverteilung und Ressourcenplanung</strong> bis zur <strong>Portfoliooptimierung und besseren Entscheidungen an den Finanzmärkten</strong> mit Python modellieren, optimieren und belegbar fundierte Entscheidungen treffen.</p>
<p>Autor / Herausgeber: Dieter Schlüter</p> <p>Autor / Herausgeber: Dieter Schlüter</p>
<p>&lt;dieter(dot)schlueter(atsign)linix(dot)de&gt;</p> <p>&lt;dieter(dot)schlueter(atsign)linix(dot)de&gt;</p>
<p>Stand: 8. September 2026</p> <p>Stand: 8. September 2026</p>

8
OR_HTML_04/index.html generated
View file

@ -104,17 +104,15 @@
<div class="hero-text"> <div class="hero-text">
<p class="hero-kicker">KOMPENDIUM</p> <p class="hero-kicker">KOMPENDIUM</p>
<h1>Optimierte Entscheidungsfindung mit Python</h1> <h1>Optimierte Entscheidungsfindung mit Python</h1>
<p class="hero-untertitel">Von den mathematischen Grundlagen zur quantitativen <p class="hero-untertitel">Von der mathematischen Modellierung zu praktischen intelligenten Lösungen für Betrieb, Technik und Finanzmärkte</p>
Portfolio-Optimierung an den Finanzmärkten.</p>
<div class="hero-badges"> <div class="hero-badges">
<span class="badge badge-indigo">HiGHS</span> <span class="badge badge-indigo">HiGHS</span>
<span class="badge badge-emerald">SciPy</span> <span class="badge badge-emerald">SciPy</span>
<span class="badge badge-cyan">Google OR-Tools</span> <span class="badge badge-cyan">Google OR-Tools</span>
<span class="badge badge-violett">CVXPY</span> <span class="badge badge-violett">CVXPY</span>
</div> </div>
<p class="hero-kompendium"><em>Ein vollständiges Kompendium mit Algorithmen, Constraint <p class="hero-kompendium"><em>Ein praxisorientiertes Kompendium für Entscheider mit Constraint Programming, Vektor- und Matrixmethoden sowie HiGHS, SciPy, Google OR-Tools und CVXPY.</em></p>
Programming, Vektor- und Matrixmethoden sowie modernen Python-Solvern (HiGHS, SciPy, <p class="hero-anwendungen">Von Personaleinsatz-, Schicht- und Vertretungsplänen, Fahrtrouten, Logistik und Maschinen- und Netzwerkauslastung über Energieverteilung und Ressourcenplanung bis zur Portfoliooptimierung und besseren Entscheidungen an den Finanzmärkten mit Python modellieren, optimieren und belegbar fundierte Entscheidungen treffen.</p>
Google OR-Tools, CVXPY)</em></p>
<p class="hero-autor">Autor / Herausgeber: Dieter Schlüter<br> <p class="hero-autor">Autor / Herausgeber: Dieter Schlüter<br>
&lt;dieter(dot)schlueter(atsign)linix(dot)de&gt;<br> &lt;dieter(dot)schlueter(atsign)linix(dot)de&gt;<br>
Stand: 8. September 2026</p> Stand: 8. September 2026</p>

View file

@ -107,7 +107,7 @@
<h1 class="unnumbered" id="optimierte-entscheidungsfindung-mit-python">Optimierte Entscheidungsfindung mit Python</h1> <h1 class="unnumbered" id="optimierte-entscheidungsfindung-mit-python">Optimierte Entscheidungsfindung mit Python</h1>
<h2 class="unnumbered" id="von-der-mathematischen-modellierung-zu-praktischen-intelligenten-lösungen-für-betrieb-technik-und-finanzmärkte">Von der mathematischen Modellierung zu praktischen intelligenten Lösungen für Betrieb, Technik und Finanzmärkte</h2> <h2 class="unnumbered" id="von-der-mathematischen-modellierung-zu-praktischen-intelligenten-lösungen-für-betrieb-technik-und-finanzmärkte">Von der mathematischen Modellierung zu praktischen intelligenten Lösungen für Betrieb, Technik und Finanzmärkte</h2>
<p><strong>Ein praxisorientiertes Kompendium für Entscheider</strong> mit Constraint Programming, Vektor- und Matrixmethoden sowie HiGHS, SciPy, Google OR-Tools und CVXPY.</p> <p><strong>Ein praxisorientiertes Kompendium für Entscheider</strong> mit Constraint Programming, Vektor- und Matrixmethoden sowie HiGHS, SciPy, Google OR-Tools und CVXPY.</p>
<p>Von <strong>Personaleinsatz-, Schicht- und Vertretungsplänen, Fahrtrouten, Logistik und Maschinen- und Netzwerkauslastung</strong> über <strong>Energieverteilung und Ressourcenplanung</strong> bis zur <strong>Portfoliooptimierung und besseren Entscheidungen an den Finanzmärkten</strong> mit Python modellieren, optimieren und belegbar fundierte Entscheidungen treffen.</p> <p>Von <strong>Personaleinsatz-, Schicht- und Vertretungsplänen, Fahrtrouten, Logistik und Maschinen- und Netzwerkauslastung</strong> über <strong>Energieverteilung und Ressourcenplanung</strong> bis zur <strong>Portfoliooptimierung und besseren Entscheidungen an den Finanzmärkten</strong> mit Python modellieren, optimieren und belegbar fundierte Entscheidungen treffen.</p>
<p>Autor / Herausgeber: Dieter Schlüter</p> <p>Autor / Herausgeber: Dieter Schlüter</p>
<p>&lt;dieter(dot)schlueter(atsign)linix(dot)de&gt;</p> <p>&lt;dieter(dot)schlueter(atsign)linix(dot)de&gt;</p>
<p>Stand: 8. September 2026</p> <p>Stand: 8. September 2026</p>

View file

@ -20,7 +20,7 @@
**Ein praxisorientiertes Kompendium für Entscheider** mit Constraint Programming, Vektor- und Matrixmethoden sowie HiGHS, SciPy, Google OR-Tools und CVXPY. **Ein praxisorientiertes Kompendium für Entscheider** mit Constraint Programming, Vektor- und Matrixmethoden sowie HiGHS, SciPy, Google OR-Tools und CVXPY.
Von **Personaleinsatz-, Schicht- und Vertretungsplänen, Fahrtrouten, Logistik und Maschinen- und Netzwerkauslastung** über **Energieverteilung und Ressourcenplanung** bis zur **Portfoliooptimierung und besseren Entscheidungen an den Finanzmärkten** mit Python modellieren, optimieren und belegbar fundierte Entscheidungen treffen. Von **Personaleinsatz-, Schicht- und Vertretungsplänen, Fahrtrouten, Logistik und Maschinen- und Netzwerkauslastung** über **Energieverteilung und Ressourcenplanung** bis zur **Portfoliooptimierung und besseren Entscheidungen an den Finanzmärkten** mit Python modellieren, optimieren und belegbar fundierte Entscheidungen treffen.
Autor / Herausgeber: Dieter Schlüter Autor / Herausgeber: Dieter Schlüter

View file

@ -23,7 +23,7 @@ und Matrixmethoden sowie HiGHS, SciPy, Google OR-Tools und CVXPY.
Von **Personaleinsatz-, Schicht- und Vertretungsplänen, Fahrtrouten, Logistik und Von **Personaleinsatz-, Schicht- und Vertretungsplänen, Fahrtrouten, Logistik und
Maschinen- und Netzwerkauslastung** über **Energieverteilung und Ressourcenplanung** bis zur Maschinen- und Netzwerkauslastung** über **Energieverteilung und Ressourcenplanung** bis zur
**Portfoliooptimierung und besseren Entscheidungen an den Finanzmärkten** mit Python **Portfoliooptimierung und besseren Entscheidungen an den Finanzmärkten** mit Python
modellieren, optimieren und belegbar fundierte Entscheidungen treffen. modellieren, optimieren und belegbar fundierte Entscheidungen treffen.
Autor / Herausgeber: Dieter Schlüter Autor / Herausgeber: Dieter Schlüter

View file

@ -37,6 +37,8 @@ import subprocess
import sys import sys
import zipfile import zipfile
from titeltexte_04 import lies_titeltexte, _saeubern as _saeubere_titeltext
HIER = os.path.dirname(os.path.abspath(__file__)) HIER = os.path.dirname(os.path.abspath(__file__))
BASIS = os.path.dirname(HIER) BASIS = os.path.dirname(HIER)
@ -389,9 +391,77 @@ def pruefe_dateien() -> list[str]:
fehlend.append(name) fehlend.append(name)
fehlend.extend(pruefe_assets()) fehlend.extend(pruefe_assets())
fehlend.extend(pruefe_titeltexte())
return fehlend return fehlend
# Die Datei, die den Titeltext ein viertes Mal traegt: als Buchtext, den ein
# Leser im Vorwort sieht. Erzeugt wird er dort NICHT - er steht mit
# Hervorhebungen im Fliesstext und gehoert dem Autor. Aber er wird geprueft.
VORWORT_DATEI = "00_Vorwort_und_Lesehilfe.md"
def pruefe_titeltexte() -> list[str]:
"""Vergleicht den Titelblock des Vorworts mit NEUER_TITEL.md.
PDF-Titelseite und Startseite lesen die Quelle direkt und koennen deshalb
gar nicht abweichen. Das Vorwort ist die dritte Stelle - und genau eine
solche ungeprueft mitgefuehrte Kopie hat dazu gefuehrt, dass die Startseite
monatelang einen anderen Untertitel zeigte als das PDF."""
quelle = lies_titeltexte()
pfad = os.path.join(HIER, VORWORT_DATEI)
if not os.path.exists(pfad):
return []
with open(pfad, encoding="utf-8") as f:
zeilen = f.read().split("\n")
# Gesucht werden die '#'- und '##'-Ueberschrift und die beiden Absaetze
# danach. Der '{-}'-Zusatz (Pandoc: nicht nummerieren) gehoert nicht zum
# Text.
def ohne_marke(text: str) -> str:
return re.sub(r"\s*\{-\}\s*$", "", text)
gefunden: dict[str, tuple[int, str]] = {}
absaetze: list[tuple[int, str]] = []
block: list[str] = []
block_start = 0
for nr, zeile in enumerate(zeilen, start=1):
if zeile.startswith("# ") and "titel" not in gefunden:
gefunden["titel"] = (nr, _saeubere_titeltext(ohne_marke(zeile[2:])))
elif zeile.startswith("## ") and "untertitel" not in gefunden:
gefunden["untertitel"] = (nr, _saeubere_titeltext(ohne_marke(zeile[3:])))
elif zeile.strip() and not zeile.startswith("#") and "untertitel" in gefunden:
if not block:
block_start = nr
block.append(zeile)
else:
if block:
absaetze.append((block_start, _saeubere_titeltext(" ".join(block))))
block = []
if len(absaetze) == 2:
break
if block:
absaetze.append((block_start, _saeubere_titeltext(" ".join(block))))
for name, eintrag in zip(("kompendium", "anwendungen"), absaetze):
gefunden[name] = eintrag
fehler = []
for name in ("titel", "untertitel", "kompendium", "anwendungen"):
if name not in gefunden:
print(f"FEHLER: {VORWORT_DATEI}: Der Titelblock enthaelt kein "
f"'{name}' - erwartet werden '#', '##' und zwei Absaetze.")
fehler.append(f"{VORWORT_DATEI}:{name}")
continue
nr, ist = gefunden[name]
if ist != quelle[name]:
print(f"FEHLER: {VORWORT_DATEI}:{nr}: '{name}' weicht von der "
f"Titelquelle ab.")
print(f" Vorwort: {ist}")
print(f" Quelle: {quelle[name]}")
fehler.append(f"{VORWORT_DATEI}:{nr}")
return fehler
# Dateien, die der Bau selbst in OR_HTML_04/assets/ erzeugt. Alles andere, was # Dateien, die der Bau selbst in OR_HTML_04/assets/ erzeugt. Alles andere, was
# der Seitenkopf einbindet, muss aus der Quelle web_04/ kommen. # der Seitenkopf einbindet, muss aus der Quelle web_04/ kommen.
ERZEUGTE_ASSETS = { ERZEUGTE_ASSETS = {
@ -2172,22 +2242,25 @@ def baue_landingpage(html_verz: str, struktur: list[dict], labels: dict,
f'<section class="kapitelgruppe"><h2>{titel}</h2>' f'<section class="kapitelgruppe"><h2>{titel}</h2>'
f'<div class="kartenraster">{karten}</div></section>') f'<div class="kartenraster">{karten}</div></section>')
# Dieselbe Quelle wie die Titelseite des PDFs (NEUER_TITEL.md). Frueher
# standen die drei Absaetze hier fest im Quelltext - und wichen deshalb
# vom PDF ab, sobald der Titel ueberarbeitet wurde.
titeltexte = lies_titeltexte()
inhalt = f""" inhalt = f"""
<section class="hero"> <section class="hero">
<div class="hero-text"> <div class="hero-text">
<p class="hero-kicker">KOMPENDIUM</p> <p class="hero-kicker">KOMPENDIUM</p>
<h1>{SITE_TITEL}</h1> <h1>{SITE_TITEL}</h1>
<p class="hero-untertitel">Von den mathematischen Grundlagen zur quantitativen <p class="hero-untertitel">{titeltexte["untertitel"]}</p>
Portfolio-Optimierung an den Finanzmärkten.</p>
<div class="hero-badges"> <div class="hero-badges">
<span class="badge badge-indigo">HiGHS</span> <span class="badge badge-indigo">HiGHS</span>
<span class="badge badge-emerald">SciPy</span> <span class="badge badge-emerald">SciPy</span>
<span class="badge badge-cyan">Google OR-Tools</span> <span class="badge badge-cyan">Google OR-Tools</span>
<span class="badge badge-violett">CVXPY</span> <span class="badge badge-violett">CVXPY</span>
</div> </div>
<p class="hero-kompendium"><em>Ein vollständiges Kompendium mit Algorithmen, Constraint <p class="hero-kompendium"><em>{titeltexte["kompendium"]}</em></p>
Programming, Vektor- und Matrixmethoden sowie modernen Python-Solvern (HiGHS, SciPy, <p class="hero-anwendungen">{titeltexte["anwendungen"]}</p>
Google OR-Tools, CVXPY)</em></p>
<p class="hero-autor">Autor / Herausgeber: Dieter Schlüter<br> <p class="hero-autor">Autor / Herausgeber: Dieter Schlüter<br>
&lt;dieter(dot)schlueter(atsign)linix(dot)de&gt;<br> &lt;dieter(dot)schlueter(atsign)linix(dot)de&gt;<br>
Stand: {aktuelles_datum()}</p> Stand: {aktuelles_datum()}</p>

View file

@ -0,0 +1,82 @@
#!/usr/bin/env python3
# titeltexte_04.py
#
# Liest den Titeltext des Buchs aus seiner EINEN Quelle:
# Kritik_und_Verbesserungsvorschlaege/NEUER_TITEL.md
#
# Warum es dieses Modul gibt: Der Titeltext stand viermal im Repository - in
# NEUER_TITEL.md, in den Konstanten von bilder_04/erzeuge_titelseite.py (PDF),
# im Vorwort und fest einprogrammiert im Hero-Block von baue_landingpage()
# (Website). Drei Ueberarbeitungen des Titels zogen die ersten drei nach und
# liessen die vierte stehen: Die Startseite zeigte monatelang einen anderen
# Untertitel als das PDF, und der Anwendungen-Absatz fehlte dort ganz.
# Aufgefallen ist es niemandem, weil nichts die Fassungen verglichen hat.
#
# Jetzt lesen PDF-Titelseite und Startseite denselben Text ueber
# lies_titeltexte(); das Vorwort bleibt von Hand gepflegter Buchtext, wird aber
# von pruefe_dateien() gegen diese Quelle geprueft.
import os
import re
HIER = os.path.dirname(os.path.abspath(__file__))
BASIS = os.path.dirname(HIER)
TITEL_QUELLE = os.path.join(BASIS, "Kritik_und_Verbesserungsvorschlaege",
"NEUER_TITEL.md")
def _saeubern(text: str) -> str:
"""Entfernt Markdown-Hervorhebungen und zieht Zeilenumbrueche zu einfachen
Leerzeichen zusammen. Die Vorlage setzt die Fettungen anders als das
Vorwort - fuer den Vergleich zaehlt allein der Wortlaut."""
return " ".join(text.replace("**", "").replace("*", "").split())
def lies_titeltexte(pfad: str = TITEL_QUELLE) -> dict[str, str]:
"""Zerlegt NEUER_TITEL.md in seine vier Bestandteile.
Aufbau der Vorlage:
# <Titel>
### <Untertitel>
**<Kompendium-Absatz>**
<Anwendungen-Absatz>
Fehlt ein Teil, wird abgebrochen statt ein halb gefuelltes Titelblatt zu
erzeugen - ein Titelblatt ohne Untertitel faellt im PDF erst auf, wenn es
gedruckt ist."""
if not os.path.exists(pfad):
raise SystemExit(f"FEHLER: Die Quelle des Titeltexts fehlt: {pfad}")
with open(pfad, encoding="utf-8") as f:
inhalt = f.read()
bloecke = [b.strip() for b in re.split(r"\n\s*\n", inhalt) if b.strip()]
texte: dict[str, str] = {}
absaetze: list[str] = []
for block in bloecke:
if block.startswith("### "):
texte.setdefault("untertitel", _saeubern(block[4:]))
elif block.startswith("# "):
texte.setdefault("titel", _saeubern(block[2:]))
elif not block.startswith("#"):
absaetze.append(_saeubern(block))
if len(absaetze) >= 2:
texte["kompendium"], texte["anwendungen"] = absaetze[0], absaetze[1]
fehlend = [name for name in ("titel", "untertitel", "kompendium", "anwendungen")
if not texte.get(name)]
if fehlend:
raise SystemExit(
f"FEHLER: In {os.path.relpath(pfad, BASIS)} fehlt: "
f"{', '.join(fehlend)}.\n"
f" Erwartet werden eine '#'-Ueberschrift (Titel), eine "
f"'###'-Ueberschrift\n"
f" (Untertitel) und zwei Absaetze (Kompendium, Anwendungen).")
return texte
if __name__ == "__main__":
for name, text in lies_titeltexte().items():
print(f"{name}:\n {text}\n")

View file

@ -2192,6 +2192,47 @@ Seiten bei jedem Lauf neu, auch wenn sich nichts geändert hat; nach Zeitstempel
hätte das jedes Mal rund 19 MB sinnlose Übertragung bedeutet. Gemessen nach einem Neubau hätte das jedes Mal rund 19 MB sinnlose Übertragung bedeutet. Gemessen nach einem Neubau
ohne inhaltliche Änderung: **0 statt 151 Dateien**. ohne inhaltliche Änderung: **0 statt 151 Dateien**.
### 🐛 Zehnter Fund: Startseite und PDF-Titelseite zeigten verschiedene Texte
Die Frage lautete, warum die Titelseite der Online-Fassung einen anderen Text trägt als die
des PDFs. Die Antwort war messbar. Der Titeltext stand **viermal** im Repository:
`NEUER_TITEL.md`, die Konstanten in `bilder_04/erzeuge_titelseite.py`, der Titelblock des
Vorworts — und noch einmal fest einprogrammiert im Hero-Block von `baue_landingpage()`.
Die drei Commits, die den Titel überarbeitet haben (`ca5621f`, `199dd3b`, `1659fb9` im
Ursprungs-Repository), fassten jedes Mal dieselben drei Dateien an. **`build_version_04.py`
war in keinem davon**, und `OR_HTML_04/index.html` taucht in ihren Dateilisten überhaupt
nicht auf. Die Startseite zeigte deshalb:
| | PDF | Online (vorher) |
| --- | --- | --- |
| Unterzeile | „Von der mathematischen Modellierung zu praktischen intelligenten Lösungen für Betrieb, Technik und Finanzmärkte" | „Von den mathematischen Grundlagen zur quantitativen Portfolio-Optimierung an den Finanzmärkten." |
| Kompendium | „Ein **praxisorientiertes** Kompendium **für Entscheider** …" | „Ein **vollständiges** Kompendium mit **Algorithmen** …" |
| Anwendungen | vorhanden | **fehlte ganz** |
**Die Behandlung ist die inzwischen eingeübte:** Nicht die Kopie korrigieren, sondern die
Kopie abschaffen. `titeltexte_04.py` zerlegt `NEUER_TITEL.md` in Titel, Untertitel,
Kompendium- und Anwendungen-Absatz; PDF-Titelseite und Startseite lesen dieselbe Funktion.
Der Autor steht bewusst weiter als Konstante im Generator — die Vorlage beschreibt das Buch,
nicht sein Impressum.
**Der entscheidende Beleg:** Die neu erzeugte `titelseite.svg` ist **byte-identisch**
(`97fb40d7…`) zur vorherigen. Der Umbau hat am PDF nachweislich nichts verändert — auch
nicht am Titelumbruch: Die früher handgesetzten drei Zeilen („Optimierte" /
„Entscheidungsfindung" / „mit Python") entstehen jetzt automatisch aus `_umbrechen()`, und
zwar exakt so. Der Textvergleich über das SVG scheiterte übrigens: Es enthält **kein einziges
`<text>`-Element**, matplotlib zeichnet alle 726 Buchstaben als Kurven. Die Prüfsumme ist
hier der bessere Zeuge.
**Das Vorwort** bleibt von Hand gepflegter Buchtext, wird aber von `pruefe_titeltexte()` bei
jedem `--check` gegen die Quelle gestellt. Gleich beim ersten Lauf fand es einen echten
Unterschied: Das Vorwort setzte einen Geviertstrich („—"), die Vorlage einen
Halbgeviertstrich („–"). Im Deutschen ist der Halbgeviertstrich der richtige — das Vorwort
folgt jetzt der Quelle.
Damit ist jede der vier Stellen abgesichert: eine ist die Quelle, zwei werden daraus erzeugt,
eine wird geprüft.
--- ---
## 8. Commit-Historie des V04-Strangs ## 8. Commit-Historie des V04-Strangs

View file

@ -155,7 +155,7 @@ python3 bilder_04/erzeuge_kondition.py
`--check` prüft mehr als die Struktur: fehlende Codezäune, tote Dateilinks, eine gebrochene `--check` prüft mehr als die Struktur: fehlende Codezäune, tote Dateilinks, eine gebrochene
Lesekette, Lösungsabschnitte mit falscher Aufgabenzahl — und **jede von Hand geschriebene Lesekette, Lösungsabschnitte mit falscher Aufgabenzahl — und **jede von Hand geschriebene
Nummer**, die der Build selbst vergeben müsste. Diese Regel ist die in diesem Projekt am Nummer**, die der Build selbst vergeben müsste. Diese Regel ist die in diesem Projekt am
häufigsten verletzte; neun Fälle wurden nachträglich gefunden, und jeder hat eine eigene häufigsten verletzte; zehn Fälle wurden nachträglich gefunden, und jeder hat eine eigene
Prüfung hinterlassen. Prüfung hinterlassen.
### Externe Werkzeuge ### Externe Werkzeuge

View file

@ -28,8 +28,18 @@ Aufruf:
""" """
import os import os
import sys
from datetime import datetime from datetime import datetime
# Der Titeltext kommt aus NEUER_TITEL.md, gelesen von titeltexte_04.py. Das
# Modul liegt im Quellverzeichnis des Buchs, dieses Skript in bilder_04/ -
# daher der Pfadeintrag. Ein zweiter Parser hier waere genau die Dopplung,
# die dieses Modul beseitigt.
_BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(_BASIS,
"Operations_Research_mit_Python_Version_04"))
from titeltexte_04 import lies_titeltexte
import matplotlib import matplotlib
matplotlib.use("Agg") matplotlib.use("Agg")
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
@ -192,26 +202,25 @@ def _setze_block(ax, zeilen, x, y0, groesse, durchschuss, **stil) -> float:
# Absaetze KOMPENDIUM und ANWENDUNGEN # Absaetze KOMPENDIUM und ANWENDUNGEN
# stehen gemeinsam in der grossen Solver-Karte. # stehen gemeinsam in der grossen Solver-Karte.
# #
# Im TITEL steht ein \n: An dieser Stelle wird bewusst umbrochen. Ohne ihn # Sie stehen NICHT hier, sondern in NEUER_TITEL.md - derselben Datei, aus der
# wuerde automatisch umbrochen und "mit Python" auseinandergerissen. Die Breite # auch die Startseite der Website ihren Hero-Block speist. Vorher trug dieses
# jeder Zeile wird in beiden Faellen geprueft. # Skript eigene Konstanten, und die Startseite noch einmal eigene; drei
# Titelueberarbeitungen zogen diese hier nach und liessen jene stehen.
# #
# Warum drei Zeilen und nicht zwei: "Entscheidungsfindung" ist bei 34 pt schon # Der TITEL kommt einzeilig an und wird von _umbrechen() auf den Satzspiegel
# 429 pt breit und damit fast so breit wie der ganze Satzspiegel (453 pt). Jede # verteilt. Das ergibt dieselben drei Zeilen wie die frueher handgesetzten
# zweizeilige Aufteilung braeuchte mindestens 649 pt - das passt bei keiner # ("Optimierte" / "Entscheidungsfindung" / "mit Python") - und das ist kein
# lesbaren Schriftgroesse. # Zufall: "Entscheidungsfindung" ist bei 34 pt schon 429 pt breit und damit
TITEL = "Optimierte\nEntscheidungsfindung\nmit Python" # fast so breit wie der ganze Satzspiegel (453 pt). Jede zweizeilige Aufteilung
UNTERTITEL = ("Von der mathematischen Modellierung zu praktischen " # braeuchte mindestens 649 pt und passt bei keiner lesbaren Schriftgroesse.
"intelligenten Lösungen für Betrieb, Technik und Finanzmärkte") _TEXTE = lies_titeltexte()
KOMPENDIUM = ("Ein praxisorientiertes Kompendium für Entscheider mit " TITEL = _TEXTE["titel"]
"Constraint Programming, Vektor- und Matrixmethoden sowie " UNTERTITEL = _TEXTE["untertitel"]
"HiGHS, SciPy, Google OR-Tools und CVXPY.") KOMPENDIUM = _TEXTE["kompendium"]
ANWENDUNGEN = ("Von Personaleinsatz-, Schicht- und Vertretungsplänen, " ANWENDUNGEN = _TEXTE["anwendungen"]
"Fahrtrouten, Logistik und Maschinen- und Netzwerkauslastung "
"über Energieverteilung " # Der Autor steht nicht in der Titelvorlage - sie beschreibt das Buch, nicht
"und Ressourcenplanung bis zur Portfoliooptimierung und besseren " # sein Impressum.
"Entscheidungen an den Finanzmärkten mit Python modellieren, "
"optimieren und belegbar fundierte Entscheidungen treffen.")
AUTOR = "Autor / Herausgeber: Dieter Schlüter" AUTOR = "Autor / Herausgeber: Dieter Schlüter"
# Satzspiegel: linker Rand und rechte Kante wie Trennlinie und Kartenreihe. # Satzspiegel: linker Rand und rechte Kante wie Trennlinie und Kartenreihe.

View file

@ -309,6 +309,7 @@ article li { margin: .3em 0; }
.hero-untertitel { color: var(--muted); font-size: 1.05rem; } .hero-untertitel { color: var(--muted); font-size: 1.05rem; }
.hero-badges { display: flex; gap: .6rem; margin-top: 1rem; flex-wrap: wrap; } .hero-badges { display: flex; gap: .6rem; margin-top: 1rem; flex-wrap: wrap; }
.hero-kompendium { color: var(--muted); margin-top: 1.4rem; max-width: 640px; } .hero-kompendium { color: var(--muted); margin-top: 1.4rem; max-width: 640px; }
.hero-anwendungen { color: var(--muted); margin-top: .8rem; max-width: 640px; }
.hero-autor { color: var(--muted); font-size: .92rem; line-height: 1.5; } .hero-autor { color: var(--muted); font-size: .92rem; line-height: 1.5; }
.badge { padding: .35em .9em; border-radius: 20px; color: #fff; font-weight: 600; font-size: .85rem; } .badge { padding: .35em .9em; border-radius: 20px; color: #fff; font-weight: 600; font-size: .85rem; }
.badge-indigo { background: var(--accent); } .badge-indigo { background: var(--accent); }