SemaTrain Logo Ein Fachportal von SemaTrain

Automatisierung & Scripting

Sie bauen kleine, nützliche Tools: CLI, Dateien, APIs, Logging und Scheduling – so, wie Python in der Praxis oft wirklich eingesetzt wird.

Hinweis: Beispiele sind bewusst „production-nah“ (Fehlerbehandlung, klare Funktionen, Logging).

Python Schulung – Kursbezug

Dieses Kapitel ist Teil des Lernpfads zur Python Schulung. Termine & Buchung laufen über SemaTrain.de.

Ziel: Sie können ein kleines Tool bauen, das Daten einliest, verarbeitet, speichert und zuverlässig geloggt wird – inkl. „läuft täglich“ (Scheduling).

Praxis-Merksatz: Ein gutes Script ist bedienbar (CLI), robust (Fehler/Logging) und wiederholbar (Scheduling).

Worum geht’s?

Lehr-/Lernziele

Nach diesem Kapitel können Sie …

1) CLI: Parameter mit argparse

SemaTrain-Beispiel: Report für einen Standort/Format erzeugen.

CLI mit argparse (LZ1) (Python)
import argparse

def parse_args():
    parser = argparse.ArgumentParser(
        prog="kurs_report",
        description="Erzeugt einen Kurs-Report (Beispieldaten)."
    )
    parser.add_argument("--standort", default="Berlin", help="z.B. Berlin, Hamburg")
    parser.add_argument("--format", default="Online", help="Online oder Praesenz")
    parser.add_argument("--limit", type=int, default=10, help="Max. Anzahl Einträge")
    return parser.parse_args()

if __name__ == "__main__":
    args = parse_args()
    print("Parameter:", args)

2) Dateien/Ordner: pathlib + CSV/JSON

CSV lesen (Beispiel)

CSV einlesen (LZ2) (Python)
from pathlib import Path
import csv

pfad_csv = Path("daten") / "kurse.csv"

with pfad_csv.open("r", encoding="utf-8", newline="") as f:
    reader = csv.DictReader(f)
    kurse = list(reader)

print("Anzahl Kurse:", len(kurse))
print("Erster Eintrag:", kurse[0])

Tipp: newline=\"\" bei CSV verhindert typische Zeilenumbruchs-Probleme (Windows).

JSON schreiben (Report)

JSON-Report schreiben (LZ2) (Python)
from pathlib import Path
import json

report = {
    "anbieter": "SemaTrain",
    "standort": "Berlin",
    "format": "Online",
    "anzahl": 2,
    "kurse": [
        {"kursname": "Python Schulung", "preis_euro": 1390},
        {"kursname": "Python Schulung", "preis_euro": 1490},
    ],
}

pfad_out = Path("out") / "report.json"
pfad_out.parent.mkdir(parents=True, exist_ok=True)

pfad_out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print("Geschrieben:", pfad_out)

3) API-Abfrage: requests + Fehlerbehandlung

Beispiel zeigt Prinzip (Timeout, Status prüfen, Exceptions). Für echte APIs brauchen Sie URL + ggf. Token.

requests: robustes API-Pattern (LZ3) (Python)
import requests

def lade_json(url: str, timeout_s: int = 8) -> dict:
    try:
        resp = requests.get(url, timeout=timeout_s)
        resp.raise_for_status()  # wirft Exception bei 4xx/5xx
        return resp.json()
    except requests.Timeout:
        raise RuntimeError("Timeout: API antwortet nicht rechtzeitig.")
    except requests.HTTPError as e:
        raise RuntimeError(f"HTTP-Fehler: {e}")
    except requests.RequestException as e:
        raise RuntimeError(f"Request-Fehler: {e}")

# Beispiel (URL ersetzen!)
# daten = lade_json("https://api.example.com/kurse")
# print(daten)
Hinweis: Installation (venv + pip)
Virtuelle Umgebung + requests installieren (Bash)
python -m venv .venv
# Windows: .venv\Scripts\activate
# macOS/Linux:
source .venv/bin/activate

pip install requests

4) Logging: statt nur print()

Logging-Grundgerüst (LZ4) (Python)
import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(message)s"
)

logger = logging.getLogger("kurs_tool")

def main():
    logger.info("Start: Kurs-Tool")
    standort = "Berlin"
    try:
        # hier würde echte Arbeit passieren
        logger.info("Standort: %s", standort)
        logger.info("Report erstellt.")
    except Exception:
        logger.exception("Unerwarteter Fehler")
        raise
    finally:
        logger.info("Ende.")

if __name__ == "__main__":
    main()

5) Scheduling: täglich laufen lassen

Cron (Linux/macOS)

Cron Beispiel (LZ4) (Bash)
# crontab -e
# täglich um 07:15 Uhr:
15 7 * * * /pfad/zur/.venv/bin/python /pfad/zum/kurs_report.py --standort Berlin --format Online >> /pfad/logs/report.log 2>&1

Windows Aufgabenplanung

  • „Aufgabe erstellen…“ → Trigger: täglich
  • Aktion: Programm starten
  • Programm/Skript: python.exe
  • Argumente: kurs_report.py --standort Berlin --format Online
  • „Start in“: Projektordner (damit relative Pfade stimmen)

Praxis-Tipp: Logdatei schreiben lassen – später wissen Sie, was passiert ist.

Praxisaufgabe (Mini)

Sie bauen ein CLI-Tool kurs_report.py, das Kursdaten filtert und einen Report schreibt.

Beitrag zu den Lehr-/Lernzielen: LZ1 (CLI), LZ2 (Dateien/JSON), LZ4 (Logging/Scheduling) – optional LZ3 (API).

  1. (LZ1) Parameter: --standort, --format, --out (Pfad), optional --limit.
  2. (LZ2) Kursdaten aus CSV oder aus eingebauter Liste laden (Beispieldaten ok).
  3. (LZ2) Report als JSON schreiben (inkl. Zeitstempel, Anzahl, Liste).
  4. (LZ4) Logging: Start/Ende + Anzahl + Fehler als logger.exception.
  5. (Bonus, LZ3) Alternative Quelle: API laden (requests) und gleiches Report-Format schreiben.
Lösungsvorschlag anzeigen (kompakt, production-nah)
Lösung: kurs_report.py (LZ1/LZ2/LZ4) (Python)
from __future__ import annotations

from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from pathlib import Path
import argparse
import json
import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(message)s"
)
logger = logging.getLogger("kurs_report")

@dataclass(frozen=True)
class Kurs:
    kursname: str
    format: str       # "Online" / "Praesenz"
    standort: str
    preis_euro: int

def parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(description="Erzeugt einen Kurs-Report (Beispieldaten).")
    p.add_argument("--standort", default="Berlin")
    p.add_argument("--format", default="Online")
    p.add_argument("--limit", type=int, default=10)
    p.add_argument("--out", default="out/report.json")
    return p.parse_args()

def lade_beispieldaten() -> list[Kurs]:
    return [
        Kurs("Python Schulung", "Online", "Berlin", 1390),
        Kurs("Python Schulung", "Praesenz", "Hamburg", 1490),
        Kurs("Python Schulung", "Online", "Hamburg", 1390),
        Kurs("Python Schulung", "Praesenz", "Berlin", 1490),
    ]

def filtere_kurse(kurse: list[Kurs], standort: str, format: str, limit: int) -> list[Kurs]:
    gefiltert = [k for k in kurse if k.standort == standort and k.format == format]
    return gefiltert[: max(limit, 0)]

def schreibe_report(pfad: Path, standort: str, format: str, kurse: list[Kurs]) -> None:
    report = {
        "anbieter": "SemaTrain",
        "erstellt_utc": datetime.now(timezone.utc).isoformat(),
        "standort": standort,
        "format": format,
        "anzahl": len(kurse),
        "kurse": [asdict(k) for k in kurse],
    }

    pfad.parent.mkdir(parents=True, exist_ok=True)
    pfad.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")

def main() -> int:
    args = parse_args()
    logger.info("Start | standort=%s | format=%s | limit=%s | out=%s", args.standort, args.format, args.limit, args.out)

    try:
        kurse = lade_beispieldaten()
        auswahl = filtere_kurse(kurse, args.standort, args.format, args.limit)
        schreibe_report(Path(args.out), args.standort, args.format, auswahl)
        logger.info("OK | Report geschrieben | anzahl=%s", len(auswahl))
        return 0
    except Exception:
        logger.exception("Fehler beim Erstellen des Reports")
        return 2
    finally:
        logger.info("Ende")

if __name__ == "__main__":
    raise SystemExit(main())

Kurz-Takeaways

Quiz: Automatisierung & Scripting

1. (LZ1) Wofür ist argparse in Python typisch?

2. (LZ2) Warum ist pathlib oft besser als String-Pfade?

3. (LZ3) Welche zwei Dinge sind bei API-Requests besonders wichtig?

4. (LZ4) Was ist der Vorteil von Logging gegenüber print() im Betrieb?

Praxisaufgabe

Mini-Projekt: „Kurs-Report“ als Tagesjob (CLI + Logging + Export)

Sie bauen ein kleines Automations-Tool, das Kursdaten (Beispieldaten) filtert, als JSON-Report speichert und sauber loggt. Optional kann das Tool Daten aus einer API holen.

Beitrag zu den Lehr-/Lernzielen:
LZ1 (CLI/argparse), LZ2 (Dateien/JSON), LZ3 (API optional), LZ4 (Logging + „Betrieb“).

Aufgabe

Lösung anzeigen (production-nah, kompakt)
Lösung: kurs_report.py (CLI + JSON + Logging + optional CSV/API) (Python)
from __future__ import annotations

import argparse
import csv
import json
import logging
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

logger = logging.getLogger("kurs_report")


# ---------- Datenmodell ----------
@dataclass(frozen=True)
class Kurs:
    kursname: str
    format: str       # "Online" / "Praesenz"
    standort: str
    dauer_tage: int
    preis_euro: float


# ---------- CLI ----------
def parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(
        prog="kurs_report",
        description="Erzeugt einen Kurs-Report (Beispieldaten) als JSON."
    )
    p.add_argument("--standort", default="Berlin", help="z.B. Berlin, Hamburg")
    p.add_argument("--format", default="Online", help="Online oder Praesenz")
    p.add_argument("--limit", type=int, default=10, help="Max. Einträge im Report")
    p.add_argument("--out", default="out/report.json", help="Output-Datei (JSON)")
    p.add_argument("--dry-run", action="store_true", help="Nur anzeigen, nichts schreiben")

    # Optional: CSV als Quelle
    p.add_argument(
        "--csv",
        default="",
        help="Optional: CSV-Datei mit Kursdaten (Header: kursname,format,standort,dauer_tage,preis_euro)"
    )

    # Optional: API als Quelle
    p.add_argument("--api", default="", help="Optional: API-URL, liefert JSON-Liste von Kursen")
    p.add_argument("--timeout", type=int, default=8, help="API-Timeout in Sekunden")

    return p.parse_args()


# ---------- Logging Setup ----------
def setup_logging() -> None:
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s | %(levelname)s | %(message)s"
    )


# ---------- Datenquellen ----------
def lade_beispieldaten() -> list[Kurs]:
    return [
        Kurs("Python Schulung", "Online",   "Berlin",   3, 1390.00),
        Kurs("Python Schulung", "Praesenz", "Hamburg",  3, 1490.00),
        Kurs("Python Schulung", "Online",   "Hamburg",  3, 1290.00),
        Kurs("Python Schulung", "Praesenz", "Berlin",   3, 1490.00),
        Kurs("Python Schulung", "Online",   "Koeln",    2,  990.00),
        Kurs("Python Schulung", "Praesenz", "Muenchen", 4, 1890.00),
    ]


def lade_csv(pfad: Path) -> list[Kurs]:
    kurse: list[Kurs] = []
    with pfad.open("r", encoding="utf-8", newline="") as f:
        reader = csv.DictReader(f)
        for z in reader:
            kurse.append(Kurs(
                kursname=str(z["kursname"]),
                format=str(z["format"]),
                standort=str(z["standort"]),
                dauer_tage=int(z["dauer_tage"]),
                preis_euro=float(z["preis_euro"]),
            ))
    return kurse


def lade_api(url: str, timeout_s: int) -> list[Kurs]:
    # requests nur nutzen, wenn API-Mode aktiv ist
    import requests  # type: ignore

    try:
        resp = requests.get(url, timeout=timeout_s)
        resp.raise_for_status()
        daten: Any = resp.json()

        if not isinstance(daten, list):
            raise RuntimeError("API liefert kein Listen-JSON.")

        kurse: list[Kurs] = []
        for item in daten:
            kurse.append(Kurs(
                kursname=str(item["kursname"]),
                format=str(item["format"]),
                standort=str(item["standort"]),
                dauer_tage=int(item["dauer_tage"]),
                preis_euro=float(item["preis_euro"]),
            ))
        return kurse

    except requests.Timeout:
        raise RuntimeError("Timeout: API antwortet nicht rechtzeitig.")
    except requests.RequestException as e:
        raise RuntimeError(f"API-Request fehlgeschlagen: {e}")


# ---------- Verarbeitung ----------
def filtere_kurse(kurse: list[Kurs], standort: str, format_: str) -> list[Kurs]:
    return [k for k in kurse if k.standort == standort and k.format == format_]


def baue_report(standort: str, format_: str, limit: int, kurse: list[Kurs]) -> dict[str, Any]:
    # Sortierung: teuerste zuerst (Beispiel)
    kurse_sortiert = sorted(kurse, key=lambda k: k.preis_euro, reverse=True)
    top = kurse_sortiert[: max(limit, 0)]

    return {
        "anbieter": "SemaTrain",
        "erstellt_utc": datetime.now(timezone.utc).isoformat(),
        "filter": {"standort": standort, "format": format_, "limit": limit},
        "anzahl_treffer": len(kurse),
        "top_kurse": [asdict(k) for k in top],
    }


def schreibe_json(pfad: Path, payload: dict[str, Any]) -> None:
    pfad.parent.mkdir(parents=True, exist_ok=True)
    pfad.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")


# ---------- Main ----------
def main() -> int:
    setup_logging()
    args = parse_args()

    logger.info(
        "Start | standort=%s | format=%s | limit=%s | out=%s | dry_run=%s",
        args.standort, args.format, args.limit, args.out, args.dry_run
    )

    try:
        # Quelle wählen: API > CSV > Beispiel
        if args.api:
            logger.info("Quelle: API (%s)", args.api)
            kurse = lade_api(args.api, args.timeout)
        elif args.csv:
            pfad = Path(args.csv)
            logger.info("Quelle: CSV (%s)", pfad)
            kurse = lade_csv(pfad)
        else:
            logger.info("Quelle: integrierte Beispieldaten")
            kurse = lade_beispieldaten()

        gefiltert = filtere_kurse(kurse, args.standort, args.format)
        logger.info("Treffer: %s", len(gefiltert))

        report = baue_report(args.standort, args.format, args.limit, gefiltert)

        if args.dry_run:
            logger.info("DRY-RUN: Report nicht geschrieben. Vorschau:")
            logger.info(json.dumps(report, ensure_ascii=False))
        else:
            schreibe_json(Path(args.out), report)
            logger.info("OK | Report geschrieben: %s", args.out)

        return 0

    except Exception:
        logger.exception("Fehler beim Erstellen des Reports")
        return 2

    finally:
        logger.info("Ende")


if __name__ == "__main__":
    raise SystemExit(main())