Textfiles lesen und schreiben

Werbung
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
Herunterladen