Textfiles lesen und schreiben AnPr Name 1 Klasse Datum Allgemeines Daten werden bekanntermaßen im Computer in einem flüchtigen Speicher, dem RAM verarbeitet. Insofern gehen sie verloren, sobald der Rechner ausgeschaltet wird. Weiterhin ist der RAM begrenzt, was in Summe eine weitere Möglichkeit fordert, Daten zu speichern. Wir alle wissen, dass uns diese Möglichkeit die Festplatte liefert, indem wir die Daten als Files speichern. Da der RAM „nur“ Zahlen im digitalen Format ablegt liegt es also nahe, dass die Daten auf dem Filesystem ebenfalls als Zahlen in digitaler Form vorliegen. In den folgenden Kapiteln werden wir uns genau diesen Prozess etwas genauer ansehen und den Code kennen lernen, welcher in Java für den Umgang mit Files ermöglicht. Hierbei werden wir uns auf Textdateien konzentrieren, wobei es dem Filesystem erst mal „egal“ ist, ob es sich gerade um ein Text- oder Binärfile handelt. Um dies zu verdeutlichen hier zwei Bytesequenzen. Eine Bytesequenz eines Binärfile (hier VLC.exe): … 74 24 14 8D B5 E0 FD FF FF C7 44 24 04 2A D8… Hier eine Bytesequenz eines Textfiles (hier AUTHORS.txt von VLC): … 69 64 65 6F 4C 41 4E 20 61 6E 64 20 74 68 65… Wie wir sehen, können wir als „normaler Mensch“ erst mal keinen systematischen Unterschied erkennen. In der Tat kann das der Computer ebenfalls nicht. Man muss ihm erstmal mitteilen, was er mit diesem File machen soll. Wenn wir die Datei AUTHORS.txt mit einem Texteditor (bspw. Notepad++) öffnen, dann werden die Bytes als Zeichen interpretiert und können von uns gelesen werden. Wir können aber auch die Datei VLC.exe mit Notepad++ öffnen. Wir werden dann ein fürchterliches Durcheinander von Zeichen sehen – und zwar solchen, welche mit bekannten Zeichen wie Buchstaben angezeigt werden können und solchen, welche nur durch irgendwelche Sonderzeichen angegeben werden. Dies liegt daran, dass die Bytes in VLC.exe nicht für das lesen in einem Texteditor geschaffen wurden, sondern durch die Interpretation durch das Betriebssystem Windows. Dies führt uns zu folgender Erkenntnis: Dateien müssen von einem Programm sinnvoll interpretiert werden. 2 Die Codierung Sehen wir uns mal den vereinfachten Weg der Informationen von der Tastatur aus bis zum Filesystem an: Zuerst geben wir die Buchstaben ein. Die Signale werden von der Tastatur über das Betriebssystem an den Texteditor gesendet. Die Tastatur schickt also Codes für die eingegebenen Zeichen. Im Texteditor wird nun eine Umsetzung gemacht. Mittels einer Art Tabelle – dem Zeichensatz, oder der Zeichencodierung – wird nun jedem gewünschten Zeichen ein neuer Code zugeordnet. Dieser Code ist jetzt der Text mit der verwendeten Zeichencodierung und wird auch 1:1 auf dem Filesystem abgelegt. Wollen wir das File nun wieder lesen, so wird mit der gleichen Zeichensatztabelle jedem Code wieder ein Zeichen zugeordnet – der Text ist wieder lesbar. Wichtig ist also, dass die Codierung beim Lesen und Schreiben identisch ist – zumindest für den Fall, dass es Zeichen mit einem Codewert über 127 gibt. ANPR_TextFiles_v02.docx Seite 1 Textfiles lesen und schreiben AnPr Sollte dies nicht so sein, könnte bspw. folgendes passieren. Wir speichern den Text „Straße“ mit der Codierung „OEM 858“ ab und lesen das File wieder mit „ISO 8859-1“. Hierbei werden alle Zeichen kleiner als 128 korrekt dargestellt, was die Zeichen S, t, r, a, s und e beinhaltet. Das Zeichen ß liegt bei E1 (also 225), was von „ISO 8859-1“ als „á“ interpretiert wird. Weiterhin ist es sinnvoll, dass bei mehreren hintereinander gestaffelten Systemen (bspw. Browser, Appserver und Datenbank), alle Systeme den gleichen Zeichensatz verwenden sollten, um derartige Verschiebungen zu vermeiden. Der gelesene Text wird also falsch dargestellt. Dies kann bspw. mit Notepad++ nachgestellt werden, indem beim Menüpunkt „Kodierung“ die entsprechenden Werte gewählt werden (Kodierung-> Zeichensatz -> Westeuropäisch). Gerade für Webanwendungen wird daher empfohlen auf ein Unicode Format zu gehen. Hier hat sich UTF 8 sehr weit verbreitet. (UTF8: Universal Character Set Transformation Format mit einer Staffelung von 8 Bits). Die ersten 128 Zeichen (also die 7 niederwertigsten Bits) werden 1:1 mit der ASCII Kodierung zugeordnet – wodurch UTF8 mit ASCII kompatibel ist. Insofern ist für alle ASCII kompatiblen Zeichen das oberste Bit des ersten Bytes immer 0. Steht es auf 1, so indiziert dies dem interpretierenden System, dass es mindestens ein weiteres Byte gibt, welches für die Kodierung verwendet wird. Dort ist wiederum hinterlegt, ob weitere Bytes existieren. Theoretisch kann ein UTF8 Zeichen mit bis zu 8 Byte codiert werden, wobei in der Praxis bis maximal 4 Bytes verwendet werden. Der große Vorteil von UTF8 ist, dass praktisch alle verfügbaren Zeichen abgebildet werden können. Nachteilig wirkt sich die Tatsache aus, dass alle Zeichen außerhalb des ASCII Zeichenraums auf jeden Fall mehr als ein Byte Speicherplatz benötigen. Wir nutzen in unseren Rechnern im Regelfall Latin 1 (bzw. ISO 8851-1), wobei für internationale Anwendungen UTF8 vorzuziehen ist – was die hohe Verbreitung von UTF8 bei Webseiten erklärt. 2.1 Sonderzeichen Alle Zeichen, welche in Texten vorhanden sind, müssen über das Charset definiert sein. Nun gibt es auch Zeichen, welche nicht zu einer Zeichendarstellung auf dem Bildschirm führen, sondern zur Steuerung der Ausgabe („nicht druckbare Zeichen“). Wir gehen an dieser Stelle lediglich auf den Zeilenumbruch ein – es sei aber darauf hingewiesen, dass es noch weitere „Steuerzeichen“ gibt (bspw. Tabulator). Der Zeilenumbruch hat eine lange Geschichte – schließlich waren die ersten Maschinen zur Textausgabe keine Computerbildschirme, sondern mechanische Schreibmaschinen. Bei diesen war es notwendig für einen Zeilenumbruch zwei Dinge zu tun – den Wagen zurückzuschieben (carriage return) und den Zeilenvorschub durchzuführen (line feed). Die Datenübermittlung via Fernschreiber war der nächste Schritt, wobei die mechanische Notwendigkeit von Wagenrücklauf und Zeilenvorschub immer noch vorhanden war. Für diese Steuerung benötigte man entsprechend zwei Steuerzeichen – CR (carriage return) und LF (line feed). Diese haben auch die Einführung der Computertechnik überlebt und finden sich somit immer noch in unseren Zeichensätzen. CR hat im ASCII Code den Wert „0D“ und LF den Wert „0A“ erhalten. Wenn wir mit Notepad++ eine Textdatei öffnen können wir diese Zeichen auch sichtbar machen. Seite 2 AnPr Textfiles lesen und schreiben Warum braucht der Computer denn immer noch zwei Zeichen für den Zeilenumbruch? Eines würde doch reichen! Antwort: Eigentlich reicht dem Computer auch ein Zeichen. In Unix und MacOS wird tatsächlich auch nur ein Zeichen, das LF verwendet (wobei bei älteren Macs bis zu einem gewissen Zeitpunkt nur CR verendet wurde). Bei Microsoft blieb man jedoch bei den beiden Zeichen CR LF. Dies bringt in der Tat einige Probleme mit sich. Wenn ich eine Datei in Unix erstelle und sie nach Windows kopiere, dann erkennt Windows den Zeilenumbruch nicht! Programme wie Notepad++ können dies aber korrigieren, indem unter Bearbeiten-> Format Zeilenende das gewünschte eingestellt werden kann. 3 Lesen von Text in Java Der Umgang mit Files wird in Java (wie auch in vielen anderen Programmiersprachen) mittels eigener Bibliotheksfunktionen erledigt. Diese liegen in Java meist unter java.io. Weiterhin bieten die meisten Programmierbibliotheken erweiterte Methoden an mit Files zu arbeiten, welche zum Teil die Schreibarbeit erheblich vereinfachen. An dieser Stelle wollen wir jedoch „nur“ die gebräuchlichsten Methoden verwenden mit dem Hinweis, dass es noch viele weitere Möglichkeiten gibt. 3.1 Nutzung des FileReaders Beginnen wir mit einer recht einfachen Methodik unter Verwendung des FileReaders. Dieser erlaubt es uns einzelne Zeichen aus einem File zu lesen und abzulegen. Hierzu müssen wir ein FileReader Objekt erst erzeugen, indem wir dem Konstruktor sagen, welches File zu öffnen ist. Anschließend lesen wir in einer Schleife Zeichen für Zeichen aus und geben es am Bildschirm aus. Die Zeichen werden allerdings nicht in einer char Variable übergeben, sondern in einer int Variable (das ist insofern praktisch, als dass die -1 geliefert werden kann, wenn das letzte Zeichen gelesen wurde). Sehen wir uns dies mal in einem Codebeispiel an: Wie wir sehen, müssen wir hier einige Dinge beachten. Wir müssen die Objekte erzeugen, müssen die entsprechenden Exceptions abfangen und vor allem müssen wir den Stream wieder schließen, was wir nach dem try/catch Block im „finally“ tun (finally wird immer durchlaufen, egal, ob eine Exception erfolgt oder nicht). Dies können wir aber nur, wenn in der Variablen „frMyReader“ tatsächlich ein Objekt liegt (also != null). Seite 3 Textfiles lesen und schreiben AnPr Warum liefert mir der FileReader nicht gleich die Zeichen als char Werte an? Dann könnte ich mir den Typecast von int zu char sparen! Antwort: Das Problem ist, dass der Leseprozess dem Aufrufer irgendwie mitteilen muss, dass er am Ende angekommen ist, was durch die Zahl -1 indiziert wird. Für diese gibt es aber keinen char Wert. Insofern werden die Zeichen als int übergeben und müssen danach in char umgewandelt werden, sofern es sich nicht um die -1 handelt. Soweit funktioniert das Programm nun. Trotzdem ist das einzelne Lesen von Zeichen als Zahl und die Konvertierung in char irgendwie nicht das Gelbe vom Ei. Hier gibt es durchaus noch sinnvolle Alternativen. 3.2 Vereinfachung mittels BufferedReader Die Standardantwort auf das Puffern von Zeichen ist der BufferedReader. Dieser „stülpt“ sich quasi über den FileReader und übernimmt den Zeichenstrom, damit wir über den Buffered Reader bequemer arbeiten können. Im Wesentlichen arbeitet der BufferedReader nun Zeilenweise und gibt uns pro Filezeile nun einen String zurück: Wie wir sehen ist der Code schon etwas schlanker geworden. In vielen Tutorials sieht man auch eine Kurzversion dieses Codes, indem der FileReader direkt im Konstruktor des BufferedReaders erzeugt wird: brMyReader = new BufferedReader(new FileReader("C:/tmp/myFile.txt"); Dies ist noch kürzer – wir müssen allerdings darauf achten, dass wir nun nicht mehr den FileReader schließen können, sondern den BufferedReader: try { brMyReader.close(); } catch (IOException e) { e.printStackTrace(); } Seite 4 AnPr Textfiles lesen und schreiben Das Schachteln der beiden Konstruktoren birgt aber die Gefahr, dass wenn bei der Erzeugung des BufferedReaders etwas schief läuft, wir keine Referenz mehr auf den FileReader haben und ihn nicht schließen können. Zum Glück tritt diese Situation aber praktisch nicht ein. Woher weiß Java eigentlich, welches Charset zu verwenden ist? Antwort: Java weiß es schlichtweg nicht – es wird das Standard Charset des Systems genutzt. Wenn wir einen anderes Charset verwenden wollen, dann müssen wir einen anderen Weg gehen. 3.3 UTF8 Codierungen lesen Das bisherige Verfahren lässt eine Flexibilität in puncto Charset vermissen. Um dies nun in unserem Reader auch zu berücksichtigen, müssen wir zwischen den Bytes und den Zeichen einen „Übersetzter“ einbauen, welcher flexibel konfigurierbar ist. Dies erledigt uns der „InputStreamReader“. Dieser ist jedoch nicht in der Lage auf das Filesystem direkt zuzugreifen. Also wird noch ein Objekt benötigt, welches in der Lage ist die Bytes vom Filesystem 1:1 zu laden. Hierfür verwenden wir den FileInputStream, der entsprechend den Pfad zur Datei erhalten muss. Sehen wir uns mal den Code an: Auch hier können die Konstruktoren direkt ineinander verschachtelt werden und nur den BufferedReader schließen – wieder mit dem potentiellen Problem, dass wir einen Zugriff auf ein nicht existierendes Objekt haben. In Unix ist der String für die Separierung von Ordnern das „/“ Zeichen – in Windows allerdings das „\“ Zeichen. Da Java ja auf beiden Betriebssystemen läuft – kommt es da nicht durcheinander? Antwort: Java ist in der Tat plattformübergreifend. Insofern kann der Pfadseparator tatsächlich problematisch werden. In aller Regel funktioniert aber das „/“ aber auch bei Windows. Wer auf Nummer sicher gehen will, baut die Pfade aus Einzelstrings auf und nutzt als Pfadseparator File.pathSeparator, dort findet sich immer der richtige. Seite 5 Textfiles lesen und schreiben 4 AnPr Schreiben von Text in Java Wie beim Lesen auch, gibt es beim Schreiben mehrere Möglichkeiten. An dieser Stelle werden wir „nur“ eine vorstellen, wer hier weitere Details wissen will, sei auf das Internet verwiesen (bspw. https://docs.oracle.com/javase/tutorial/essential/io/index.html). Die hier gezeigte Methodik ist prinzipiell der inverse Weg wie die Nutzung des InputStreamReades. Es werden über einen BufferedWriter die Strings an das File angehängt. Diese wiederum werden als Character Werte an den OutputStreamWriter geliefert, der wiederum verschiedene Charsets supported und die Bytes erzeugt. Die Kommunikation mit dem Filesystem wird von dem FileOutputStream übernommen. Drei Punkte sind hierbei „neu“. Zum einen kann man über einen boolean Parameter dem FileOutputStream mitteilen, dass er ein existierendes File „verlängern“ (true) oder überschreiben soll (false). Zum anderen kann der Zeilenumbruch über die Funktion „newLine()“ eingetragen werden, wodurch er bspw. bei Unix Systemen „CR“ ist und bei Windows „CR LF“ ist. Schließlich fällt uns noch der „flush“ Befehl auf, der dafür sorgt, dass die Daten in einem „Rutsch“ an das File gehängt werden. Seite 6 AnPr 5 Textfiles lesen und schreiben Lizenz Diese(s) Werk bzw. Inhalt von Maik Aicher (www.codeconcert.de) steht unter einer Creative Commons Namensnennung - Nicht-kommerziell - Weitergabe unter gleichen Bedingungen 3.0 Unported Lizenz. Seite 7