Lesen und Schreiben von Daten-Dateien

@BOOKON

\begin{wrapfigure}[10]{r}{0.5\textwidth} \vspace{-1.1\baselineskip} \begin{center} \includegraphics[width=0.40\textwidth]{images/scrabble.png} \end{center} \caption{Read und Write als Scrabble} \vspace{-1.1\baselineskip} \end{wrapfigure}

Python bietet viele Möglichkeiten Daten aus Dateien zu lesen und in Dateien zu schreiben. Wie man dies ohne zusätzliche Module mit reinem Python machen kann haben wir im Kapitel \ref{chapter:dateien} (\nameref{chapter:dateien}) gesehen.

NumPy bietet jedoch optimierte Methoden für die speziellen Numpy-Datenstrukturen, die es einem ermöglichen mit einem Befehl komplette Arrays einzulesen oder rauszuschreiben. In unserem Kapitel \ref{datentyp-objekt-dtype} (\nameref{datentyp-objekt-dtype}) haben wir bereits die Numpy-Funktionen genfromtxt, loadtxt und savetxt kennengelernt. In diesem Kapitel wollen wir diese und andere Funktionen von Numpy näher betrachten.

@BOOKOFF

@WEBON Scrabble with the Text Numpy, read, write, array

Es gibt in Numpy viele Wege um Daten aus Dateien zu lesen und ebenso zu schreiben. In diesem Kapitel werden die verschiedenen Möglichkeiten diskutieren und die dazugehörigen Funktionen betrachten. @WEBOFF


Text-Dateien speichern mit savetxt

Die ersten zwei Funktionen, die wir uns anschauen möchten, sind savetxt und loadtxt.

Im folgenden einfachen Beispiel definieren wir ein Array x und speichern es als Text-Datei mit savetxt:

In [2]:
import numpy as np

x = np.array([[1, 2], 
              [3, 4],
              [5, 6]], np.int32)

np.savetxt("test.txt", x)

Die Datei "test.txt" ist eine Text-Datei und der Inhalt sieht wie folgt aus, wenn wir ihn uns in einem Linux-Terminal anschauen\footnote{Unter Windows kann man das Kommanda 'type test.txt' benutzen}

$ more test.txt
1.000000000000000000e+00 2.000000000000000000e+00
3.000000000000000000e+00 4.000000000000000000e+00
5.000000000000000000e+00 6.000000000000000000e+00

Da das Array aus Integern besteht, hätte man hier eher eine Datei erwartet in der ganze Zahlen und nicht Float-Zahlen stehen. Man kann aber das Ausgabeformat selbst bestimmen. Im Folgenden speichern wir das Array in der Datei test2.txt mit drei Nachkommastellen und in der Datei test3.txt als Integers mit vorangestellten Leerzeichen, wenn die Anzahl der Stellen kleiner als 4 ist. Dafür übergeben wir einen Format-String an den Parameter fmt. Im vorigen Beispiel haben wir gesehen, dass der Default-Delimiter ein Leerzeichen ist. Wir können das Verhalten anpassen, indem wir einen String dem Parameter "delimiter" mitgeben. In den meisten Fällen wird dies ein einzelnes Zeichen sein. Jedoch kann ebenso eine ganze Zeichen-Sequenz übergeben werden, z.B. ein Smiley " :-)":

In [3]:
np.savetxt("test2.txt", x, fmt="%2.3f", delimiter=",")
np.savetxt("test3.txt", x, fmt="%04d", delimiter=" :-) ")

Die neu erstellten Dateien sehen folgendermaßen aus:

$ more test2.txt
1.000,2.000
3.000,4.000
5.000,6.000
$ more test3.txt
0001 :-) 0002
0003 :-) 0004
0005 :-) 0006

Die komplette Syntax von "savetxt" sieht folgendermaßen aus:

savetxt(fname, 
        X, 
        fmt='\%.18e', 
        delimiter=' ', 
        newline='\n', 
        header='', 
        footer='', 
        comments='# ')
Parameter Bedeutung
X Array-Ähnliche Daten, die in einer Text-Datei gespeichert werden sollen.
fmt String oder Sequenz von Strings, optional.
Ein einzelner Format-String (\%10.5f),
eine Sequenz aus String-Formaten oder ein Multi-Format-String,
z.B. 'Iteration \%d -- \%10.5f', wobei hier der "delimiter" ignoriert wird.
Für komplexe "X", sind folgende Optionen für "fmt" erlaubt:
a) Ein einzelnes Spezifikationssysmbol, "fmt='\%.4e'",
liefert eine Zahlen-Formatierung, wie "' (\%s+\%sj)' \% (fmt, fmt)".
b) Ein vollständiger Spezifikations-String,
der alle reellen und vorstellbaren Fälle umfasst.
z.B. "' \%.4e \%+.4j \%.4e \%+.4j \%.4e \%+.4j'", für 3 Spalten.
c) Eine Liste mit Spezifikationen, eine pro Spalte - in diesem Fall müssen die
reellen und vorstellbaren Teile getrennte Spezfikatoren haben,
d.h. "['\%.3e + \%.3ej', '(\%.15e\%+.15ej)']" für 2 Spalten.
delimiter Ein String, der für die Separierung der Spalten genutzt wird.
newline Ein String (z.B. "\n", "\r\n" or ",\n"), der eine Zeile abschliesst statt der Standard-Zeilen-Endung.
header Ein String, der an den Beginn der Datei geschrieben wird.
footer Ein String, der an das Ende der Datei geschrieben wird.
comments Ein String, der for "header" und "footer" gestellt wird, um diese als Kommentar zu markieren. Als Default wird hier das Hash-Tag "#" benutzt.


Text-Dateien laden mit loadtxt

loadtxt ohne Parameter

Jetzt werden wir die Datei "test.txt" einlesen, die wir im vorigen Unter-Kapitel erstellt haben:

In [4]:
y = np.loadtxt("test.txt")
print(y)
[[ 1.  2.]
 [ 3.  4.]
 [ 5.  6.]]

Spezielle Trenner

In [5]:
y = np.loadtxt("test2.txt", delimiter=",")
print(y)
[[ 1.  2.]
 [ 3.  4.]
 [ 5.  6.]]

Auch nichts Neues erhalten wir, wenn wir die Datei einlesen, in dem wir als Separator ein Smiley verwendet haben:

In [6]:
y = np.loadtxt("test3.txt", delimiter=" :-) ")
print(y)
[[ 1.  2.]
 [ 3.  4.]
 [ 5.  6.]]

Selektives Einlesen von Spalten

Häufig ist es so, dass man aus einer solchen Datei nur bestimmte Spalten einlesen will. Zu diesem Zweck übergibt man dem Parameter usecols ein Tupel mit den gewünschten Spaltenindizes. Dabei beginnt die Nummerierung wie üblich mit dem Index 0. Um die Wirkungsweise des Parameters usecols besser demonstrieren zu könnten, erzeugen und speichern wir zuerst ein Array mit 6 Spalten:

In [18]:
Z = np.random.randint(-10, 10, size=(4,10))
print(Z)
np.savetxt("test3.txt", Z, fmt="%1d", delimiter=" ")
[[  3  -6   7   6   8   1   4  -7 -10  -8]
 [ -5  -6  -7   9   3  -4   4   4   6   3]
 [ -9   4   0  -6  -4   9  -4  -5  -1  -5]
 [  6 -10  -6   8   9  -6   6   4   8   6]]
In [8]:
y = np.loadtxt("test3.txt", 
               delimiter=" :-) ", 
               usecols=(0, 1))
print(y)
[[ 1.  2.]
 [ 3.  4.]
 [ 5.  6.]]
In [ ]:
 

Datenkonvertierung beim Einlesen

@BOOKON Häufig liegen Daten in der einzulesenden Datei in einem Format vor, dass wir wandeln müssen. So könnte beispielsweise eine Datei Werte in Fahrenheit-Temperaturen enthalten, die wir aber für unsere Berechnungen in Celsius benötigen. Liest man diese Daten ein, so wie wir es eben getan haben, so müsste man Sie in einem Folgeschritt in Celsius wandeln.

Wir zeigen dies an der Datei temperatures.txt:

Chicago New York Boston Dallas
74.3 69.7 76.1 77.5
80.2 73.8 82.4 84.2
86.3 79.4 89.2 90.2

Die erste Zeile enthält die Beschreibung der Spalten. Wir müssen diese beim Einlesen überspringen. Dazu stellen wir den Parameter skiprows auf 1. @BOOKOFF

In [ ]:
 

@BOOKON Das folgende Beispiel empfehlen wir Ihnen selbst zu versuchen. @BOOKOFF

@WEBON In unserem nächsten Beispiel lesen wir die Datei "times_and_temperatures.txt" aus dem Kapitel Generatoren des Python-Tutorials. Jede Zeile enthält eine Zeitangabe im Format "hh::mm::ss" und eine zufällige Temperatir zwischen 10.0 und 25.0 C°. Wir müssen den Zeit-String in einen Float-Wert konvertieren. Die Zeit wird in Minuten und Sekunden angegeben. Wir definieren zuerst eine Funktion, die "hh::mm::ss" in Minuten wandelt: @WEBOFF

In [ ]:
def time2float_minutes(time):
    if type(time) == bytes:
        time = time.decode()
    t = time.split(":")
    minutes = float(t[0])*60 + float(t[1]) + float(t[2]) * 0.05 / 3
    return minutes

for t in ["06:00:10", "06:27:45", "12:59:59"]:
    print(time2float_minutes(t))

Sie werden festgestellt haben, dass wir den Typ der Zeit gegen Binär geprüft haben. Der Grund liegt in der Benutzung unserer Funktion "time2float_minutes" in loadtxt im nächsten Beispiel. Die Schlüsselwort-Parameter converters beinhaltet ein Dictionary, welches eine Funktion für jede Spalte hält (der Schlüssel der Spalte entspricht dem Schlüssel des Dictionaries) um die String-Daten der Spalte in Float-Werte zu konvertieren. Die String-Daten sind ein Byte-String. Deshalb müssen wir diesen in unserer Funkion in einen Unicode-String transferieren:

In [ ]:
y = np.loadtxt("times_and_temperatures.txt", 
               converters={ 0: time2float_minutes})
print(y)
In [ ]:
# delimiter = ";" , # d.h. benutze ";" als als Delimiter statt Leerzeichen


tofile

tofile ist eine Funktion um den Inhalt eines Arrays sowohl im Binär-Format, als auch im Text-Format, in eine Datei zu schreiben.

A.tofile(fid, sep="", format="%s")

Die Daten aus dem ndarray A sind nun in "C"-Reihenfolge geschrieben, ohne Rücksicht der Reihenfolge aus A.

Die Datei, die mit dieser Methode geschrieben wurde, kann mit der Funktion fromfile() wieder geladen werden.

Parameter Bedeutung
fid Kann entweder ein offenes Datei-Objekt sein, oder ein String mit einem Dateinamen.
sep Der String "sep" definiert den Separator, der für die Text-Ausgabe zwischen den Elementen verwendet wird. Wenn der String leer gelassen wird (''), wird eine Binär-Datei geschrieben, genau wie file.write(a.tostring()).
format Format-String für die Text-Ausgabe. Jeder Eintrag im Array wird so formatiert, indem dieser in den nächsten Python-Typ konvertiert wird und dann "format" angewendet wird.

Anmerkung:

Informationen zur Byte-Reihenfolge und Präzision gehen verloren. Deshalb ist keine gute Idee die Funktion zu benutzen um Daten zu archivieren oder zwischen Maschinen mit verschiedener Byte-Reihenfolge zu transportieren. Einige dieser Probleme können überwunden werden bei der Ausgabe der Daten als Text-Datei es auf die Kosten der Geschwindigkeit und Dateigröße geht.

In [ ]:
dt = np.dtype([('time', [('min', int), ('sec', int)]),
               ('temp', float)])
x = np.zeros((1,), dtype=dt)
x['time']['min'] = 10
x['temp'] = 98.25
print(x)

fh = open("test6.txt", "bw")
x.tofile(fh)


fromfile

fromfile liest Daten, die mit tofile geschrieben wurden. Es ist möglich Binär-Daten zu lesen, wenn der Typ der Daten bekannt ist. Es ist ebenfalls möglich einfach formatierte Txt-Dateien zu parsen. Die Daten aus der Datei werden als Array zurückgegeben.

Die allgemeine Syntax sieht wie folgt aus:

numpy.fromfile(file, dtype=float, count=-1, sep='')

Parameter Bedeutung
file 'file' kann entweder ein offenes Datei-Objekt sein oder ein String mit einem Dateinamen der gelesen werden soll.
dtype definiert den Daten-Typ des Arrays, der aus der Daten-Datei konstruiert wird. Bei Binär-Dateien dient es zur Festlegung der Größe und Bety-Reihenfolge der Elemente der Datei.
count definiert die Anzahl der Elemente, die gelesen werden sollen. -1 bedeutet, das alle gelesen werden.
sep Der String "sep" gibt den Separator an der verwendet wird, wenn die Datei eine Text-Datei ist. Wenn der String leer ist (''), so wird die Datei wie eine Binär-Datei behandelt. Ein Leerzeichen (' ') in einem Separator steht für 0 oder mehrere Leerzeichen. Ein Separator der nur Leerzeichen enthält muss mindestens einem Leerzeichen entsprechen.
In [ ]:
fh = open("test4.txt", "rb")

np.fromfile(fh, dtype=dt)
In [ ]:
import numpy as np
import os

# platform dependent: difference between Linux and Windows
# data = np.arange(50, dtype=np.int)

data = np.arange(50, dtype=np.int32)
data.tofile("test4.txt")

fh = open("test4.txt", "rb")
# 4 * 32 = 128
fh.seek(128, os.SEEK_SET)

x = np.fromfile(fh, dtype=np.int32)
print(x)

Vorsicht:

Es können Probleme auftreten wenn tofile und fromfile für Daten-Speicherung (Storage) benutzt werden, denn Binär-Dateien sind nicht Plattform-Unabhängig. Über tofile werden keine Informationen zur Byte-Reihenfolge oder Daten-Typen abgespeichert. Daten können im .npy-Format Plattform-Unabhängig gespeichert werden, wenn stattdessen save und load verwendet werden.



Best Practice um Daten zu laden und zu speichern

Der empfohlene Weg um Daten mit Numpy in Python zu speichern und zu laden besteht aus load und save. Im folgenden Beispiel benutzen wir eine temporäre Datei:

In [ ]:
import numpy as np

print(x)

from tempfile import TemporaryFile

outfile = TemporaryFile()

x = np.arange(10)
np.save(outfile, x)

outfile.seek(0) # Only needed here to simulate closing & reopening file
np.load(outfile)



Und noch ein anderer Weg: genfromtxt

Es gibt noch einen weiteren Weg um tabellarische Daten aus einer Datei zu lesen um Arrays zu konstruieren. Wie der Name schon verrät, sollte die Datei eine Text-Datei sein. Die Datei kann ebenfalls eine Archiv-Datei sein. genfromtxt kann die Archiv-Formate gzip und bzip2 verarbeiten. Der Typ des Archivs wird durch die Datei-Erweiterung angegebenm d.h. '.gz' für gzip und '.bz2' für bzip2.

genfromtxt ist langsamer als loadtxt, jedoch kann es mit fehlenden Daten umgehen. Es verarbeitet die Datei in zwei Phasen. Zuerst werden die Zeilen in Strings konvertiert. Anschliessend werden die Strings in den geforderten Daten-Typ konvertiert. Auf der anderen Seite arbeitet loadtxt mit nur einem Schritt, wodurch es schneller ist.

In [ ]: