Textdateien lesen und schreiben


Auf dem C16 hat sich der Umgang mit Dateien auf das Speichern ("SAVE") und Laden ("LOAD") von Programmdateien beschränkt. Angesichts der Tatsache, dass damals meist nur Audiocassetten als Speichermedium zur Verfügung standen - kein Wunder. Auf einem modernen PC sieht das anders aus. Er bietet mit der Festplatte die Möglichkeit, sehr schnell auf Dateien zugreifen zu können. Und zwar nicht nur auf Programmdateien - bei den paar Kilobyte ist die Geschwindigkeit nicht relevant. Aber insbesondere auf Datendateien, also Dateien, in die unsere Programme Daten reinschreiben und sie wieder auslesen. Wir können also Dateien als eine Art Hauptspeicherersatz benutzen. Und in diesem Zusammenhang sind die Vorteile Legion:

Na, Appetit bekommen? Dann wollen wir mal anfangen!

Auf welche Datenträger kann ich Dateien schreiben?

Hier ein Tipp, wenn Sie Ihre ersten Versuche starten:

Arten des Dateizugriffs: Sequentiell und Random Access

Es gibt zwei Arten, wie man Dateien nutzen kann. Bei der ersten lesen oder schreiben wir eine Datei wie ein Magnetband: Alles hintereinander, von Anfang bis Ende. Das ist der programmiertechnisch einfachere, s.g. sequentielle Zugriff. Beim Random Access-Zugriff (wahlfreien Zugriff) weisen wir an, irgendwo in der Mitte die Datei zu lesen. Das geht. Denn die Datei besteht zunächst wie alles beim Computer aus Bytes. Und wir können sagen: "Bitte fange mit 200567. Byte an und nicht mit dem ersten!" Byte 200566 (wir zählen immer von null weg!) ist dann die Adresse des Bytes - genau wie im Hauptspeicher. (Siehe Bits, Bytes und Basic)

In der Regel werden wir bezüglich RA-Zugriff allerdings komfortablere Möglichkeiten haben, so auch bei qbasic. Wir können anweisen, nicht das i-te Byte aus der Datei zu lesen, sondern das i-te Element. Dabei kann jedes Element, das wir geschrieben haben, ein ganzes Array sein. Oder zumindest eine Fliesskommazahl, die ja bekanntlich mehr Speicher braucht als nur ein Byte. Mit diesen Möglichkeiten werden wir uns im nächsten Kapitel beschäftigen.

Binary Dateien und Textdateien

Es gibt zwei grundverschiedene Dateitypen, die haben wir schon bei der Einführung in DOS kennengelernt: Binary-Dateien und Textdateien. Ganz, ganz früher in der PC-Steinzeit gab es den Grundsatz, grosse Dateien als Binary-Dateien zu schreiben. Das sollte man heute bleiben lassen. Heute gilt die dicke Empfehlung: Schreiben Sie selbst Daten auf die Festplatte und können Sie das Format selbst bestimmen, so schreiben Sie eine Textdatei! Der Grund ist einfach: Textdateien können Sie mit einem normalen Editor lesen. Und damit auch kontrollieren, wenn Ihr Programm mal Mist gebaut haben sollte. Und Sie können an die Daten auch dann noch rankommen, wenn Sie das Programm gerade mal nicht zur Hand haben. Und Sie werden sehen: Wenn Sie nicht ein ganz, ganz, ganz ordentlicher Mensch sind, werden Sie spätestens nach drei Jahren nie das passende Programm zu den Daten mehr haben. Das ist einfach so, glauben Sie mir es. Schauen Sie sich qbasic an: Es dauert nicht mehr lange und Sie müssen ziemliche Klimmzüge machen, qbasic überhaupt noch auftreiben und zum Laufen bringen zu können!

Übrigens sind Sie mit dieser Textdateien-Strategie auf der Höhe der Zeit: Das "modernste" Dateiformat nennt sich XML und ist ein Textformat. Auch Webseiten und das Word-Austauschformat RTF sind Textformate.

Sie natürlich sich auch mit Binary-Dateien befassen müssen, z.B. wenn Sie es mit Bilddateien lesen oder schreiben wollen (oder gar Videos! Das kann ich Ihnen in qbasic aber nur begrenzt empfehlen...) Insbesondere wenn Sie mit qbasic eine Datenbank bauen wollen (was durchaus geht!), müssen Sie RA-Dateizugriffe machen und diese funktionieren nur mit Binaries.

Schreiben von Textdateien

Das Schreiben oder Lesen Dateien geschieht in 3 Schritten:

  1. Öffnen der Datei
  2. Schreiben oder Lesen
  3. Schliessen der Datei

Punkt 1: Öffnen einer Datei

Das Kommando sieht ein bisschen kompliziert aus, ist aber ganz einfach:

Lesezugriff: OPEN "datei.txt" FOR INPUT AS #1

Löschender Schreibzugriff: OPEN "datei.txt" FOR OUTPUT AS #1

Anhängender Schreibzugriff: OPEN "datei.txt" FOR APPEND AS #1

Die Befehle unterscheiden sich also nur im Schlüsselwort INPUT, OUTPUT und APPEND. "Löschender Schreibzugriff" heisst: Wenn die Datei schon existiert, dann wird sie gelöscht, bevor nun neu geschrieben wird. "Anhängender Schreibzugriff" heisst: Die Datei bleibt bestehen und das, was wir nun neu schreiben, wird hinten an gehängt.

Das Ende jedes OPEN-Befehls, dieses "#1", ist ein Name, unter dem Sie die Datei (engl. das "File") ansprechen können, das s.g. Filehandle. Sie werden ihn beim Schreiben oder Lesen brauchen. Es gibt durchaus die Möglichkeit, mehrere Files offen zu haben. Sehr gebräuchlich ist, eines zum Lesen und eines zum Schreiben zu öffnen.

Punkt 2: Schreiben von Text in Dateien

Das ist nun einfacher, als Sie vielleicht denken. Sie schreiben in eine Textdatei genauso, wie Sie auf den Bildschirm schreiben: Mit PRINT. Das Einzige, was Sie machen müssen, ist, den Filehandle davorzuhängen. Wir schreiben also:

OPEN "text1.txt" FOR OUTPUT AS #1
PRINT #1,"Hallo Welt! Ich bin in einer Datei!"
CLOSE #1

Und schon haben wir unser erstes Programm fertig. (Das Close kommt erst im nächsten Kapitel, ist aber wohl nicht schwer.)

Wenn Sie dieses Programm ausführen, scheint gar nichts zu passieren. Klar, auf dem Bildschirm passiert ja auch nichts. Aber wenn Sie nun in Ihrem Dateimanager oder mit dem dir-Kommando von der Konsole aus das betreffende Verzeichnis anschauen, müsste dort eine neue Datei namens "text1.txt" stehen. Diese können Sie nun mit einem Editor, z.B. edit, notepad oder qbasic selbst öffnen und schauen, was drin steht. Und? Hat alles geklappt?

Wenn Sie die Datei nicht finden sollten...

...keine Panik. Ihr qbasic kann Dateien schreiben und Ihr PC auch und Sie auch. Ist nur eine Frage, wo der Fehler steckt...

Punkt 2: Lesen der Datei

Das Lesen ist grundsätzlich nicht schwieriger als das Schreiben. Probleme kann es nur dann machen, wenn der Lesevorgang nicht genau weiss, welche Textteile der Datei in welcher Variablen abgespeichert werden sollen. Aber das Problem haben wir erst später. Bisher ist alles noch ganz einfach. Wie beim Schreiben ist auch beim Lesen der Befehl bekannt: INPUT #1,a$. Dies liest die nächste Zeile der Datei und speichert sie in a$. Dann mal los!

OPEN "text1.txt" FOR INPUT AS #1
INPUT #1,a$
PRINT a$
CLOSE #1

Jetzt müsste unser Text auf dem Bildschirm erscheinen.

Fehlermeldung: "Datei ist schon geöffnet"

Das wird beim Testen unweigerlich passieren: Man startet das Programm neu und plötzlich funktioniert der OPEN-Befehl nicht mehr - mit der obigen Fehlermeldung. Was passiert da? Nun, Dateien bleibt in QBASIC auch dann geöffnet, wenn man den Programmlauf abbricht, das Programm verändert und dann neu startet. Da hilft nichts anderes, als in die Eingabezeile (F6) zu hüpfen und ein manuelles "close #1" abzuschicken, bevor man neu startet, falls man den Lauf zuvor vorzeitig abgebrochen hat.

Ende der Datei: EOF()-Funktion

Beim Lesen ist es wichtig, zu wissen, ob man das Dateiende schon erreicht hat. Leseversuche, wenn man alles schon "weggelesen" hat, sind nicht sinnvoll. Diese Info gibt uns die Funktion EOF(Filenumber). Sie ist True, falls das Fileende erreicht wurde. Wir schreiben also z.B.

OPEN "test.txt" FOR INPUT AS #1
WHILE NOT EOF(1)
  INPUT #1,a$
  ? a$
WEND
CLOSE(1)

um uns die gesamte Datei anzeigen zu lassen, egal wie gross sie ist.

Umlaute in DOS- und Windows-Texten

Wenn Sie zufällig mit einem Windows-Programm einen Text erstellt haben, in dem Umlaute vorkommen, z.B. "Käse oder Brötchen?", dann werden Sie enttäuscht sein, was dabei herauskommt, wenn Sie das in DOS einlesen. Das liegt daran, dass die deutschen Umlaute nicht zum Umfang des s.g. ASCII-Zeichensatzes gehören und unter DOS anders kodiert sind, als in der unter Windows verwendeten Kodierung, der s.g. ANSI-Kodierung. Wenn man also in einem DOS-Programm Umlaute verwendet, sieht man unter Windows nur Quatsch und umgekehrt. Viele Windowsprogramme haben Optionen, mit denen man den Text explizit als DOS-Text einladen kann. Umgekehrt gibt's diese Option natürlich meist nicht.

Was tun? Nun, wir werden in unseren qbasic-Programmen selten Brieftexte verarbeiten. Sondern eher technischere Daten, z.B. Zahlentabellen u.ä. Hier empfiehlt es sich, falls mal irgendwo Text auftaucht, diesen mit oe, ae usw. zu schreiben. Das kann man überall lesen. (Ist übrigens auch eine Empfehlung für die Programmkommentare!)

Und wenn man es mit Namenslisten o.ä. zu tun hat? Dann gibt es mehrere Möglichkeiten:

Arrays schreiben und lesen: Trennzeichen

In Arrays speichern wir Zahlen- oder Stringtabellen. Oft sind das Daten, die im Programm von vornherein feststehen, s.g. Parameter. Bis jetzt haben wir diese entweder mit LET-Anweisungen direkt im Programmtext festgehalten oder in DATA-Zeilen verpackt. Aber es gibt eine viel bessere und flexiblere Methode: Wir schreiben sie mit einem Editor in eine Textdatei und lesen sie von dort ins Programm!

Unser Übungsbeispiel sollen die Schulnoten einer Klasse sein. Wir haben es mit einer Klasse von 32 Schülern zu tun und sie haben 4 Arbeiten geschrieben, ausserdem gibt es eine mündliche Note. Wir wollen nun sowohl die Note jedes Schülers als auch die Klassendurschnitte errechnen.

Die Arbeit, ein Programm zu schreiben, das die ganzen Einzelnoten aufnimmt, können wir uns sparen. Das können wir mit einem Editor genauso machen.

Die Tabelle soll folgendes Format haben:

Name      Note_P1  Note_P2  Note_P3  Note_P4  Note_muendl 

P1,P2,P3,P4 sind die Abkürzungen für die Prüfungen. In einer Zeile stehen also insgesamt sechs Angaben. Wie lesen wir diese sechs Angaben ein? Machen wir einmal folgenden Versuch:

Mueller 1.5 2.5 3.5 2.5 2

Zwischen jeder Zahl lassen wir ein Leerzeichen. Wir speichern das in der Datei "test1.txt" ab und lesen es mit Vierzeiler von oben ein. Was ist in a$ gespeichert? Die ganze Zeile! Inklusive aller Leerzeichen! qbasic erkennt also nicht, dass die Leerzeichen eigentlich Trennzeichen sein sollen und nach "Mueller" die erste Angabe schon beendet ist. Was jetzt tun? Nun, andere Trennzeichen ausprobieren. Tabulatoren klappen auch nicht. Aber Kommas gehen! Die Angabe

Mueller,1.5,2.5,3.5,2.5,2
führt dazu, dass nur noch "Mueller" in a$ gespeichert ist. Und die Abwandlung des Programms in
DIM note(5)
OPEN "test1.txt" FOR INPUT AS #1
INPUT #1, NAME1$
FOR i = 0 TO 4
  INPUT #1, note(i)
NEXT i
PRINT NAME1$;
FOR i = 0 TO 4
  PRINT TAB(10 + i * 10); note(i);
NEXT i
CLOSE #1
sollte eigentlich das gewünschte Resultat erbringen.

Jetzt gibt es noch eine Kleinigkeit: Wir wollen in die erste Zeile unserer Input-Datei eine Kopfzeile schreiben, die auch schon in der Datei ersichtlich werden lässt, was was ist. Die Datei soll also so aussehen:

Name Note_P1 Note_P2 Note_P3 Note_P4 Note_muendl

Mueller,1.5,2.5,3.5,2.5,2
Mayer,3.5,3.5,3,4,3.5,4
Hinter Mueller und Mayer kommen natürlich noch die anderen 30 Schüler. Wir müssen nun unser Programm anweisen, die ersten beiden Zeilen zu überspringen. Wie machen wir das? Nun, wir können die ersten beiden Zeilen natürlich einlesen; wir lesen sie einfach in eine x-beliebige Variable ein, deren Inhalt nicht weiters wichtig ist. Wichtig ist nur, dass in der Datei in der ersten und zweiten Zeile keine Kommas auftauchen, damit wir die ganze Zeile in einem Rutsch in eine Stringvariable bekommen. Das Ganze sieht dann so aus:

DIM note(5)
OPEN "test1.txt" FOR INPUT AS #1
FOR i = 0 TO 1: INPUT #1, a$: NEXT i
INPUT #1, NAME1$
FOR i = 0 TO 4
  INPUT #1, note(i)
NEXT i
PRINT NAME1$;
FOR i = 0 TO 4
  PRINT TAB(10 + i * 10); note(i);
NEXT i
PRINT
CLOSE #1

Von einer Tabellenkalkulation nach qbasic

Wenn Sie sich mit einer Tabellenkalkulation wie StarCalc oder Excel auskennen, dann werden Sie sich sagen: Ja, so eine Tabelle kann ich doch viel schöner dort erstellen, als in einem Editor! Gibt es eine Möglichkeit, so eine Tabelle in qbasic einzulesen?

Gibt es zuhauf. Im Prinzip können Sie schon jetzt in qbasic so gut wie alles programmieren, sogar ein DVD-Authoring-Programm...Aber der Reihe nach.

Ich beziehe mich in meiner Erklärung jetzt auf Excel, andere Programme funktionieren ganz ähnlich. Man erstellt also die Tabelle ohne grosse Formatierung. Dann markiert man die Tabelle und speichert die Markierung als Datei des Typs "csv" ab. Das heisst: "Comma Seperated Value". Dieser Name ist irreführend, denn es sind nicht Kommas, die die Werte trennen, sondern Semikolons. Das liegt daran, dass im Deutschen Kommas schon die Dezimaltrennzeichen sind. Und das ist auch das Problem beim Übergang von csv nach qbasic: qbasic ist Englisch und braucht Punkte als Dezimaltrennzeichen und Kommas als Trennzeichen. Was tun?

Nun, jeder Editor hat heute eine "Suchen" und "Ersetzen"-Funktion. Man geht also in den Editor und ersetzt mittels dieser Funktion zunächst alle Kommas durch Punkte. Und dann alle Semikolons durch Kommas. (In dieser Reihenfolge!) Und schon ist die englische csv-Datei fertig.

Jetzt besteht noch eine Stolperfalle: Unsere Kopfzeile. Sie ist natürlich nun auch kommagetrennt. Deshalb muss unser Programm so angepasst werden, dass es die Kopfzeile "formattreu" einliest, d.h. wirklich sechs Strings pro Zeile. Oder den LINE INPUT-Befehl benutzt. Siehe nächster Abschnitt. Das war's dann aber auch schon.

LINE-INPUT-Befehl

In manchen Fällen will man tatsächlich, dass die ganze Zeile einer Stringvariablen übergeben wird, egal ob darin ein Komma vorkommt oder nicht. Wie mache ich das? Dafür gibt es einen extra Befehl: LINE INPUT. LINE INPUT #1,a$ übergibt die ganze Zeile an a$. Auf diese Weise können Zeilen in die Dummy-Variable a$ "weggelesen" werden, egal was sie enthalten.

Width-Befehl

Manches ist bei QBASIC/VBasic nicht so toll gelöst. So die Tatsache, dass der Fileausgabe genau die gleiche physische Spaltenbreite von 80 Zeichen pro Zeile unterstellt wird wie der Bildschirmausgabe. D.h., die Ausgabe bricht auch dann nach 80 Zeichen um, wenn man noch gar keinen Zeilenumbruch geschrieben hat. Ein bisschen lindern kann man das Problem mit dem Width-Befehl: Mit WIDTH #1,255 setze ich die Fileausgabe auf 255 Zeichen pro Zeile Maximalwert für File #1. Auch das ist viel zu wenig. Wenn man mehr braucht, hat man erstmal Pech gehabt. Das gilt übrigens auch für die Eingabe: QBASIC ist nicht in der Lage, Files zu lesen, deren Zeilenbreite 255 Zeichen übersteigt...

Übungen

  1. Schreiben Sie auf der Basis des gerade Gelernten das Notenberechnungsprogramm! Geben Sie die Ergebnisse auf dem Bildschirm aus.
  2. Schreiben Sie eine Variante, die die Ergebnisse in die Datei "results.txt" schreibt.
  3. Schreiben Sie die Programme aus dem letzten Kapitel so um, dass die Spritedaten aus einer Datei eingelesen werden und nicht aus DATA-Zeilen!
  4. Nicht ganz einfach: Schreiben Sie ein Unterprogramm, das in einer Textdatei nach einem frei definierbaren Schlüsselwort sucht, also solange die Textdatei einliest, bis das Schlüsselwort gefunden wurde. Dieses Unterprogramm ist sehr nützlich, um in ein und derselben Datei mehrere Tabellen unterzubringen, die man jeweils durch ein Schlüsselwort kennzeichnet. Man kann dann praktisch wahlfrei auf die einzelnen Tabellen zugreifen. So funktionieren Initialisierungsdateien unter Linux!

Exkurs: "Reengeneering" von Text-Dateiformaten am Beispiel von Postscript (für den kleinen Gebrauch)

Hier entlang