Python unter Linux: Dateien
Dateien dienen zum dauerhaften Speichern von Informationen. Sie können erzeugt und gelöscht werden, ihnen kann man Daten anhängen oder vorhandene Daten überschreiben. Selbstverständlich sind die Daten lesbar, sie können auch ausführbar sein. Es können sowohl Textdateien wie auch zeichen- oder blockorientierte Dateien bearbeitet werden.
Öffnen und Schließen von Dateien
[Bearbeiten]Um eine Datei zu erzeugen, reicht es, sie zum Schreiben zu öffnen:
#!/usr/bin/python
datei = open("hallo.dat", "w")
datei.close()
user@localhost:~$ ./datei1.py ; ls *.dat
hallo.dat
In diesem Beispiel wird eine leere Datei mit dem Namen hallo.dat erzeugt. Gibt es eine solche Datei, wird sie überschrieben, dafür sorgt das Dateiattribut "w"
(write). Eine Datei wird zum Lesen geöffnet, wenn man das Attribut "r"
(read) benutzt. Das Attribut "a"
(append) hingegen wird eine neue Datei erzeugen, falls sie noch nicht existiert, sonst hingegen öffnen und Daten an das Ende anhängen.
Lesen und Schreiben
[Bearbeiten]Über die Zeilen einer Textdatei lässt sich leicht iterieren, wie folgendes Beispiel zeigt. Es werden nützliche Informationen aus der Datei /etc/passwd angezeigt:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
datei = open("/etc/passwd", "r")
for zeile in datei:
liste = zeile[:-1].split(":")
print "Benutzer '%s' benutzt die Shell %s" % (liste[0], liste[6])
datei.close()
user@localhost:~$ ./pw.py
Benutzer 'root' benutzt die Shell /bin/bash
Benutzer 'daemon' benutzt die Shell /bin/sh
Benutzer 'bin' benutzt die Shell /bin/sh
...
Es wird bei dem Beispiel über jede Zeile in der Datei iteriert. Die Zeilen werden um das Zeilenende (Newline-Zeichen) verkürzt (zeile[:-1]) und dann nach Doppelpunkten aufgeteilt (split(":")).
Dateien können auch mit den Methoden write() und read() beschrieben und gelesen werden:
#!/usr/bin/python
# -*- coding: utf-8 -*-
datei = open("hallo.dat", "w")
datei.write("Hallo Welt\n\n")
datei.write("Foo Bar\n")
datei.close()
datei = open("hallo.dat", "r")
x = datei.read()
datei.close()
print x
user@localhost:~$ ./datei2.py
Hallo Welt
Foo Bar
Die Methode write() schreibt die Daten nacheinander in die Datei, Zeilenumbrüche müssen explizit angegeben werden. read() hingegen liest die gesamte Datei ein und erzeugt daraus einen einzelnen String.
Um ein anderes Objekt als einen String zu speichern und zu lesen, muss man sich schon sehr anstrengen. Das folgende Beispiel schreibt eine Liste von Tupeln, die aus einem Namen und einer Zahl bestehen. Anschließend wird diese Datei eingelesen:
#!/usr/bin/python
studenten = [('Peter', 123), ('Paula', 988), ('Freddi', 851)]
datei = open("studenten.dat", "w")
for student in studenten:
s = str(student) + '\n'
datei.write(s)
datei.close()
datei = open("studenten.dat", "r")
for z in datei:
name, matnum = z.strip('()\n ').split(',')
name = name.strip("\' ")
matnum = matnum.strip()
print "Name: %s Matrikelnummer: %s" % (name, matnum)
datei.close()
user@localhost:~$ ./datei4.py
Name: Peter Matrikelnummer: 123
Name: Paula Matrikelnummer: 988
Name: Freddi Matrikelnummer: 851
Tupel werden genau so geschrieben, wie sie da stehen, denn sie werden auch so in einen String umgewandelt. Je Tupel wird eine Zeile geschrieben. Liest man nun eine solche Zeile wieder ein, erhält man als String den Wert ('Peter', 123). Dieser wird mit der Methode strip() verändert, und zwar werden alle Klammern am Anfang und Ende des Strings wie auch Leerzeichen und Zeilenumbrüche entfernt. Man erhält also den String Peter', 123. Dieser String wird mit der Methode split() in zwei Teile geteilt, wobei das Komma als Trennzeichen dient. Die so entstandene Liste wird je Teil um führende Anführung- und Leerzeichen bereinigt. strip() ohne Argumente entfernt sogenannte Whitespaces, also unter anderem Leer- und Tabulatorzeichen und Zeilenumbrüche. Erst jetzt haben wir zwei Strings, die den ursprünglichen Elementen der Tupel ähnlich sehen. Leichter wird es mit Pickle gehen.
Dateien mit Pickle
[Bearbeiten]Den Funktionen, die Pickle anbietet, kann man wirklich nahezu jedes Objekt anbieten, Pickle kann es speichern. Ob es sich um Zahlen, Listen, Tupel oder ganze Klassen handelt ist Pickle dabei gleich. Der Aufruf ist recht einfach, es gibt eine Funktion zum pickeln und eine zum entpickeln:
#!/usr/bin/python
import pickle
studenten = [('Peter', 123), ('Paula', 988), ('Freddi', 851)]
datei = open("studenten.dat", "w")
pickle.dump(studenten, datei)
datei.close()
datei = open("studenten.dat", "r")
meine_studenten = pickle.load(datei)
datei.close()
print meine_studenten
user@localhost:~$ ./pickle1.py
[('Peter', 123), ('Paula', 988), ('Freddi', 851)]
Der Funktion dump() übergibt man die Daten sowie einen Dateihandle. Pickle kümmert sich dann um das Schreiben. Das Laden passiert mit load(). Diese Funktion nimmt lediglich das Dateiobjekt auf.
Dateien mit json
[Bearbeiten]JSON ist das bessere Pickle, oder zumindest meist die bessere Wahl. JSON ist das Format, wenn es um den Datenaustausch innerhalb von Webanwendungen geht. Aber es lässt sich genau so gut abseits des Webs nutzen. Pickle ist pythonspezifisch, JSON dagegen versteht sich mit jeder nennenswerten Programmiersprache. Hier das Pickle-Skript umgeschrieben für JSON:
#!/usr/bin/python
import json
studenten = [('Peter', 123), ('Paula', 988), ('Freddi', 851)]
datei = open("studenten.json", "w")
json.dump(studenten, datei, indent=4)
datei.close()
datei = open("studenten.json", "r")
meine_studenten = json.load(datei)
datei.close()
print meine_studenten
user@localhost:~$ ./json1.py
[[u'Peter', 123], [u'Paula', 988], [u'Freddi', 851]]
Da JSON keine Tupel kennt, werden die Name-Nummer-Tupel zu Listen. Und Pythons JSON-Strings sind immer Unicode. Neben Listen, Zeichenketten und Zahlen können noch Dictionarys, True, False und None gespeichert werden.
Ein weiterer Vorteil von JSON offenbart sich beim Blick in die Datei studenten.json:
[
[
"Peter",
123
],
[
"Paula",
988
],
[
"Freddi",
851
]
]
Das ist so ziemlich das Optimum an Lesbarkeit. dump()s optionales Argument indent bestimmt dabei die Einrücktiefe. Ohne diese Angabe wird das ein, immer noch gut lesbarer Einzeiler.
Zeichenorientierte Dateien
[Bearbeiten]Zeichenorientierte Dateien sind ein Strom aus einzelnen Bytes, die sich nicht notwendigerweise als Text darstellen lassen. Um einzelne Zeichen aus einer Datei zu lesen, übergeben wir der Methode read() die Anzahl der auf einmal zu lesenden Zeichen.
Als ein Beispiel zeichenorientierter Dateien benutzen wir ein Programm, welches uns Lottozahlen zufällig vorhersagt. Hierzu lesen wir unter Linux und anderen unixähnlichen Betriebssystemen die Datei /dev/random aus, welche recht gute Zufallszahlen erzeugt. Diese werden in einem set gespeichert. Wir benutzen hier ein set, damit wir eindeutige verschiedene Werte aufnehmen können. Sobald wir sechs verschiedene Zahlen im Bereich 1 bis 49 haben, werden diese in eine Liste konvertiert, sortiert und angezeigt:
#!/usr/bin/python
datei = open("/dev/random", "r")
lottozahlenSet = set()
while len(lottozahlenSet) < 6:
zahl = ord(datei.read(1))
if 0 < zahl < 50:
lottozahlenSet.add(zahl)
datei.close()
lottozahlen = list(lottozahlenSet)
lottozahlen.sort()
print lottozahlen
user@localhost:~$ ./lottozahlen.py
[5, 6, 18, 19, 23, 41]
Es wird aus der zeichenorientierten Datei /dev/random jeweils ein einzelnes Zeichen gelesen, in eine Zahl verwandelt und in das lottozahlenSet eingefügt. Die sortierte Liste wird ausgegeben.
Blockorientierte Dateien
[Bearbeiten]Manche Dateien sind so aufgebaut, dass sie mehrfach den gleichen Datentyp speichern, wie zum Beispiel Folgen von Fließkommazahlen, Soundinformationen einer Wave-Datei, Login-Informationen wie in /var/log/wtmp oder auch feste Dateiblöcke wie in /dev/sda. Solche Dateien nennt man blockorientiert.
Numerische Blöcke
[Bearbeiten]Dateien, die einfache Folgen numerischer Werte aufnehmen können, werden bequem mit dem Modul Array bearbeitet. Das folgende Beispiel legt eine solche Datei an und speichert zwei Fließkommazahlen in ihr. Diese Werte werden anschließend wieder gelesen und ausgegeben. Bitte beachten Sie, dass der Inhalt der in diesem Beispiel angelegten Datei keine Textdarstellung der gespeicherten Zahlen ist.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import array
# Array anlegen
fa = array.array("f")
fa.append(1.0)
fa.append(2.0)
# In datei schreiben
datei = open("block1.dat", "w")
fa.tofile(datei)
datei.close()
# Aus Datei lesen
datei = open("block1.dat", "r")
ra = array.array("f")
ra.fromfile(datei, 2)
datei.close()
# Ausgeben
for zahl in ra:
print zahl
user@localhost:~$ ./block1.py
1.0
2.0
Ein Array wird mit array(code) angelegt. Eine kleine Auswahl an Codes stellen wir ihnen in dieser Tabelle vor:
Typ | Code |
---|---|
Zeichen | c und u (Unicode) |
int | i |
long | l |
float | f |
Es können nur Werte mit dem angegebenen Typ, in unserem Beispiel Fließkommazahlen, gespeichert werden. Die Methode append() fügt dem Array eine Zahl hinzu, mit tofile(dateihandle) wird das gesamte Array geschrieben.
Liest man ein solches Array wieder ein, benutzt man die Methode fromfile(dateihandle, anzahl). Hier muss man neben der Datei noch die Anzahl der Werte wissen, die man auslesen möchte.
Zur Ausgabe können wir einfach in bekannter Weise über das Array iterieren.
Strukturierte Blöcke
[Bearbeiten]Während man es bei Log-Dateien meistens mit Textdateien zu tun hat, gibt es hin und wieder Systemdateien, die einen blockorientierten Aufbau haben. Beispielsweise ist die Datei /var/log/lastlog eine Folge von Blöcken mit fester Länge[1]. Jeder Block repräsentiert den Zeitpunkt des letzten Logins, das zugehörige Terminal und den Hostnamen des Rechners, von dem aus man sich angemeldet hat. Der Benutzer mit der User-ID 1000 hat den tausendsten Block. Der Benutzer root mit der User-ID 0 den nullten Block.
Hat man einmal herausgefunden, wie die Datei aufgebaut ist, und welche Datentypen hinter den einzelnen Einträgen stecken, kann man sich den Blocktyp mit Pythons Modul struct nachbauen. Hierfür ist der Formatstring gedacht, der im folgenden Beispiel in kompakter Form den Datentyp eines lastlog-Eintrags nachahmt. Wir bestimmen den letzten Anmeldezeitpunkt des Benutzers mit der User-ID 1000 und ahmen so einen Teil des Unix-Befehles lastlog nach:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import struct
format = "i32s256s"
userID = 1000
blockgroesse = struct.calcsize(format)
datei = open("/var/log/lastlog", "r")
datei.seek(userID * blockgroesse)
eintrag = datei.read(blockgroesse)
b = struct.unpack_from(format, eintrag)
datei.close()
print "Zeitstempel:", b[0]
print "Terminal:", str(b[1])
print "Hostname:", str(b[2])
user@localhost:~$ ./lastlog1.py
Zeitstempel: 1241421726
Terminal: :0
Hostname:
Der Formatstring ist i32s256s. Hinter dieser kryptischen Abkürzung verbirgt sich ein Datentyp, der einen Integer enthält (i), dann eine Zeichenkette (s) von 32 Byte Länge gefolgt von einer weiteren Zeichenkette, die 256 Zeichen lang ist.
Wir bestimmen die Blockgröße mit der Methode calcsize(Formatstring). Diese Blockgröße wird in Bytes gemessen. Anschließend wird die Datei /var/log/lastlog geöffnet und mit seek(Position) bis zu einer bestimmten Stelle in der Datei vorgespult.
Ein Eintrag der Datei wird gelesen und mit Hilfe der Methode unpack_from(Formatstring, String) in ein 3-Tupel konvertiert. String-Daten in diesem Tupel müssen explizit konvertiert werden, da man ansonsten angehängte NULL-Bytes mitführt.
Der Zeitstempel als Unix-Zeit wird zusammen mit den anderen Angaben ausgegeben.
Binärdateien
[Bearbeiten]Unter Binärdateien verstehen wir Dateien, die nicht ausschließlich aus Text bestehen, sondern insbesondere auch nicht darstellbare Zeichen enthalten. Solche Dateien haben wir in den vorgenannten Abschnitten zu blockorientierten Dateien schon kennen gelernt. Technisch macht Linux keinen Unterschied zwischen Text- und Binärdateien.
Mit den uns bekannten Funktionen können wir leicht Informationen aus solchen Dateien extrahieren. Das folgende Beispiel zeigt, wie wir die Anzahl der Kanäle und die Kennung einer Wave-Datei[2] ermitteln.
#!/usr/bin/python
# -*- coding: utf-8 -*-
datei = open("test.wav", "rb")
datei.seek(8)
text = str(datei.read(4))
print "Kennung:", text
datei.seek(22)
# nur das höherwertige Byte auslesen
kanaele = ord(datei.read(1))
datei.close()
print "Die WAV-Datei hat", kanaele, "Kanäle"
user@localhost:~$ ./binary1.py
Kennung: WAVE
Die WAV-Datei hat 1 Kanäle
Die Datei wird im Modus "rb" geöffnet. "b" steht dabei für "binary". Da Linux (anders als zum Beispiel Windows) keinen Unterschied zwischen Text- und Binärdateien macht, wäre das "b" nicht nötig. Aber Plattformunabhängigkeit ist eine von Pythons hervorstechenden Merkmalen, die Sie auch bei der Programmierung anstreben sollten. So läuft das Skript fehlerlos auf jedem Betriebssystem.
Die gesuchten Informationen, Kennung und Kanäle, stehen an bestimmten Positionen in der Datei, die wir mit seek() anspringen. Anschließend werden die Kennung wie auch die Anzahl der Kanäle gelesen und ausgegeben.
Bitte beachten Sie, dass diese Art auf Medienformate zuzugreifen nur das Konzept verdeutlichen soll. Zu den meisten Medienformaten gibt es eigene Module, die den Aufbau eines Formates kennen und somit einen gut dokumentierten Zugriff auf die Inhalte bieten. Für den hier dargestellten Zugriff auf das Medienformat WAVE bietet Python das Modul wave an.
Verzeichnisse
[Bearbeiten]Wenn man vom Umgang mit Dateien redet, gehören Verzeichnisse unweigerlich dazu. Die beiden häufigsten Funktionen in diesem Zusammenhang sind wohl, zu überprüfen ob ein Verzeichnis existiert und es gegebenenfalls anzulegen. Ein Beispiel:
#!/usr/bin/python
# -*- coding: utf-8 -*-
import os
datei = "test.txt"
verzeichnis = "test"
pfad = verzeichnis + os.sep + datei
if not os.path.exists(verzeichnis):
os.mkdir(verzeichnis)
offen = open(pfad, "w")
offen.write("hallo")
offen.close()
user@localhost:~$ ./folders1.py; ls test
test.txt
Das Programm erzeugt einen Pfad, der aus einem Verzeichnis, einem Trennzeichen und einem Dateinamen besteht. Das Trennzeichen erfährt man mit os.sep, unter unixähnlichen Betriebssystemen ist dies meistens der Schrägstrich (/). Die Funktion os.path.exists() prüft, ob eine Datei oder ein Verzeichnis vorhanden ist. Wenn nicht, dann wird mit os.mkdir() ein neues erzeugt und anschließend eine Datei hineingeschrieben.
Eine andere Anwendung ist herauszufinden, in welchem Verzeichnis wir aktuell sind und dann alle enthaltenen Verzeichnisse und Dateien ab dem übergeordneten Verzeichnis aufzulisten. Dieses Programm implementiert den Befehl ls -R ..:
#!/usr/bin/python
import os
print "Wir sind hier->", os.getcwd()
os.chdir("..")
for r, d, f in os.walk(os.getcwd()):
print r, d, f
user@localhost:~$ ./folders2.py
Wir sind hier-> /home/tandar/x/y
/home/tandar/x ['y'] ['datei1.txt']
/home/tandar/x/y [] ['datei2.txt']
Mit der Funktion getcwd() finden wir das aktuelle Verzeichnis heraus. Anschließend können wir mit chdir(Pfad) in das übergeordnete oder ein anderes Verzeichnis wechseln. Für jenes Verzeichnis rufen wir walk(Pfad) auf, und erhalten eine Menge von 3-Tupeln, die aus einem Basispfad, einer Liste mit enthaltenen Verzeichnissen und einer Liste mit enthaltenen Dateien besteht. Diese Angaben geben wir im Programm schlicht aus. walk() berücksichtigt übrigens keine symbolischen Links auf Verzeichnisse. Sind solche zu erwarten, kann walk(top=Pfad, followlinks=True) benutzt werden.
Zusammenfassung
[Bearbeiten]In diesem Kapitel haben Sie einen Überblick über die Verwendung von Dateien und Ordnern mit Python bekommen. Textdateien lassen sich unkompliziert lesen und schreiben, bei anderen Datentypen als Text bietet sich Pickle an. Block- und zeichenorientierte Dateien lassen sich ebenfalls bequem lesen und beschreiben, wobei im Fall der blockorientierten Dateien der Aufbau bekannt sein muss. Die meisten dateibasierten Operationen lassen sich mit Funktionen aus dem Modul os ansprechen, dessen Inhalt wir im Kapitel Überblick über vorhandene Module näher beleuchten.