Pascal-Tutorial (Vorwort, Installation, Kapitel 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)

Dateizugriff

9.1. Dateien lesen

Um Daten auf Dauer zu speichern, ist der Arbeitsspeicher ungeeignet. Spätestens nach dem Ausschalten des Computers gehen die Daten im Arbeitsspeicher verloren. Im Normalfall sogar bereits dann, wenn Sie das Programm beenden. Sollen größere Datenmengen gespeichert werden, ist der Arbeitsspeicher ebenso ungeeignet (obwohl groß relativ ist; jedenfalls speichern Festplatten ein Vielfaches). Eine Lösung stellt die Speicherung der Daten in Dateien dar. Wie Sie gleich sehen werden, ist es in Pascal sehr einfach, auf Dateien zuzugreifen.

Es gibt unterschiedliche Methoden, in Dateien zu schreiben oder diese zu lesen. Die Art des Schreibens/Lesens muss festgelegt werden. Will man beispielsweise eine Textdatei (Text bedeutet allgemein Zeichen) auslesen, so benötigt man den Typ Text. Weiters gibt es den allgemeinen Typ FILE. Verwendet man diesen, werden Dateien binär ausgelesen/geschrieben.

Betriebssystemseitig gilt die Unterscheidung zwischen Text- und Binärdateien aber nicht für Linux und UNIX-Systeme, wo alles eine "Binärdatei" ist. Natürlich besteht jede Datei - auch unter Windows-Systemen - aus binären Daten und wird auch dementsprechend gelesen. Wichtig ist, wie die Daten interpretiert werden. Will man dies weiter spezialisieren, gibt man mit FILE OF den genaueren Typ an.

Damit ist es möglich, z.B. eine Datei einzulesen, die aus reinen Integer-Zahlen besteht. Diese würde man dann mit FILE OF Integer angeben.

Der Typ wird wie folgt festgelegt:

VAR Bezeichner: Typ;    (* Bezeichner ist Dateivariable/-zeiger *)

Sieht aus wie eine Variablendeklaration, und richtig, eine solche ist es auch. Der Bezeichner ist NICHT der Pfad der Datei, sondern lediglich ein Bezeichner (Name), der benötigt wird, um auf die Datei zuzugreifen (ein Dateizeiger)! Ein Beispiel wäre:

VAR Textdatei: Text;

Textdatei wäre dann der Bezeichner, der benötigt wird, um auf Datei (hier vom Typ Text) zuzugreifen. Auf welche Datei genau (Pfad der Datei!) zugegriffen wird, wird hier noch nicht entschieden. Dies muss - wie ich gleich zeigen werde - mit Assign festgelegt werden. Es können natürlich auch mehrere Variablen von einem Dateityp deklariert werden. Dies ist mitunter dann notwendig, wenn auf mehrere Dateien zugegriffen werden soll, d.h. wenn diese zur selben Zeit geöffnet sind.

Zunächst wollen wir Dateien lesen. Der erste Schritt nach der Deklaration der Dateivariable ist die Festlegung des Pfades mit Assign:

Assign (Dateivariable, Pfad);

Assign legt fest, auf welche Datei über die Dateivariable zugegriffen wird (für später wichtig zu wissen). Beispielsweise:

Assign (Textdatei, 'C: ile01.txt');

Der nächste Schritt ist das Öffnen der Datei (bzw. soll in eine Datei geschrieben werden und diese existiert noch nicht, das Eröffnen der Datei; siehe 9.2). Das geschieht, will man nur aus der Datei lesen, mit Reset:

Reset (Dateivariable);    (* oeffnet die Datei zum Lesen *)

Nun ist die Datei geöffnet und wir können aus ihr Daten lesen. Wider aller Erwartungen benötigt man hierfür nur Read bzw. ReadLn. Read liest beim Auslesen von Textdateien bis zum Zeilenende und bleibt dort stehen. ReadLn liest zwar auch bis zum Zeilenende, springt jedoch in die nächste Zeile, was für das Lesen der nächsten Zeile wichtig ist.

Aus diesem Grund werden wir beim Lesen aus Textdateien ReadLn verwenden. Read werden wir in einem anderem Zusammenhang noch kennen lernen. Beachten Sie, dass ReadLn immer nur eine Zeile einliest. Um die gesamte Datei einzulesen, verwendet man eine Schleife. Diese soll dann beendet werden, wenn das Dateiende erreicht ist (unsere Abbruchbedingung). Dazu existiert die Funktion EOF (Abkürzung für End Of File; eine boolesche Funktion), die dann TRUE zurückliefert, wenn das Dateiende erreicht ist.

Wichtig ist auch, dass zuvor geöffnete Dateien immer geschlossen werden! Sonst kann es vorkommen, dass andere Programme nicht auf die Datei zugreifen können. Dateien schließen Sie mit Close:

Close (Dateivariable);

Das nächste Beispiel demonstriert, wie eine beliebige Textdatei eingelesen werden kann. Der gelesene Inhalt wird am Bildschirm ausgegeben:

PROGRAM Dateien_lesen;
USES Crt;
 
VAR Datei: Text;     (* Dateizeiger *)
    Gelesen, Pfad: String;
 
BEGIN
  ClrScr;
  WriteLn ('Welche Datei soll eingelesen werden: (Pfad)');
  ReadLn (Pfad);
 
  Assign (Datei, Pfad);     (* Pfad festlegen *)
  Reset (Datei);    (* Datei zum Lesen oeffnen *)
 
  REPEAT
    ReadLn (Datei, Gelesen);   (* eine Zeile lesen *)
    WriteLn (Gelesen);   (* eine Zeile am Bildschirm ausgeben *)
  UNTIL EOF (Datei);  (* Abbruch, wenn das Zeilenende erreicht ist; also wenn EOF TRUE liefert *)
 
  Close (Datei);  (* Datei schliessen *)
 
  ReadKey;
END.

Zunächst wird mit Assign der Pfad der festgelegt. Mit Reset wird sie dann zum Lesen (und nur dazu; in die Datei schreiben wäre ungültig bzw. nicht möglich!) geöffnet. Die darauffolgende Schleife wird solange ausgeführt, bis das Dateiende erreicht ist, dann liefert die Funktion EOF TRUE zurück. In der Schleife wird immer eine Zeile aus der Datei eingelesen und in der Variable Gelesen gespeichert. Anschließend wird mit WriteLn der Inhalt von Gelesen auf dem Bildschirm angezeigt.

Aus dem vorigen Beispiel geht hervor, wie ReadLn zum Lesen aus Dateien verwendet wird: (standardmäßig liest ReadLn ja von der Tastatur)

ReadLn (Dateivariable, Variable);

Aus der Datei, auf die die Dateivariable zeigt, wird eine Zeile eingelesen und in der angegebenen Variable gespeichert. ReadLn lässt sich nur auf Textdateien anwenden! Bei allen anderen Typen muss Read zum Lesen verwendet werden.

9.2. Dateien schreiben und kopieren

Das Schreiben in Dateien funktioniert sehr ähnlich und ist auch nicht weiter komplizierter. Zum Öffnen der Datei muss nun ReWrite anstelle von Reset verwendet werden. Existiert eine Datei noch nicht, wird sie neu angelegt. Existiert sie bereits, wird sie überschrieben. Achtung: Das geschieht ohne Vorwarnung!

Möchten Sie einen Text an eine Textdatei anfügen (hinten anhängen), können Sie stattdessen Append zum Öffnen verwenden (worauf ich in diesem Tutorial nicht näher eingehe). Mehr zu Append finden Sie in der Dokumentation Ihres Compilers oder in einem guten Pascal-Buch. In die Datei kann mit Write oder WriteLn geschrieben werden. WriteLn muss wie folgt verwendet werden: (Write genauso)

WriteLn (Dateivariable, Text);

Zum Beispiel:

WriteLn (Textdatei, 'Ein Text');

Natürlich kann statt eines Textes auch eine String-Variable angegeben werden, die einen Text enthält. Das nächste Beispiel demonstriert, wie ein beliebiger Text, den der Benutzer eingibt, in eine Datei geschrieben werden kann:

PROGRAM Dateien_schreiben;
USES Crt;
 
VAR Datei: Text;
    Schreiben, Pfad: String;
 
BEGIN
  ClrScr;
  WriteLn ('Pfad der zu Zieldatei:');
  ReadLn (Pfad);
 
  Assign (Datei, Pfad);
  ReWrite (Datei);   (* Datei zum Schreiben oeffnen! *)
 
  WriteLn ('Geben Sie nun den Text ein, der in die Datei geschrieben werden soll.');
  WriteLn ('Geben Sie "exit" ein, um abzuschließen.');
 
  REPEAT
    ReadLn (Schreiben);
    IF Schreiben <> 'exit' THEN
      WriteLn (Datei, Schreiben);   (* eine Zeile in die Datei schreiben *)
  UNTIL Schreiben = 'exit';   (* Abbruchbedingung *)
 
  Close (Datei);   (* Datei wieder schliessen *)
 
END.

Das Beispiel sollte soweit selbsterklärend sein.

Wie bereits erwähnt, können Dateien auch binär eingelesen werden. Das nächste Beispiel zeigt einen typischen Anwendungsfall dafür. Das nächste Programm ermöglicht es, Dateien zu kopieren:

PROGRAM Dateien_kopieren;
USES Crt;
 
VAR Quelldatei, Zieldatei: FILE;    (* Wichtig: Typ FILE! *)
    Quelldatei_Pfad, Zieldatei_Pfad: String;
    Speichern: Char;
    Gelesen, Geschrieben: Integer;
 
BEGIN
  ClrScr;
 
  WriteLn ('Welche Datei soll kopiert werden:');
  ReadLn (Quelldatei_Pfad);
  WriteLn ('Unter welchem Pfad und Dateinamen soll diese Datei gespeichert werden:');
  ReadLn (Zieldatei_Pfad);
 
  Assign (Quelldatei, Quelldatei_Pfad);
  Reset (Quelldatei,1);   (* Quelldatei nur zum Lesen oeffnen *)
 
  Assign (Zieldatei, Zieldatei_Pfad);
  ReWrite (Zieldatei,1);  (* Zieldatei zum Schreiben oeffnen *)
 
  REPEAT
    BlockRead (Quelldatei, Speichern, 1, Gelesen);
 
    IF (gelesen <> 0) THEN
      Blockwrite (Zieldatei, Speichern, 1, Geschrieben);
 
  UNTIL (Gelesen <> Geschrieben) OR (Gelesen = 0);
 
  Close (Quelldatei);
  Close (Zieldatei);
 
END.

Da dieses Beispiel sehr viel Neues enthält, möchte ich es näher erläutern. Zunächst deklarieren wir die zwei Variablen Quelldatei und Zieldatei vom untypisierten Typ FILE. Untypisiert bedeutet, dass die Daten einfach als binär angesehen werden. Diese zwei Variablen benötigen wir für den Zugriff auf die Dateien. Wir benötigen zwei Variablen, da beide Dateien gleichzeitig geöffnet sind. Aus der einen Datei lesen wir die Daten, und in die andere Datei schreiben wir die eben gelesenen Daten. Das geht solange, bis die Quelldatei komplett gelesen wurde (am Ende angelangt).

In den Variablen Quelldatei_Pfad und Zieldatei_Pfad werden die Pfade der Quell- und Zieldatei gespeichert. Nachdem der Benutzer diese eingegeben hat, werden die Dateien geöffnet. Achten Sie bei beiden Eingaben darauf, dass Sie die kompletten Pfade angeben; z.B. C: iles ile01.txt und nicht bloß C: iles. Die Quelldatei (diese Datei soll kopiert werden) wird mit Reset nur zum Lesen geöffnet. Die Zieldatei (die soll eine exakte Kopie der Quelldatei werden) wird mit ReWrite zum Schreiben geöffnet. Existiert die Zieldatei bereits, wird sie gnadenlos überschrieben.

Was tun, wenn man herausfinden will, ob eine Datei existiert oder nicht? Kleiner Tipp am Rande: (ein wenig kompliziert, aber funktioniert wunderbar) Zunächst muss der Benutzer den Pfad der Datei eingeben. Öffnen Sie diese Datei zum Lesen mit Reset und versuchen Sie, einen Wert in eine Variable einzulesen.

Wichtig ist, dass Sie vorher mit der Compilerdirektive {$I-} die Fehlerüberprüfung ausgeschaltet haben. Schreiben Sie dazu einfach {$I-} in den Quellcode (ohne Semikolon am Schluss). Diese sollten Sie später wieder mit {$I+} einschalten (beides gilt für Turbo Pascal 7.0, die Compilerdirektive kann bei anderen Compilern anders lauten). Testweise können Sie die Fehlerüberprüfung aber auch über die Optionen Ihres Compilers abschalten!

Nachdem ein Wert in eine Variable eingelesen wurde, überprüfen Sie den Wert der Variable IORESULT. Diese enthält automatisch während der Programmausführung die Fehlercodes, die das Betriebssystem liefert. Läuft der Programmablauf korrekt ab, steht in der Variable eine 0, anderenfalls ein anderer Wert, eben der Fehlercode, den das Betriebssystem geliefert hat. Steht nach dem versuchten Lesen eine 0, existiert die Datei. Denn dann konnte das Programm etwas aus der Datei lesen, anderenfalls existiert die Datei nicht.

Hätte IORESULT den Wert 0 (Datei existiert dann), könnten Sie als Programmierer eine Abfrage einzubauen, die den Benutzer fragt, ob er die Datei wirklich überschreiben will. Das muss natürlich vor dem ReWrite stehen, sonst ist es zu spät. ;-)

Mit BlockRead ist es möglich, Daten aus einer Datei blockweise einzulesen und in einer Variable zu speichern. Blockweise bedeutet in diesem Fall, dass wir bis zu 65536 Bytes in einem Block (also auf einmal) einlesen können. Um diese abzuspeichern, benötigt man ein Array. Da wir hier aber nur 1 Byte einlesen (dritter Parameter), reicht eine Char-Variable vollkommen aus.

Der erste Parameter bei BlockRead gibt die Dateivariable an, der zweite die Variable, die die eingelesenen Daten aufnimmt. Der dritte Parameter gibt an, wie groß die Datenmenge sein soll, die auf einmal gelesen werden soll und der vierte Parameter (optional; hier eine Angabe zu machen ist nicht erforderlich) gibt eine Variable an, die als Kontrolle dient, ob etwas gelesen wurde (den Wert dieser Variable vergleicht man üblicherweise mit der Variable, die bei BlockWrite angegeben wurde).

Bei BlockWrite ist es ähnlich: Der erste Parameter gibt die Dateivariable der Zieldatei an, der zweite die Variable mit den zu schreibenden Daten. Der dritte Parameter gibt an, wie große die Datenmenge sein soll, die geschrieben werden soll (dieser Wert sollte mit dem 3. Parameter von BlockRead übereinstimmen; wir lesen 1 Byte und schreiben 1 Byte). Der letzte Parameter ist wiederum optional. Hier kann eine Variable angegeben werden, die als Kontrollwert für die Schreiboperation dienen soll (ähnlich dem 4. Parameter von BlockRead). Die Schleife wird dann beendet, wenn nichts gelesen wurde oder wenn die beiden Kontrollwerte nicht mehr übereinstimmen.

Dieses Beispiel hat allerdings einen gravierenden Nachteil: Es kopiert die Dateien sehr langsam. Dafür kann es nicht passieren, dass eine Datei zu groß wird. Bei Dateien mit einer Größe unterhalb der Blockgröße kann das der Fall sein (dies hat allerdings keine negativen Auswirkungen). Das nächste Beispiel liest immer 1024 Byte auf einmal ein und schreibt diese kurz darauf in eine Datei. Die Funktion SizeOf liefert die Größe einer Variable in Byte zurück. In diesem Fall liefert sie 1024.

PROGRAM Dateien_kopieren_schneller;
USES Crt;
 
VAR Quelldatei, Zieldatei: FILE;
    Quelldatei_Pfad, Zieldatei_Pfad: String;
    Speichern: Array [1..1024] OF Char;    (* Array so gross wie die Blockgroesse *)
    Gelesen, Geschrieben: Integer;
 
BEGIN
  ClrScr;
  WriteLn ('Welche Datei soll kopiert werden:');
  ReadLn (Quelldatei_Pfad);
  WriteLn ('Unter welchem Pfad und Dateinamen soll diese Datei gespeichert werden:');
  ReadLn (Zieldatei_Pfad);
 
  Assign (Quelldatei, Quelldatei_Pfad);
  Reset (Quelldatei,1);
 
  Assign (Zieldatei, Zieldatei_Pfad);
  ReWrite (Zieldatei,1);
 
  REPEAT
    BlockRead (Quelldatei, Speichern, SizeOf(Speichern), Gelesen);  (* SizeOf liefert hier 1024 zurueck, also die Groesse des Arrays *)
 
    IF (gelesen <> 0) THEN
      Blockwrite (Zieldatei, Speichern, SizeOf(Speichern), Geschrieben);
 
  UNTIL (Gelesen <> geschrieben) OR (Gelesen = 0);
 
  Close (Quelldatei);
  Close (Zieldatei);
 
END.

Um das Kapitel Dateizugriff abzuschließen, möchte ich noch einmal das Telefonbuch-Beispiel aus Kapitel 7 hernehmen. Ich habe versprochen, zu zeigen, wie diese Daten in einer Datei abgelegt werden können. So geht's:

PROGRAM Telefonbuch_Datei;
USES Crt;
 
TYPE Daten = RECORD
       Name: String[50];
       Adresse: String[255];
       Telefonnummer: String[15];
     END;
 
     Gesamte_Daten = ARRAY [1..100] OF Daten;
 
VAR Datensatz: Gesamte_Daten;
    i: 0..100;
    Datei: FILE OF Gesamte_Daten;
 
BEGIN
  ClrScr;
 
  Assign (Datei, 'daten.dat');
  {$I-} Reset (Datei);   (* Datei zum Lesen oeffnen; die Compilerdirektive verhindert Fehlermeldungen *)
  Read (Datei, Datensatz);  (* die gesamte Struktur einlesen *)
  Close (Datei); {$I+}
 
  IF IORESULT <> 0 THEN
  BEGIN
    WriteLn ('Fehler beim Lesen der Daten! Ende.');
    Exit;  (* ggf. auskommentieren bei Problemen *)
  END;
 
  REPEAT
 
    Write ('Welchen Eintrag verwenden: (1-100; 0 = Programmende) ');
    ReadLn (i);
 
    IF i = 0 THEN Exit;  (* Programm beenden *)
 
	(* Wenn Datensatz leer ist, Daten eingeben lassen *)
    IF (Datensatz[i].Name = '') OR (Datensatz[i].Adresse = '') OR (Datensatz[i].Telefonnummer = '') THEN
    BEGIN
      WriteLn ('Eintrag ',i);
      Write ('Name: ');
      ReadLn (Datensatz[i].Name);
      WriteLn ('Adresse:');
      ReadLn (Datensatz[i].Adresse);
      Write ('Telefonnummer: ');
      ReadLn (Datensatz[i].Telefonnummer);
      WriteLn;
 
      ReWrite (Datei);  (* Datei zum Schreiben oeffnen *)
      Write (Datei, Datensatz);   (* speichern; wichtig: geht nur mit Write! *)
      Close (Datei);
    END
    ELSE  (* Daten anzeigen *)
    BEGIN
      WriteLn ('Name: ',Datensatz[i].Name);
      WriteLn ('Adresse:');
      WriteLn (Datensatz[i].Adresse);
      WriteLn ('Telefonnummer: ',Datensatz[i].Telefonnummer);
    END;
 
  UNTIL i = 0;   (* So lange wiederholen, bis der Benutzer 0 eingibt *)
 
  ReadKey;
END.

Das Beispiel macht Folgendes: Das Telefonbuch besteht aus 100 "Speicherplätzen". Damit Daten gespeichert bzw. wieder abgerufen werden können, muss man sich für einen, bzw. beim Wiederabrufen für den richtigen Speicherplatz entscheiden. Ist der Speicherplatz bereits belegt, werden die eingespeicherten Daten angezeigt, anderenfalls fordert das Programm den Benutzer auf, Eingaben zu machen (Name, Telefonnnummer und Adresse).

Abgespeichert werden die Daten in einer Datei. Wir können (bzw. das Beispiel tut das nicht) die Daten allerdings nicht direkt in die Datei schreiben, sondern müssen ein exaktes Abbild der zu schreibenden Daten im RAM (Random Access Memory = Arbeitsspeicher; genaue Übersetzung: Speicher mit wahlfreiem Zugriff) erzeugen. Darum benötigen wir die Variable Datensatz.

Diese ist vom Typ Gesamte_Daten (derselbe Typ, von dem auch die Datei ist!). Der Typ Gesamte_Daten ist im TYPE-Block mit ARRAY [1..100] OF Daten definiert. Übrigens ist es nicht gleichgültig, ob Datensatz vom Typ Gesamte_Daten oder ARRAY [1..100] OF Daten ist, obwohl dies prinzipiell die gleiche Wirkung haben sollte. Wurden die Daten in die Variable Datensatz geschrieben, schreiben wir die Variable Datensatz mit Write (Datei, Datensatz); zurück in die Datei.

Das Programm speichert die Daten in der Datei daten.dat. Diese wird im lokalen (= aktuellen) Verzeichnis abgelegt, daher in dem Verzeichnis, aus dem das Programm gestartet wurde (in dem die ausführbare Datei des Programmes liegt). Diese Art der Pfadangabe bezeichnet man als relative Pfadangabe (relativ, ausgehend vom aktuellen Verzeichnis), während man Pfadangaben wie C:\dir1 ile01.exe als absolute Pfadangaben bezeichnet. Bei absoluten Pfadangaben ist es egal, aus welchem Verzeichnis das Programm gestartet wird. Das Programm wird immer versuchen, die Datei der absoluten Angabe gemäß am angegebenen Ort abzulegen.

Höchstwahrscheinlich haben Sie sich bereits einmal die Dateigröße angesehen. Es dürfte Ihnen dann aufgefallen sein, dass sich diese nicht ändert, egal wieviele Adressdaten in der Datei abgespeichert werden. Die Ursache dafür liegt darin, dass die Datei von einem selbstdefinierten Typ ist. Der Typ besteht aus einer Struktur mit 3 Stringvariablen. Die erste hat eine Maximallänge von 50 Zeichen (Name), die zweite eine von 255 (Adresse) und die dritte eine von 15 Zeichen (Telefonnummer). Das Ganze steht in der Datei 100-mal. Und zwar mindestens 100-mal und max. 100-mal, denn die Dateigröße verändert sich nicht.

Damit die gespeicherten Daten wiedergefunden werden, müssen diese an einem fest definierten Platz stehen. Für jedes Zeichen ist ein fixer Platz reserviert. Öffnen Sie die Datei daten.dat mit einem Texteditor (oder besser Hexeditor) und Sie können erkennen, dass diese mit Leerzeichen gefüllt ist. Leerzeichen stehen an allen Plätzen, die noch nicht mit Daten "gefüllt" wurden.

Vorheriges Kapitel Nächstes Kapitel