Lesen und Schreiben von Daten-Dateien

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.


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:

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 Zeichensequenz übergeben werden, z.B. ein Smiley " :-)":

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


Text-Dateien laden mit loadtxt

loadtxt ohne Parameter

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

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

Spezielle Trenner

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

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

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:

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

Datenkonvertierung beim Einlesen

def fahrenheit2celsius(t):
    return (float(t) - 32) * 5 / 9
converters_dict = {0:fahrenheit2celsius,
                   1:fahrenheit2celsius,
                   2:fahrenheit2celsius,
                   3:fahrenheit2celsius}
# Alternativ könnte converters_dict auch so definiert werden:
#converters_dict = dict(zip(range(4), [fahrenheit2celsius]*4)
y = np.loadtxt("temperatures.txt", 
               delimiter=" ", 
               skiprows=1,
               converters=converters_dict)
print(y)
[[23.5        20.94444444 24.5        25.27777778]
 [26.77777778 23.22222222 28.         29.        ]
 [30.16666667 26.33333333 31.77777778 32.33333333]]

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:

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))
360.1666666666667
387.75
779.9833333333333

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. Der 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 Funktion in einen Unicode-String transferieren:

y = np.loadtxt("times_and_temperatures.txt", 
               converters={ 0: time2float_minutes})
print(y)
[[ 360.    20.1]
 [ 361.5   16.1]
 [ 363.    16.9]
 ...
 [1375.5   22.5]
 [1377.    11.1]
 [1378.5   15.2]]


tofile

tofile ist eine Funktion, die es ermöglicht, 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. Übergibt man diesem Parameter einen leeren String, 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 es 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 Textdatei geht es auf Kosten der Geschwindigkeit und Dateigröße.

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)
[((10, 0), 98.25)]


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 Textdateien 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='')

fh = open("test4.txt", "rb")
print(np.fromfile(fh, dtype=dt))
[((  4294967296,  12884901890), 1.06099790e-313)
 (( 30064771078,  38654705672), 2.33419537e-313)
 (( 55834574860,  64424509454), 3.60739285e-313)
 (( 81604378642,  90194313236), 4.88059032e-313)
 ((107374182424, 115964117018), 6.15378780e-313)
 ((133143986206, 141733920800), 7.42698527e-313)
 ((158913789988, 167503724582), 8.70018274e-313)
 ((184683593770, 193273528364), 9.97338022e-313)]
import numpy as np
import os
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)
[32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49]

Vorsicht:

Es können Probleme auftreten, wenn tofile und fromfile für Datenspeicherung (Storage) benutzt werden, denn Binär-Dateien sind nicht plattformunabhängig. Über tofile werden keine Informationen zur Byte-Reihenfolge oder Daten-Typen abgespeichert. Daten können im .npy-Format plattformunabhä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 in der Benutzung von load und save. Im folgenden Beispiel benutzen wir eine temporäre Datei:

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)
[32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49]
Führt man obigen Code aus, erhält man folgende Ausgabe:
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])



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 angegeben 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. Anschließend werden die Strings in den geforderten Daten-Typ konvertiert. Auf der anderen Seite arbeitet loadtxt mit nur einem Schritt, wodurch es schneller ist.