- 1 Section
- 10 Lessons
- unbegrenzt
- Python Fortgeschritten10
- 1.1Module und Pakete: import, pip, venv
- 1.2Objektorientierung in Python: Klassen, __init__, self
- 1.3Vererbung und Mehrfachvererbung in Python
- 1.4Decorators und Generatoren
- 1.5Fehlerbehandlung vertieft: eigene Exceptions
- 1.6Reguläre Ausdrücke mit re
- 1.7Arbeiten mit APIs: requests, JSON
- 1.8Automatisierung mit os, subprocess, pathlib
- 1.9Unit-Tests in Python: pytest
- 1.10Praxisprojekt: Automatisierungsskript mit Tests
Automatisierung mit os, subprocess, pathlib
Python ist eine perfekte Sprache für Automatisierung: Dateien massenhaft umbenennen, Logs auswerten, regelmäßig Backups starten, externe Programme aufrufen. Wo du sonst ein Bash- oder Shell-Skript schreiben würdest, geht in Python oft mehr – lesbar und portabel.
Diese Lektion zeigt die drei wichtigsten Werkzeuge dafür: pathlib für Datei-Pfade und Verzeichnisse, os für Betriebssystem-Funktionen und Umgebungsvariablen, und subprocess für das Aufrufen externer Programme. Dazu ein paar typische Automatisierungs-Aufgaben aus dem Alltag.
1) Warum pathlib statt os.path?
Früher hat man in Python mit String-Funktionen aus os.path mit Pfaden gearbeitet. Seit Python 3.4 gibt es pathlib – objektorientiert, klar lesbar, plattform-unabhängig. Es ist der moderne Standard.
Path("a") / "b" / "c" ist mehr als nur eleganter – sie nutzt auf jedem System die richtigen Trennzeichen (/ oder \) und ist sicher bei mit-Spaces-versehenen Pfaden.2) Path-Objekte erzeugen
Ein Path repräsentiert einen Pfad – muss aber nicht existieren. Du kannst ihn aus Strings bauen, mit Schrägstrich verlängern und Methoden für Operationen aufrufen:
from pathlib import Path p = Path("/home/anna/projekte") unter = p / "python" / "hello.py" print(unter) # /home/anna/projekte/python/hello.py # Aktuelles Verzeichnis print(Path.cwd()) # /home/anna # Home-Verzeichnis des Benutzers print(Path.home()) # /home/anna # Absolut machen print(Path("datei.txt").resolve())
3) Pfad-Eigenschaften abfragen
Ein Path-Objekt hat viele Attribute und Methoden, mit denen du Bestandteile herauspickst:
"daten.csv""daten"".csv"archiv.tar.gz: [".tar", ".gz"]p = Path("/home/anna/projekte/report.pdf") p.name # 'report.pdf' p.stem # 'report' p.suffix # '.pdf' p.parent # Path('/home/anna/projekte') p.parts # ('/', 'home', 'anna', 'projekte', 'report.pdf') # Neue Endung p.with_suffix(".docx") # Path('.../report.docx') # Neuer Name komplett p.with_name("backup.pdf") # Path('.../backup.pdf')
4) Dateien lesen und schreiben
pathlib hat eingebaute Kurzformen für die häufigsten Datei-Operationen:
from pathlib import Path p = Path("notiz.txt") # Schreiben (überschreibt!) p.write_text("Hallo Welt", encoding="utf-8") # Lesen text = p.read_text(encoding="utf-8") print(text) # Binärdaten Path("bild.png").write_bytes(daten) inhalt = Path("bild.png").read_bytes()
Für große Dateien oder zeilenweises Lesen weiter mit open() und with-Block (wie schon in Python Grundlagen):
with p.open("r", encoding="utf-8") as f: for zeile in f: print(zeile.strip())
5) Verzeichnisse durchlaufen
Mit iterdir() kommst du an alle Einträge eines Verzeichnisses. Mit glob und rglob filterst du nach Mustern:
from pathlib import Path ordner = Path(".") # Direkter Inhalt for eintrag in ordner.iterdir(): print(eintrag.name, "DIR" if eintrag.is_dir() else "FILE") # Nur Python-Dateien (eine Ebene) for py in ordner.glob("*.py"): print(py) # Rekursiv alle Python-Dateien for py in ordner.rglob("*.py"): print(py) # Mit Muster: nur Logs vom Januar 2024 for log in ordner.rglob("2024-01-*.log"): print(log)
rglob ist „rekursives glob" – durchsucht alle Unterverzeichnisse. Praktisch für „finde mir alle Bilder" oder „alle Configs":
# Alle Bilder bilder = [p for p in ordner.rglob("*") if p.suffix.lower() in {".jpg", ".png", ".webp"}] print(len(bilder))
6) Verzeichnisse erzeugen, verschieben, löschen
from pathlib import Path import shutil # Verzeichnis anlegen, inkl. Eltern Path("output/2024/01").mkdir(parents=True, exist_ok=True) # Datei verschieben/umbenennen Path("alt.txt").rename("neu.txt") # Datei löschen Path("temp.txt").unlink() # wirft FileNotFoundError wenn weg Path("temp.txt").unlink(missing_ok=True) # schweigend ignorieren # Leeres Verzeichnis löschen Path("leer").rmdir() # Verzeichnis MIT Inhalt löschen – braucht shutil shutil.rmtree("output/alt") # Datei kopieren shutil.copy("daten.csv", "backup/daten.csv") shutil.copy2("daten.csv", "backup/") # mit Metadaten shutil.copytree("projekt", "backup/projekt") # ganzer Ordner
shutil.rmtree: löscht ohne Nachfrage rekursiv. Ein Tippfehler im Pfad und plötzlich ist mehr weg als gewollt. Lieber mit Path.exists() vorher prüfen oder einen Probelauf machen, der nur die Pfade ausgibt.
7) Das os-Modul
Auch wenn pathlib die meisten Pfad-Aufgaben übernimmt, brauchst du os noch für ein paar Dinge: Umgebungsvariablen, Prozess-Info und Betriebssystem-Erkennung.
os.environ["PATH"]'posix' auf Linux/Mac, 'nt' auf Windows.'/' oder '\\'.'\n' oder '\r\n'.import os # Umgebungsvariable lesen – mit Fallback home = os.environ.get("HOME", "/tmp") api_key = os.environ.get("API_KEY") if not api_key: raise RuntimeError("API_KEY ist nicht gesetzt") # Plattform erkennen import sys if sys.platform == "win32": print("Wir laufen auf Windows") elif sys.platform == "darwin": print("macOS") else: print("Linux oder anderes Unix")
8) Was ist subprocess?
Manchmal willst du ein anderes Programm aufrufen – git, ffmpeg, einen Shell-Befehl, ein anderes Python-Skript. Dafür gibt es das Modul subprocess. Es startet einen neuen Prozess, übergibt Argumente und lässt dich auf das Ergebnis warten.
Die einfachste und empfohlene Funktion ist subprocess.run():
import subprocess ergebnis = subprocess.run( ["git", "status", "--porcelain"], capture_output=True, text=True ) print(ergebnis.returncode) # 0 wenn alles ok print(ergebnis.stdout) # String mit Standardausgabe print(ergebnis.stderr) # String mit Fehlerausgabe
9) subprocess.run – die Parameter
["git", "log", "-1"]. Das Programm wird direkt gestartet, ohne Shell. Sicher!CalledProcessError. Sehr nützlich.TimeoutExpired.None = aktuelle erben.try: r = subprocess.run( ["ping", "-c", "3", "example.de"], capture_output=True, text=True, timeout=10, check=True ) print(r.stdout) except subprocess.CalledProcessError as e: print(f"ping fehlgeschlagen mit Code {e.returncode}: {e.stderr}") except subprocess.TimeoutExpired: print("ping hat zu lange gebraucht") except FileNotFoundError: print("ping ist auf diesem System nicht installiert")
10) Liste vs. Shell-String
Du kannst subprocess.run auch einen String und shell=True übergeben. Mach das so selten wie möglich:
# SICHER: Liste – jeder Wert ist ein separates Argument subprocess.run(["echo", dateiname]) # GEFÄHRLICH: Shell-String – Sonderzeichen werden interpretiert subprocess.run(f"echo {dateiname}", shell=True)
dateiname z. B. "; rm -rf /" ist, führt die Shell das aus! Das ist eine der häufigsten Sicherheits-Lücken. Mit der Liste-Form gibt es das Problem nicht – jedes Element ist ein eigenes Argument, kein Sonderzeichen wird interpretiert. shell=True nur, wenn du wirklich Pipes oder Shell-Glob brauchst, und auch dann mit größter Vorsicht (siehe Secure Coding).
11) Beispiele für Automatisierung
Drei typische Mini-Skripte, die zeigen, wie pathlib, os und subprocess zusammenspielen.
Beispiel 1: Alle PDFs in einem Ordner zählen und Größen aufsummieren:
from pathlib import Path ordner = Path("/home/anna/dokumente") pdfs = list(ordner.rglob("*.pdf")) gesamt = sum(p.stat().st_size for p in pdfs) print(f"{len(pdfs)} PDFs, gesamt {gesamt / 1024 / 1024:.1f} MB")
Beispiel 2: Dateien umbenennen nach Muster:
from pathlib import Path ordner = Path("fotos") for i, foto in enumerate(sorted(ordner.glob("*.jpg")), start=1): neu = ordner / f"urlaub_{i:03d}.jpg" foto.rename(neu) print(f"{foto.name} → {neu.name}")
Beispiel 3: Git-Status für mehrere Projekte abfragen:
import subprocess from pathlib import Path projekte = Path("~/projekte").expanduser() for p in projekte.iterdir(): if not (p / ".git").is_dir(): continue r = subprocess.run( ["git", "status", "--porcelain"], cwd=p, capture_output=True, text=True ) geaendert = len(r.stdout.splitlines()) print(f"{p.name:30} {geaendert} Änderungen")
12) Externe Programme: Standard-Pipeline
Mit subprocess.run und input= kannst du auch Pipelines bauen, ähnlich wie in der Shell mit |:
import subprocess # Output von ls in grep weiterleiten ls = subprocess.run(["ls", "-l"], capture_output=True, text=True) grep = subprocess.run( ["grep", "\\.py$"], input=ls.stdout, capture_output=True, text=True ) print(grep.stdout)
Für komplexere Pipelines lohnt sich oft, die Verarbeitung in Python selbst zu machen statt mehrere Subprozesse zu starten – meist schneller und portabler.
13) Logging in Automatisierungs-Skripten
Bei Skripten, die regelmäßig (z. B. per Cron) laufen, ist Logging Pflicht – sonst weißt du nicht, was wann passiert ist. Das eingebaute logging-Modul reicht:
import logging from pathlib import Path logging.basicConfig( filename=Path("backup.log"), level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s" ) log = logging.getLogger(__name__) log.info("Backup gestartet") try: mache_backup() except Exception: log.exception("Backup fehlgeschlagen") raise log.info("Backup fertig")
Logging löst auch das Problem, dass print in einem Cron-Job ins Leere geht. Mehr dazu in Debugging & Fehleranalyse.
14) Wann Python statt Bash?
Bash und PowerShell sind super für sehr kurze Pipelines. Bei mehr als ein paar Zeilen, bei nötiger Fehlerbehandlung oder bei komplexer Logik wechsel zu Python:
| Eher Bash / Shell | Eher Python |
|---|---|
| Zwei, drei Befehle hintereinander | Mehr als 20 Zeilen |
| Pipe-Heavy mit grep/sed/awk | Strukturierte Daten (JSON, CSV) |
| Reine Shell-Verwaltung | HTTP-/API-Aufrufe |
| Einmaliger Quick-Fix | Wiederkehrender Job |
| Reine Systemverwaltung | Logik mit Fehlerbehandlung |
| Plattform-spezifisch | Cross-Plattform Linux + Windows |
Python-Skripte sind besser testbar (siehe Unit-Tests in Python: pytest), portabler und für Außenstehende meist lesbarer als ein verschachteltes Bash-Skript.
15) Häufige Fehler
- Strings statt pathlib mischen:
"/" + dateiname– funktioniert auf Windows nicht.Path("/") / dateinameist portabel - shell=True bei dynamischen Werten: Sicherheits-Lücke. Immer Liste mit Argumenten
- Timeout vergessen: ein hängender Subprozess blockiert das Skript ewig
- check=True vergessen: ein fehlgeschlagenes externes Programm wird ignoriert, das Skript läuft mit falschen Daten weiter
- os.environ direkt schreiben: gilt für Kind-Prozesse, ändert aber nicht die Shell, die Python startete
- capture_output und stdout/stderr-Parameter gemischt:
capture_output=Trueist Kurzform; manuellstdout=subprocess.PIPE, stderr=subprocess.PIPE - text=False (Standard) bei String-Operationen: stdout ist dann bytes – muss erst dekodiert werden. Mit
text=Truedirekt String - Pfade als rohe Strings überall: jedes Mal neu joinen, neu prüfen – pathlib zentralisiert das
Path nicht eleganter wäre. Jedes Mal, wenn du shell=True schreibst, halt inne und überleg, ob du die Eingabe wirklich kontrollierst. Und jedes Skript, das in Produktion läuft, braucht Logging und Fehlerbehandlung – sonst suchst du Fehler nachts.
Zusammenfassung
pathlib ist der moderne Weg, mit Pfaden zu arbeiten: Path("a") / "b" / "c.txt", dann Methoden wie exists, is_file, read_text, write_text, iterdir, glob, rglob. os liefert Umgebungsvariablen (os.environ.get), Plattform-Info und Prozess-Funktionen. subprocess.run startet externe Programme: erstes Argument ist immer eine Liste, capture_output=True für Ausgabe, text=True für Strings, check=True wirft Exception bei Fehler, timeout= verhindert Hänger. shell=True meiden – Sicherheits-Risiko durch Injection. Logging ist Pflicht in regelmäßig laufenden Skripten. Verzeichnisse erstellen mit mkdir(parents=True, exist_ok=True), kopieren/löschen mit shutil.
