Dateien und I/O (open, with, Text und Bytes, Pfade)
Track Python · PCAP-Fundament · ca. 45 Min.
Worum es geht
Fast jedes Skript liest oder schreibt Dateien: Logs, Konfiguration, Exporte, Uploads. In Node rufst du dafür fs.readFileSync auf und bekommst alles auf einmal. Python gibt dir ein Dateiobjekt (file object), das du öffnest, benutzt und wieder schließt. Dabei entscheidet der Modus (mode), ob eine Datei angelegt, geleert oder erweitert wird, und ob du Text (str) oder rohe Bytes (bytes) bekommst. Die Quelle ordnet Datei-I/O in der PCAP dem Sammelblock “Sonstiges” (Block 5, 9 Fragen, 22 %) zu. Nach der Lektion sagst du die Wirkung der Modi w, a, x sicher voraus, nutzt with automatisch richtig, trennst Text und Bytes sauber und baust Pfade mit os.path und pathlib.
Ablauf: Übersetzung JS/TS nach Python, sechs Schritte mit lauffähigem Code, dann fünf Übungen. Die Beispiele laufen im Browser auf einem virtuellen Dateisystem (virtual file system). Deine echten Dateien sind nie betroffen.
Von JS/TS her gedacht
| Aufgabe | Node (fs, path) |
Python |
|---|---|---|
| Alles lesen | fs.readFileSync(p, "utf8") |
open(p, encoding="utf-8").read() (besser im with) |
| Schreiben, Datei leeren | fs.writeFileSync(p, s) |
open(p, "w", encoding="utf-8") |
| Anhängen | fs.appendFileSync(p, s) |
Modus "a" |
| Nur anlegen, wenn neu | Flag "wx" bei writeFileSync (bitte prüfen) |
Modus "x" |
| Rohdaten | Buffer |
bytes, veränderbar: bytearray |
| Pfad bauen | path.join(a, b) |
os.path.join(a, b) oder Path(a) / b |
| Ausgabe | process.stdout.write(s) |
sys.stdout.write(s) (print schreibt dorthin) |
Der größte Unterschied im Verhalten: In Node ist eine Datei nach readFileSync einfach gelesen. In Python hat das Dateiobjekt einen Lesezeiger (file position), und ein zweites read() startet dort, wo das erste aufgehört hat. Außerdem bleibt ein geöffnetes Dateiobjekt offen, bis du es schließt.
Konzept in kleinen Schritten
Schritt 1: open() mit with schreiben und lesen
open(pfad, modus, encoding=...) liefert ein Dateiobjekt. with schließt es am Ende des Blocks automatisch, auch wenn im Block eine Exception fliegt. Eine Datei ist iterierbar: die for-Schleife liefert Zeile für Zeile. write(s) gibt die Zahl der geschriebenen Zeichen zurück.
Merke: Die Zeilen enthalten das Zeilenende \n. Mit line.rstrip() oder line.rstrip("\n") entfernst du es. writelines schreibt genau die Strings, die du übergibst:
Schritt 2: Die Modi w, a, x, r
| Modus | Datei fehlt | Datei existiert |
|---|---|---|
r (Standard) |
FileNotFoundError |
wird gelesen |
w |
wird angelegt | wird sofort geleert |
a |
wird angelegt | es wird hinten angefügt |
x |
wird angelegt | FileExistsError |
w leert schon beim open, nicht erst beim ersten write. Eine Datei, die du nur mit "w" öffnest und wieder schließt, ist danach leer.
Alle diese Fehler sind Unterklassen von OSError. Das Attribut e.errno liefert die Fehlernummer. Weitere typische Fehler: PermissionError, IsADirectoryError. Mit r+ liest und schreibst du, ohne die Datei zu leeren.
Schritt 3: Lesen, Position, Lesezeiger
read()liest alles,read(n)höchstensnZeichen (im BinärmodusnBytes).readline()liest eine Zeile,readlines()alle Zeilen als Liste.tell()liefert die aktuelle Position,seek(pos)springt dorthin.
Das zweite read() liefert '', weil der Lesezeiger schon am Ende steht. seek(0) setzt ihn zurück.
Schritt 4: with und close()
Ohne with musst du selbst close() aufrufen, und zwar auch dann, wenn dazwischen etwas schiefgeht. with erledigt das für dich. Das Dateiobjekt lebt nach dem Block weiter, ist aber geschlossen.
Schritt 5: Text, Bytes und Encoding
Im Textmodus ("r", "w", Standard t) arbeitest du mit str. Python wandelt dabei zwischen Zeichen und Bytes um, nach dem Encoding. Der Standard ist plattformabhängig, deshalb gibst du immer encoding="utf-8" an. Im Binärmodus ("rb", "wb") bekommst du bytes ohne jede Umwandlung.
Ein Zeichen ist nicht ein Byte: In UTF-8 brauchen ö zwei und € drei Bytes.
Mit dem falschen Encoding gibt es Mojibake (kaputte Zeichen) oder einen Fehler:
bytes ist unveränderbar, bytearray veränderbar. Mit readinto(buf) füllst du einen vorhandenen Puffer (buffer).
Schritt 6: Pfade und Verzeichnisse
os:getcwd(),listdir(p),mkdir(p),makedirs(p),remove(f),rmdir(p),rename(a, b).os.path:join,exists,isfile,isdir,basename,dirname,splitext.pathlib.Path: objektorientiert,Path("a") / "b.txt",.read_text().
Zwei Verhaltensweisen, die man kennen muss:
mkdir legt nur einen Ordner an, dessen Elternordner existieren muss. makedirs legt auch die Zwischenordner an.
Falle: die Prüfungs- und Praxisklassiker
"w"leert die Datei sofort, auch wenn du nichts schreibst. Anhängen heißt"a".writeundwritelinesfügen kein\nein. Zeilen ausreadline()und aus der Iteration enthalten dagegen das\n.- Ohne
encoding="utf-8"hängt das Ergebnis von der Plattform ab. Auf deinem Rechner läuft es, auf dem Server nicht. len(text)ist nicht die Dateigröße. Zeichen (str) und Bytes (bytes) trennst du bewusst.- Zweites
read()liefert''. Der Lesezeiger steht am Ende,seek(0)setzt zurück. - Datei ohne
withöffnen und bei einer Exception nie schließen. os.path.join("/data", "/etc")ergibt/etc, nicht/data/etc.mkdirstattmakedirsbei verschachtelten Ordnern. Außerdem wirftmakedirseinenFileExistsError, wenn der Ordner schon existiert, außer du gibstexist_ok=Truean.
Übungen
Übung 1: Modi vorhersagen
Der Code steht fest, du sagst das Ergebnis voraus. Er läuft in einem leeren Ordner.
with open("k.txt", "w") as f:
f.write("eins\n")
with open("k.txt", "a") as f:
f.write("zwei\n")
with open("k.txt", "w") as f:
f.write("drei\n")
with open("k.txt", "a") as f:
n = f.write("vier!\n")
try:
open("k.txt", "x")
fehler = "keiner"
except OSError as e:
fehler = type(e).__name__Trage das Tupel (inhalt, fehler, n) ein. inhalt ist der gesamte Text von k.txt am Ende.
Gehe die vier open-Zeilen der Reihe nach durch und frage dich bei jeder: Bleibt der alte Inhalt stehen, oder ist er schon weg, bevor geschrieben wird? Beim "x" zählt nur, ob die Datei schon existiert. Und was gibt write zurück, nichts oder eine Zahl?
antwort = ("drei\nvier!\n", "FileExistsError", 6)
antwortÜbung 2: with und Exception (Multiple Choice)
try:
with open("w.txt", "w", encoding="utf-8") as f:
f.write("a\n")
raise ValueError("boom")
f.write("b\n")
except ValueError:
pass
inhalt = open("w.txt", encoding="utf-8").read()Was gilt danach für f.closed und inhalt? Gib den Buchstaben als String zurück.
- A:
f.closedistFalse,inhaltist"", denn das Schreiben wurde abgebrochen. - B:
f.closedistTrue,inhaltist"", dennwithrollt bei Fehlern zurück. - C:
f.closedistTrue,inhaltist"a\n", dennwithschließt immer. - D:
f.closedistTrue,inhaltist"a\nb\n", dennwithschreibt alles zu Ende.
Was macht with beim Verlassen des Blocks, und ändert sich das, wenn der Block durch eine Exception verlassen wird? Gehe die Zeilen im Block einzeln durch: Welche laufen, welche nicht?
antwort = "C"
antwortÜbung 3: Zeilen sauber einlesen
Schreibe lese_eintraege(pfad). Die Datei ist UTF-8-Text. Die Funktion liefert eine Liste der Zeilen mit diesen Regeln:
- Nur das Zeilenende wird entfernt, andere Leerzeichen am Anfang und Ende der Zeile bleiben erhalten.
- Zeilen, die leer sind oder nur aus Leerzeichen bestehen, werden übersprungen.
- Die Datei muss nach dem Aufruf geschlossen sein, und
encoding="utf-8"ist anzugeben.
Die Iteration über die Datei liefert Zeilen mit Zeilenende. Welche Methode entfernt gezielt nur dieses, nicht aber Leerzeichen am Zeilenanfang? Und wie erkennst du eine Zeile, die nur aus Leerzeichen besteht, ohne die Zeile selbst zu verändern? Bedenke: Die letzte Zeile hat vielleicht gar kein Zeilenende, und eine leere Datei liefert eine leere Liste.
def lese_eintraege(pfad):
eintraege = []
with open(pfad, "r", encoding="utf-8") as f:
for zeile in f:
zeile = zeile.rstrip("\n")
if zeile.strip():
eintraege.append(zeile)
return eintraege
lese_eintraegeÜbung 4: Text und Bytes trennen
Schreibe schreibe_und_messe(pfad, text). Die Funktion schreibt text im Textmodus als UTF-8 in die Datei pfad, liest die Datei danach im Binärmodus wieder ein und liefert ein Dict:
"zeichen": Anzahl der Zeichen vontext"bytes": Anzahl der Bytes in der Datei"roh": der Dateiinhalt alsbytes
Zwei Dateizugriffe, jeder mit eigenem Modus: einer schreibt Text mit Encoding, einer liest ohne jede Umwandlung. Bei welchem Modus bekommst du bytes? Und welche der beiden Längen bekommst du aus dem str, welche aus den gelesenen Daten? Prüfe mit einem Wort wie "Gökay", ob deine zwei Zahlen verschieden sein können.
def schreibe_und_messe(pfad, text):
with open(pfad, "w", encoding="utf-8") as f:
f.write(text)
with open(pfad, "rb") as f:
roh = f.read()
return {"zeichen": len(text), "bytes": len(roh), "roh": roh}
schreibe_und_messeÜbung 5: Ordner und Datei anlegen
speichere(basis, teile, text) soll in basis die Unterordner aus der Liste teile anlegen (verschachtelt, in dieser Reihenfolge), darin die Datei notiz.txt mit text schreiben und den Pfad der Datei zurückgeben. Die Funktion muss wiederholt aufrufbar sein: Beim zweiten Aufruf mit anderem Text steht in der Datei nur der neue Text. Ist teile leer, liegt die Datei direkt in basis. Fülle die zwei Lücken.
Zwei Fragen an die Ordner-Zeile: Sie muss auch mehrere Ebenen auf einmal anlegen können, und sie darf beim zweiten Aufruf nicht scheitern, obwohl der Ordner schon da ist. Schau mit help(os.makedirs) in die Doku, wie du das Zweite einstellst. Bei der Datei-Zeile zählt, was mit altem Inhalt passieren soll.
import os
def speichere(basis, teile, text):
ordner = os.path.join(basis, *teile)
os.makedirs(ordner, exist_ok=True)
pfad = os.path.join(ordner, "notiz.txt")
with open(pfad, "w", encoding="utf-8") as f:
f.write(text)
return pfad
speichereMerksatz und Prüfstein
Merksatz: with open(pfad, modus, encoding="utf-8") schließt die Datei immer, "w" leert sofort, "a" hängt an, "x" legt nur Neues an, und Zeichen (str) sind nicht Bytes (bytes).
Prüfstein (offene Frage): Eine Datei mit dem Text "Gökay" ist laut Dateimanager 6 Bytes groß, len("Gökay") ist aber 5. Erkläre, warum das kein Widerspruch ist, was im Textmodus beim Lesen passiert und was du sehen würdest, wenn du die Datei mit encoding="latin-1" liest.
Quelle: quellen/python-glossar-pcap-pcpp1.md, Abschnitt “Dateien und I/O” mit “Modi von open()”, “Begriffe” und “Verzeichnisse und Pfade”. Die Zuordnung von Datei-I/O zu Block 5 (9 Fragen, 22 %) steht im Abschnitt “Prüfungsblöcke (PCAP-31-03)”. exist_ok steht in der Quelle nur im PCPP1-Teil (Tabelle os). Nicht in der Quelle, aber mit Python 3.13 und Pyodide 0.27 ausgeführt und geprüft: die Path-Attribute .name, .stem, .suffix, .parent, write_text, os.path.join mit absolutem zweitem Teil, der Node-Vergleich zu appendFileSync und dem Flag wx sowie die Aussage zu UTF-8-Bytes pro Zeichen (bitte prüfen, ob sie in der PCAP geprüft werden).