Letztes Update: 12.12.2016
Behandelte Befehle: loadStrings(), saveStrings(), char, String, charAt(), equals(), toLowerCase(), toUpperCase(), indexOf(), substring(), split(), JSONObject, saveJSONObject, loadJSONObject, XML, loadXML(), saveXML()

Bislang kennen Sie nur eine Art, Informationen zu speichern: Variablen. Diese Variablen werden allerdings allesamt gelöscht, sobald Sie auf den Stop-Button drücken! Wie merken Sie sich dann den aktuellen Spielstand eines Spiels oder die Adressen in einem digitalen Adressbuch?

Die Antwort ist: in Dateien auf der Festplatte. Oder vielleicht in der Cloud, also im Internet? Technisch gesehen ist der Unterschied nicht allzu groß. Wir beginnen mal mit Dateien.

Zunächst müssen wir aber unser Wissen um die Datentypen String und char etwas auffrischen.

12.1 String und char

(Siehe auch Kapitel 6.2)

Bevor wir komplexere Daten laden und speichern, sollten wir unser Wissen über Strings vertiefen. "String" ist englisch für "Schnur" oder "Kette". Das deutet darauf hin, dass ein String eine Aneinanderreihung von Zeichen (engl. character) ist. Man sagt deshalb auch Zeichenkette.

Datentyp char

In Processing gibt es auch einen Datentyp für einzelne Zeichen - dieser heißt char (für character). Sie können ein einzelnes Zeichen in einer Variable abspeichern. Das Zeichen wird immer mit einfachen Anführungszeichen umgeben:

char c1 = 'a';
char c2 = '5';
char c3 = '%';

Sie haben den Datentyp char schon kennen gelernt, als Sie Tastaturabfragen programmiert haben:

if (key == 'a') {
   ...
}
if (key == 's') {
   ...
}

Beachten Sie, dass der Datentyp char ein primitiver Datentyp ist, d.h. er wird klein geschrieben und bei einer Variablenzuweisung wird der Wert kopiert.

Datentyp String

Der Typ String wird groß geschrieben und ist eine Klasse, wie Sie sie auch selbst schreiben könnten. String wurde aber netterweise von den Entwicklern der Sprache Java geschrieben. Es ist nur eine von vielen Klassen, die in der Java-Standardbibliothek mitgeliefert werden und jedem in Java (und damit auch in Processing) zur Verfügung stehen.

Sie haben Strings in verschiedenen Kontexten gesehen, meistens als Ausgabe in println():

String message = "hallo";
println(message);
hallo

Sie haben auch gelernt, dass man Strings zusammenfügen kann. Dies nennt man String-Konkatenation und funktioniert mit dem normalen Plus-Zeichen:

String message = "hallo";
println(message + " bob");
hallo bob

Strings haben verschiedene Methoden, um z.B. die Länge des Strings zu bekommen, gibt es die Methode length(). Zu beachten: im Gegensatz zu Arrays ist dies eine Methode und benötigt daher Klammern:

String message = "hallo";
println(message.length());
5

Etwas gewöhnungsbedürftig, aber korrekt ist das Ausführen von Methoden auf dem String direkt, ohne dass eine Variable definiert wird:

println("hallo".length());
5

Da Strings aus Zeichen (character) zusammengesetzt sind, können Sie die einzelnen Zeichen auch rausziehen, und zwar mit der String-Methode charAt() :

String message = "hallo";

for (int i = 0; i < message.length(); i++) {
  println(message.charAt(i));
}
h
a
l
l
o

Wichtige String-Methoden (Processing)

In der Processing-Dokumentation können Sie wichtige String-Methoden nachschlagen, die auch hier erläutert sind. Es gibt allerdings auch viele Methoden, die Sie zwar verwenden können, die aber nicht in Processing dokumentiert sind. Für diese Methoden müssen Sie in der Java-Dokumentation nachschlagen - dazu kommen wir im Anschluss an diesen Abschnitt.

Die Methode equals() vergleicht zwei Strings und gibt true oder false zurück. Man beachte, dass Groß- und Kleinschreibung bedeutungsunterscheidend ist!

String message = "hallo";
String name = "bob";

if (message.equals("hallo")) {
  println("guten tag");
} else {
  println("wie bitte?");
}

println(name.equals("Bob"));
guten tag
false

Die Methoden toLowerCase() und toUpperCase() wandeln einen String in Kleinbuchstaben (lower) bzw. Großbuchenstaben (upper) um. Die Bezeichnung (case = engl. für Schublade) kommt von den Setzkästen, wo der obere für die Großbuchstaben, der untere für die Kleinbuchstaben bestimmt war.

String message = "Hallo";
String name = "Bob";

println(message.toLowerCase());
println(name.toUpperCase());
hallo
BOB

Tipp: Groß-/Kleinschreibung ignorieren. Wenn man feststellen möchte, ob zwei Strings "gleich" sind und dabei Groß-/Kleinschreibung ignorieren will, der verwende z.B. toLowerCase() wie folgt:

String message = "hAllo BOB";

if (message.toLowerCase().equals("hallo bob")) {
  println("erkannt");
} else {
  println("nicht erkannt");
}
erkannt

Wichtig zu verstehen ist hier, dass die Methode toLowerCase() wieder ein String-Objekt zurückgibt (nämlich den String in Kleinbuchstaben) und dass man auf diesem Objekt direkt weiterarbeiten kann: in unserem Fall mit equals().

Die Methode indexOf() erlaubt es, einen String a in einem anderen String b zu suchen. Wir schnuppern also ein wenig "Google"-Luft. Die Methode gibt eine Zahl zurück. Bei -1 wurde der String a nicht gefunden. Eine nicht-negative Zahl n besagt, dass String a an Stelle n in b beginnt.

	String a = "all";
	String b = "hallo";

	println(b.indexOf(a)); // suche a in b
1

Im Beispiel suche ich String a = "all" in String b = "hallo". Dann bekomme ich n = 1 zurück, weil "all" and Stelle 1 in "hallo" beginnt. Nicht vergessen, dass die erste Stelle bei Null beginnt.

String message = "Hallo Bob";

println(message.indexOf("foo"));
println(message.indexOf("Bob"));
println(message.indexOf("Ha"));
println(message.indexOf("ha"));
-1
6
0
-1

Auch hier ist Groß-/Kleinschreibung bedeutungsunterscheidend. Können Sie den Trick von oben anwenden, wenn Sie einen String durchsuchen wollen und dabei Groß-/Kleinschreibung ignorieren möchten?

Die Methode substring() erlaubt Ihnen das gezielte Zerschneiden eines Strings. Wollen Sie die Zahl aus dem String "Heidi (24)" herausschneiden, dann geben Sie die Startposition an, das wäre die 7, und die exklusive Endposition, d.h. die Position, die nicht mehr rausgeschnitten werden soll, hier also die 9.

String message = "Heidi (24)";

println(message.substring(7, 9)); // zweite Position exkl.
24

substring() gibt es auch mit einem Argument. Dann wird nur vorn abgeschnitten und nach hinten raus der ganze Rest genommen:

String message = "Hallo Bob";

println(message.substring(6)); // nur ein Argument
Bob

Wichtige String-Methoden (Java)

Weitere String-Methoden finden Sie in der Java-Dokumentation der Standardbibliothek. Schauen Sie sich diesen Link mal an, um einen Eindruck vom Umfang der Java-Standardbibliothek zu bekommen.

Die Dokumentation für die Klasse String enthält alle Methoden, die Sie verwenden können. Ich empfehle, die folgenden Methoden anzuschauen und auszuprobieren:

  • trim()
  • split()
  • replace()
  • startsWith()
  • endsWith()

Hier seien nur trim(), split() und startsWith() kurz erklärt.

Die Methode trim() erfernt alle Leerzeichen und Zeilenumbrüche, die ganz am Anfang oder ganz am Ende des Strings stehen. Dies ist oft notwendig, wenn Daten aus einem File rausgezogen werden sollen.

Beispiel:

String s = "    Harry ";
println("-->" + s.trim() + "<--");
-->Harry<--

Die Methode split() zerschneidet einen String anhand eines Trennzeichens und liefert einen Array mit den Einzelteilen zurück (Typ String[]). Allgemein formuliert:

STRINGVARIABLE.split("TRENNZEICHEN");

Zum Beispiel:

String x ="Mein Name ist Hase";
String[] woerter = x.split(" "); // trenne x nach Leerzeichen

Nehmen wir an, wir haben Koordinaten wie folgt in einem String vorliegen:

"x:105 y:-20 z:72"

Wir möchten die einzelnen Zahlen herausziehen. Zunächst müssen wir die drei Teile bekomman und zerschneiden dazu den String anhand des Trennzeichens " ".

String message = "x:105 y:-20 z:72";

String[] parts = message.split(" ");

for (int i = 0; i < parts.length; i++) {
  println(parts[i]);
}
x:105
y:-20
z:72

Wenn ich jetzt die Zahlen einlesen wollte, könnte ich mit Hilfe von indexOf() nach dem Doppelpunkt suchen und mit diesem Ergebnis und mit Hilfe von substring die Zahl abtrennen.

Die Methode startsWith() erlaubt Ihnen zu prüfen, ob ein String mit einem bestimmten Teilstring beginnt. Vielleicht möchten Sie in einer Liste von Namen (Strings) alle Namen rausfiltern, die mit "G" beginnen, oder alle Namen, die mit "Ha" beginnen.

Technisch funktioniert das wie folgt:

String s = "Harry";
boolean kommtVor = s.startsWith("Ha");

In unserem Fall bekämen wir also true zurück. Beachten Sie dass Groß-/Kleinschreibung berücksichtigt wird, d.h. s.startsWith("ha") würde false zurückliefern.

Übungsaufgaben

(a) Teilstring suchen und finden

Gegeben sei der String:

String str = "Hier ist die Info: James Bond";

Schreiben Sie Code, der "James Bond" auf der Konsole ausgibt. Der Code soll auch bei anderen Agenten funktionieren, z.B.:

String str = "Hier ist die Info: Spiderman";

Der Code soll auch funktionieren, wenn sich der Text vor dem Doppelpunkt ändert. Testen Sie im Anschluss Ihren Code mit:

String str = "      Codename:    James Bond  ";
Tipp

Suchen Sie nach dem Doppelpunkt. Verwenden Sie ansonsten indexOf(), substring() und trim().

Passen Sie auf, an welcher Stelle Sie trim() einsetzen!

(b) Suchen und ersetzen

Gegeben sei der String-Array:

String[] lines = {"Hello FOO", "Guten Tag, FOO",
                  "Bonjour FOO"};

Schreiben Sie Code, der alle Array-Elemente ausgibt, wobei das Wort "FOO" durch "James Bond" ersetzt werden soll.

Hinweis: Sie können die Aufgabe entweder mit replace oder mit den Funktionen indexOf/substring lösen. Ich empfehle, auf jeden Fall die Lösung mit indexOf/substring zu probieren.

Eine Alternativaufgabe ist, statt nur zu printen einen zweiten Array zu erstellen, wo - wie oben - alle FOO durch "James Bond" ersetzt sind.

Eine leichte Erweiterung der Aufgabe ist es, Sätze wie "Hey FOO, you will die!" aufzunehmen und dort FOO wie oben zu ersetzen (ohne das Satzende zu verlieren).

(c) Suchen und finden

Gegeben sei der String-Array:

String[] lines = {"This is it", "DO NOT DO THIS", "Why not",
                  "This is right", "That not", "What is this"};

Schreiben Sie Code, der alle Array-Elemente ausgibt, die mit "this" beginnen oder enden. Dabei soll Groß-/Kleinschreibung keine Rolle spielen.

Tipp
Verwenden Sie toLowerCase().

12.2 Dateien erzeugen und lesen

Eine Datei ist zunächst mal für unsere Zwecke ein Text, der auf der Festplatte unter einem Namen wie z.B. "foo.txt" gespeichert wird. Die Datei können Sie dann nicht nur mit Processing, sondern auch mit handelsüblichen Text-Editoren wie Notepad, Word etc. lesen und ändern.

Video: Text lesen & schreiben (6:25)

Daten schreiben

Das Schreiben von Daten nennt man auch save (engl. für Retten). Sie können mit der Methode saveStrings einen beliebig großen Array von Strings abspeichern. Dazu übergeben Sie der Funktion den Namen der Datei und den Array. Die Datei wird im Verzeichnis Ihres Processing-Programms gespeichert. Sie enthält auf jeder Zeile einen String. Im folgenden Beispiel wird eine Textdatei mit Namen "meineDaten.txt" mit drei Zeilen erzeugt:

// drei Zeilen speichern

String[] zeilen = new String[3];

zeilen[0] = "Das ist";
zeilen[1] = "ein Text";
zeilen[2] = "auf drei Zeilen";

saveStrings("meineDaten.txt", zeilen);

Wenn wir "foo.txt" mit einem Texteditor öffnen, dann sehen wir folgendes:

Das ist
ein Text
auf drei Zeilen

Daten lesen

Das Lesen von Daten nennt man auch load (engl. für Laden). Die Funktion loadStrings erwartet einen Dateinamen und liefert ein Array von Strings zurück, welches den Text in der Datei zeilenweise enthält.

Das Beispielprogramm liest eine Datei mit Namen "meineDaten.txt", die sich im Verzeichnis des Processing-Programms befinden muss, und schreibt den Inhalt zeilenweise auf die Konsole:

// Textdatei lesen

String[] lines = loadStrings("meineDaten.txt");

for (int i = 0; i < lines.length; i++) {
  println("> " + lines[i]);
}

Sie müssen nicht unbedingt die gleichen Daten einlesen wie die, die Sie oben erzeugt haben. Probieren Sie folgendes: Schreiben Sie einen Text mit einem Editor und laden Sie ihn mit obigen Programm (Dateiname anpassen!).

Daten im Web lesen

Heutzutage wird es immer selbstverständlicher, dass Daten nicht auf der Festplatte, sondern auch (oder aussschließlich) im Internet gespeichert werden. Man spricht dann auch davon, dass die Daten "in der Cloud" gespeichert sind.

Nehmen wir zum Beispiel Webseiten: Webseiten sind im Grunde Dateien mit Text. Dieser Text ist annotiert (engl. marked up) mit Informationen zu Struktur und Layout. Das ganze nennt man auch das HTML-Format (hypertext mark-up language).

Wenn wir in unserem Browser eine Adresse wie z.B. "http://hs-augsburg.de" eingeben, dann wird ein Server kontaktiert und die Datei "index.html" angefordert. Der Server (z.B. der Server der Hochschule Augsburg) schickt diese Datei zurück, die der Browser dann anhand der Annotationen schön gelayoutet darstellt.

Erstaunlicherweise funktioniert die Processing-Funktion loadStrings auch im Internet. Das heißt, man kann sich Daten von einem Server herunterladen und anschauen. Zum Bespiel fordern wir hier die HTML-Datei "index.hml" vom Server der Hochschule Augsburg an.

String[] data = loadStrings("http://hs-augsburg.de");
printArray(data);

Wir bekommen zurück (nur ein Ausschnitt):

[0] "<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">"
[1] "<html lang="de">"
[2] "	<head>	  "
[3] "<meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> "
[4] "<meta name="description" content="Hochschule Augsburg" />"
[5] "<meta name="keywords" content="Hochschule Augsburg, HS Augsburg, HSA, Hochschule, Hochschule Augsburg, Startseite Hochschule" />"
[6] "<meta name="language" content="de" />"
[7] "<title>Hochschule Augsburg</title>"
...

Im Abschnitt über JSON sehen Sie noch ein interessanteres Beispiel.

Übungsaufgaben

(a) Textdatei laden

Sie sollen eine gegebene Datei laden und auf der Konsole ausgeben.

Zunächst müssen Sie ein leeres Programm erstellen und speichern, damit Processing ein Verzeichnis für Ihr Programm erzeugt.

Starten Sie einen Text-Editor Ihrer Wahl (NotePad, WordPad, Atom, Emacs ...) und speichern Sie folgenden Text als Datei "data.txt" in das Programmverzeichnis:

Auf der Mauer,
auf der Lauer
sitzt ne kleine Wanze

Schreiben Sie anschließend Code (statischer Modus), der den Text einliest und auf der Konsole ausgibt.

Auf der Mauer,
auf der Lauer
sitzt ne kleine Wanze

(b) Zahlen in Array laden

Sie sollen wieder eine gegebene Datei laden. Diesmal handelt es sich um Zahlen, jeweils getrennt durch ein Semicolon.

25;1;-42;13;88

Schreiben Sie die Funtion loadToIntArray, die einen Dateinamen (String) bekommt und die in der entsprechenden Datei gespeicherten Zahlen als int-Array zurückgibt.

Testen Sie Ihre Funktion, indem Sie eine Datei "data.txt" mit den obigen Zahlen befüllen und dann folgenden Code ausführen:

void setup() {
  println(loadToIntArray("data.txt"));
}

Sie sollten sehen:

[0] 25
[1] 1
[2] -42
[3] 13
[4] 88

(c) Wörter zeilenweise speichern

Der Satz "Once upon a time in a galaxy far, far away." sei in einer Variablen mydata gespeichert. Ihre Aufgabe ist es, den Inhalt dieser Variable so in einer Datei zu speichern, dass in der Datei jedes Wort auf einer eigenen Zeile erscheint (Komma gehört zum vorigen Wort).

Verwenden Sie dazu die String-Methode split (s.o.).

(d) Einfacher Texteditor *

Schreiben Sie einen einfachen Texteditor mit einem Fenster der Größe 300x150 und 7 Zeilen.

Als Cursor verwenden Sie das Hashsymbol. Erlauben Sie nur einen eingeschränken Zeichensatz (z.B. nur 'a' bis 'z'). Die Cursortasten hoch/runter können zum Zeilenwechsel genutzt werden, der Cursor steht aber immer am Ende der Zeile. Mit der Backspace-Taste löschen Sie das letzte Zeichen der Zeile, wo der Cursor steht. Mit Enter kommen Sie in die nächste Zeile.

Hier ein interaktives Beispiel (Feld anklicken):

(Backspace funktioniert i.d.R. hier nicht, weil eine Browserfunktion damit verknüpft ist.)

Wenn Sie die Eingabe programmiert haben, implementieren Sie das Speichern und Laden Ihres Textes.

Tipp
Speichern Sie den Text in einem Array von Strings. Merken Sie sich die aktuelle Zeile. Das Zeichen für den Cursor (Hash) hängen Sie beim Ausgeben des Textes im Grafikfenster einfach immer an die aktuelle Zeile hinten an.

12.3 Daten und Objekte (CSV)

Inwiefern helfen mir Dateien, wenn ich in einem Spiel den Punktestand und die Position der Spielerfigur und der gegnerischen Figuren etc. abspeichern will? Wir sehen uns dazu Beispiele an.

Da wir in Dateien mit Texten arbeiten, müssen wir dafür sorgen, dass z.B. numerische Daten beim Schreiben in Text umgewandelt werden. Umgekehrt, wenn wir den Text wieder lesen, muss der Text wieder in eine Zahl umgewandelt werden.

Objekt speichern

Als Beispiel schauen wir uns einen Ball an, der umherfliegt und von den Wänden abprallt:

Als Code:

int x;
int y;
int xspeed;
int yspeed;

void setup() {
  size(200, 200);
  x = width/2;
  y = height/2;
  xspeed = (int)random(1, 2);
  yspeed = (int)random(2, 4);
}

void draw() {
  background(100);
  noStroke();
  fill(255);

  ellipse(x, y, 20, 20);
  x += xspeed;
  y += yspeed;

  if (x < 0 || x > width) {
    xspeed = -xspeed;
  }
  if (y < 0 || y > height) {
    yspeed = -yspeed;
  }
}

Die Frage, die man sich stellen muss, wenn man ein Objekt speichern und später wieder laden will, lautet: Welches sind die Eigenschaften, die das Objekt beschreiben? Unser Ball wird beschrieben durch vier Integer-Werte: x-Position, y-Position, x-Geschwindigkeit, y-Geschwindigkeit. Diese Werte wollen wir speichern. Das machen wir ganz einfach, indem wir jeden Wert auf eine Zeile schreiben:

void saveConf() {
  String[] data = new String[4];

  // Zahlen werden hier in Strings
  // umgewandelt:
  data[0] = "" + x; // Kein Leerzeichen einbauen!!!
  data[1] = "" + y;
  data[2] = "" + xspeed;
  data[3] = "" + yspeed;

  saveStrings("conf.txt", data);
}

Beachten Sie, dass wir die Zahlen in Strings umwandeln, indem wir die doppelten Quotes voranstellen. Dann wird der String-Array mit saveStrings() gespeichert.

Vorsicht: Setzen Sie kein Leerzeichen zwischen die Quotes, sonst gibt es Probleme beim Einlesen, da Processing eine Zahl mit führendem Leerzeichen nicht als Zahl erkennt.

Jetzt brauchen wir eine Funktion, die die Daten aus der Datei wieder ausliest. Hier verwenden wir die Processing-Funktion int(), um aus einem String (z.B. in data[0], wo die erste Zeile steht) eine Integer-Zahl zu machen:

void loadConf() {
  String[] data = loadStrings("conf.txt");

  // Strings jeweils in Zahlen umwandeln
  x = int(data[0]);
  y = int(data[1]);
  xspeed = int(data[2]);
  yspeed = int(data[3]);
}

Um das ganze auszuprobieren, bauen wir eine Tastatursteuerung ein, die die aktuelle Ball-Konfiguration mit "s" speichert und mit "l" wieder lädt.

void keyPressed() {
  if (key == 's') {
    saveConf();
  }
  if (key == 'l') {
    loadConf();
  }
}

Sie können jetzt jederzeit die aktuelle Position mit "s" speichern. Sobald Sie "l" drücken, springt der Ball an die gespeicherte Position zurück.

Sehen Sie sich auch die Datei "conf.txt" auf Ihrer Festplatte an.

Mehrere Objekte speichern (CSV-Format)

Jetzt wollen wir mehrere Bälle abspeichern. Dazu verwenden wir die Klasse Ball aus Kap. 12:

// Klasse Ball (in eigenem Reiter "Ball")
class Ball
{
  float xpos;
  float ypos;
  float xspeed;
  float yspeed;

  // Konstruktor
  // erzeugt Ball an zufälliger Position
  // mit zufälliger Geschwindigkeit
  Ball()
  {
    xpos = random(10, width-10);
    ypos = random(10, height-10);
    xspeed = random(0,3);
    yspeed = random(0,3);
  }

  // Methode zum Zeichnen
  void draw() {
    noStroke();
    ellipse(xpos, ypos, 20, 20);
  }

  // Methode zum Bewegen, inkl. Kollision
  void move() {
    xpos += xspeed;
    ypos += yspeed;

    if (xpos < 10 || xpos > width-10) {
      xspeed = -xspeed;
    }
    if (ypos < 10 || ypos > height-10) {
      yspeed = -yspeed;
    }
  }
}

Wir erzeugen einen Array mit mehreren Bällen:

// Hauptprogramm
Ball[] balls = new Ball[5]; // neues Array

void setup() {
  // Array befüllen mit Objekten
  for (int i = 0; i < balls.length; i++) {
    balls[i] = new Ball();
  }
}

void draw() {
  background(100);

  // Funktionen der Objekte regelmäßig aufrufen
  for (int i = 0; i < balls.length; i++) {
    balls[i].draw();
        balls[i].move();
  }
}

Zum Speichern mehrerer Bälle speichern wir nicht jede Information (Position, Geschwindigkeit) auf einer eigenen Zeile. Stattdessen speichern wir alle Informationen für einen Ball auf einer einzigen Zeile. Jede Zeile sieht dann so aus:

x-Position,y-Position,x-Geschwindigkeit,y-Geschwindigkeit

Dieses Format nennt man auch comma separated values oder kurz CSV. Die Datei lässt man entsprechend auf .csv enden. Wir schreiben eine Methode save, die unsere Bälle zeilenweise speichert. Dazu müssen wir einen neuen Array erzeugen, den wir dann in saveStrings() verwenden können.

void save() {
  String[] output = new String[balls.length];
  for (int i = 0; i < output.length; i++) {
    output[i] = balls[i].xpos + "," + balls[i].ypos + "," +
    balls[i].xspeed + "," + balls[i].yspeed;
  }
  saveStrings("conf.csv", output);
}

Wir hängen das ganze an die Taste 's':

void keyPressed() {
  if (key == 's') {
    save();
  }
}

Wenn wir das Programm starten, die Taste s drücken und anschließend in "conf.csv" hineinschauen, könnte das z.B. wie folgt aussehen. Man beachte, dass wir es hier mit float-Werten zu tun haben. Zum Glück verwenden die Amerikaner einen Punkt für float-Werte, so dass unser CSV-Format nicht durcheinander gerät:

66.716225,79.98076,-2.7014487,-0.539416
46.550686,56.259285,0.14038557,2.3454807
80.12446,57.08667,-2.5058165,-2.9651637
53.829018,90.70273,-1.2547593,-2.2724154
71.8666,49.218903,-2.9265237,-1.0065991

Das Laden der Ballpositionen programmieren Sie als Übung.

Übungsaufgaben

(a) Vektor speichern/laden

Gegeben sei folgendes Programm, das einen Ball an eine Zufallsposition hängt und bei jedem Mausklick an eine neue Zufallsposition bringt:

PVector foo = new PVector();

void setup() {
  foo.x = random(0, width);
  foo.y = random(0, height);
}

void draw() {
  background(255);
  ellipse(foo.x, foo.y, 20, 20);
}

void mousePressed() {
  foo.x = random(0, width);
  foo.y = random(0, height);
}

Schreiben Sie eine Funktion save(), die den Vektor in einer Datei "position.txt" speichert. Sie dürfen hier (ausnahmsweise) auf die globale Variable foo zugreifen. Die Funktion soll mit Taste 's' aufgerufen werden.

Schreiben Sie eine Funktion load(), die diese Datei einliest und die Variable foo entsprechend setzt. ie Funktion soll mit Taste 'l' aufgerufen werden.

Sie erhalten ein Programm, mit dem Sie die aktuelle Position speichern und später wiederherstellen können.

Versuchen Sie, das Programm mit dem CSV-Format zu realisieren, d.h. speichern Sie alle Informationen auf einer einzigen Zeile.

(b) Bälle laden

Kopieren Sie sich den Code vom Unterkapitel "Mehrere Objekte speichern (CSV-Format)".

Schreiben Sie analog zur bestehenden Funktion save() eine Funktion load(), die die Datei "conf.csv" lädt und die Bälle entsprechend an die gespeicherten Positionen setzt (und die gespeicherten Geschwindigkeiten verwendet).

Hinweise: Dazu benötigen Sie die String-Methode "split()" (s.u.). Um einen String x in einen float-Wert umzuwandeln, schreiben Sie einfach float(x).

(c) Autos speichern und laden

Verwenden Sie die Klasse Auto. Erzeugen Sie einen Array von 5 Autos und speichern Sie diesen im CSV-Format.

class Auto {
  String firma;
  int kilometerstand;
  float verbrauchProKm;

  Auto(String f, int km, float verbrauch) {
    firma = f;
    kilometerstand = km;
    verbrauchProKm = verbrauch;
  }
}

Schreiben Sie ein zweites Programm, dass diese Autos von einer Datei einliest und auf der Konsole ausgibt.

(d) Statistik laden, parsen und darstellen

Das statistische Bundesamt stellt für nicht-kommerzielle Zwecke eine Reihe von interessanten Umfragedaten kostenfrei bereit. In dieser Aufgabe beschäftigen wir uns mit einer Statistik zu den Studierendenzahlen an deutschen Hochschulen.

Laden Sie sich die Datei hochschulen.csv herunter und öffnen Sie sie in einem Texteditor (z.B. Wordpad, Word, TextEdit). Hier ein Ausschnitt (gekürzt!):

...

;Deutsche;Deutsche;Deutsche;Ausländer;Ausländer;Ausländer
;männlich;weiblich;Insgesamt;männlich;weiblich;Insgesamt
WS 1998/99;907403;727254;1634657;92321;73673;165994
WS 1999/00;872178;723246;1595424;95460;79605;175065
WS 2000/01;870016;741820;1611836;99906;87121;187027

...

Sie haben hier eine Tabelle mit den Studierendenzahlen, geteilt nach männlich, weiblich, Ausländer etc. Wir interessieren uns nur für die Gesamtzahl in der letzten Spalte (ist oben rausgekürzt...).

Lesen Sie die Daten mit Processing ein und stellen Sie die Daten als Kurve da, z.B. die Gesamtzahl der Studierenden. Beachten Sie, dass die Werte mit ";" getrennt sind und nicht mit Komma. Sie finden die Daten in der letzten Spalte.

Hinweis: Sie finden Informationen zu split() und startsWith() hier im Kapitel unter 12.3. unter "Wichtige String-Methoden (Java)".

Tipps zum Vorgehen:

  1. Sie können alles im statischen Modus programmieren.
  2. Versuchen Sie zunächst, die Datei einzulesen und alle "relevanten" Zeilen auf der Konsole auszugeben (beginnen mit "WS"), schauen Sie sich dazu die String-Methode startsWith() an (Kap. 12.3)
  3. Anschließend probieren Sie, die letzte Spalte der Tabelle (Studierende insgesamt) auf der Konsole auszugeben. Verwenden Sie hier split()
  4. Beim Zeichnen versuchen Sie zunächst, nur Punkte zu malen. Sehen Sie sich für die Darstellung die Funktion map() an (Processing-Referenz).

Die Daten stammen von der GENESIS-Online-Datenbank, Copyright Statistisches Bundesamt, Wiesbaden.

12.4 Dateiformat JSON

Das Format JSON ist besonders im Web-Bereich im Zusammenhang mit der Programmiersprache JavaScript extrem weit verbreitet und mittlerweile fast eine Art Standard. JSON steht für JavaScript Object Notation.

Das JSON-Format ist relativ leicht zu verstehen und eignet sich gut für Daten, die in Objekten oder Arrays gespeichert werden (oder auch in Arrays von Objekten). Im Vergleich zu dem XML-Format (siehe nächsten Abschnitt) ist JSON weniger sperrig, also (für Menschen) leichter zu lesen und zu schreiben.

JSON-Format

Eine beispielhafte JSON-Datei könnte so aussehen:

{
  "Spieler": "Harry",
  "Codename": "Terminator",
  "Punkte": 551
}

Wie man sieht, speichert man Daten in Form von Attributen (z.B. "Spieler") und zugehörigen Werten (z.B. "Harry"). Man spricht auch von Attribut-Wert-Paaren. Die gesammelten Attribut-Wert-Paare zwischen den geschweiften Klammern bilden ein Objekt.

Natürlich kann man auch mehrere Objekte speichern. Man spricht dann von einem Array; dieser wird durch eckige Klammern gekennzeichnet:

[
  {
    "Spieler": "Harry",
    "Codename": "Terminator",
    "Punkte": 551
  },
  {
    "Spieler": "Sally",
    "Codename": "Wonderwoman",
    "Punkte": 83461
  }
]

Sowohl Objekte als auch Arrays können miteinander verschachtelt werden. Zum Beispiel hier ein Array als Wert eines Attributs:

{
  "Spieler": "Harry",
  "Codenamen": [
    { "name": "Terminator" },
    { "name": "Seppl" },
    { "name": "Yoda" }
  ]
  "Punkte": 551
}

Oder hier ein Objekt als Wert eines Attributs:

{
  "Spieler": "Harry",
  "Codename": "Terminator",
  "Spielstand": {
    "Welt": "Magic Castle",
    "Punkte": 551,
    "Lebensenergie": 8
  }
}

In unseren Beispielen werden wir uns auf einfache, nicht-verschachtelte Fälle beschränken.

JSON speichern und laden

Wieder schauen wir uns ein Ball-Programm an:

float x = 50;
float y = 50;
float xspeed = 1.5;
float yspeed = 1;

void draw() {
  background(0);
  ellipse(x, y, 20, 20);
  x += xspeed;
  y += yspeed;

  if (x < 0 || x < width) {
    xspeed = -xspeed;
  }
  if (y < 0 || y < height) {
    yspeed = -yspeed;
  }
}

Speichern

Um den Ball mit seiner Bewegung zu speichern, müssen wir vier Werte für Position und Geschwindigkeit speichern. Wir tun dies in einer eigenen Funktion save(). Dort müssen wir zunächst ein neues Objekt vom Typ JSONObject anlegen:

void save() {
  JSONObject data = new JSONObject();
  ...
}

In diesem Objekt können wir beliebige Attribute und zugehörige Werte festlegen. Dazu gibt es eine Reihe von Funktionen, je nach Datentyp des Werts: setInt, setFloat, setBoolean und setString. Wir setzen vier Attribute und speichern anschließend das JSON-Objekt in einer Datei.

void save() {
  JSONObject data = new JSONObject();
  data.setFloat("x", x);
  data.setFloat("y", y);
  data.setFloat("xspeed", xspeed);
  data.setFloat("yspeed", yspeed);
  saveJSONObject(data, "ball.json");
}

Jetzt müssen wir noch das save() auslösen, z.B. per Tastendruck:

void keyPressed() {
  if (key == 's') {
    save();
  }
}

In der Datei "ball.json", die wir im gleichen Verzeichnis wie unser Processing-Programm finden sollten, sehen wir jetzt z.B.

{
  "xspeed": -1.5,
  "x": 44,
  "y": 12,
  "yspeed": -1
}

Laden

Die Daten zu laden bedeutet, dass die vier Variablen für den Zustand des Balls über die Werte in der Datei "ball.json" neu gesetzt werden.

Dazu schreiben wir die Funktion load und lesen die Datei "ball.json". Das Lesen gibt uns ein befülltes JSON-Objekt zurück. Das ist sehr praktisch im Vergleich zum Einlesen eines Texts wie oben, da wir nicht die Attribute rausfiltern müssen.

void load() {
  JSONObject data = loadJSONObject("ball.json");
  ...
}

Dieses Objekt können wir jetzt befragen und die entsprechenden Werte in die Variablen stecken. Dafür gibt es wieder eine Reihe von Methoden, je nachdem, welchen Datentyp man lesen möchte (getInt, getFloat, getBoolean, getString).

void load() {
  JSONObject data = loadJSONObject("ball.json");
  x = data.getFloat("x");
  y = data.getFloat("y");
  xspeed = data.getFloat("xspeed");
  yspeed = data.getFloat("yspeed");
}

Zum Testen brauchen wir wieder die Kontrolle über Tasten:

void keyPressed() {
  if (key == 's') {
    save();
  }
  if (key == 'l') {
    load();
  }
}

JSON mit Arrays

Um Arrays von Daten zu speichern und zu lesen benötigen Sie nicht nur Objekte vom Typ JSONObject, sondern auch Objekte vom Typ JSONArray.

Wenn wir als Beispiel eine Textdatei mit Namen "notenliste.json" ansehen. Sie enthält eine Reihe von JSON-Objekten, jeweils mit Name und Note.

[
  {
    "name" : "Susi",
    "note" : 1.3
  },
  {
    "name" : "Tobi",
    "note" : 4
  },
  {
    "name" : "Klaus",
    "note" : 1.0
  },
  {
    "name" : "Lea",
    "note" : 2.3
  },
  {
    "name" : "Jessi",
    "note" : 1.7
  }
]

Wir können die Datei als "Array" von JSON-Objekten einlesen.

JSONArray dataArray = loadJSONArray("notenliste.json");

Diese Objekt ist kein Array im Sinne der Sprache Java/Processing, sonst müssten wir schreiben "JSONObject[]". Stattdessen haben wir hier ein Objekt, das einem Java-Array ähnlich ist: wir können es befragen, wie viele Elemente es enthält (Methode size) und wir können ein einzelnes Element mit Hilfe der Indexzahl und der Methode getJSONObject() bekommen:

for (int i = 0; i < dataArray.size(); i++) {
  JSONObject data = dataArray.getJSONObject(i);
  ...
}

Jetzt, wo wir das jeweilige JSON-Objekt in der Schleife haben, können wir z.B. Name und Note ausgeben:

JSONArray dataArray = loadJSONArray("notenliste.json");

for (int i = 0; i < dataArray.size(); i++) {
  JSONObject data = dataArray.getJSONObject(i);
  String name = data.getString("name");
  float note = data.getFloat("note");
  println(name + " hat Note " + note);
}
Susi hat Note 1.3
Tobi hat Note 4.0
Klaus hat Note 1.0
Lea hat Note 2.3
Jessi hat Note 1.7

Mit der Kombination aus JSON-Objekten und JSON-Arrays können Sie beliebig komplexe Daten speichern und laden.

JSON und Objekte

Wenn man eine Klasse hat, kann man sich ziemlich direkt vorstellen, wie eine JSON-Repräsentation eines Objekts auszusehen hat. Nehmen wir diese Klasse:

class Produkt {
  int laufnummer;
  String bezeichnung;
  float preis;
  boolean verfuegbar;
}

In JSON sollte das wohl so aussehen:

{
  "laufnummer": 31,
  "bezeichnung": "SD-Speicherkarte 32 GB",
  "preis": 10.99,
  "verfuegbar": true
}

Um Objekte vom Typ Produkt zu speichern, können wir innerhalb der Klasse eine Methode anlegen, die ein JSON-Objekt erzeugt (wir haben hier außerdem noch einen Konstruktor angelegt):

class Produkt {
  int laufnummer;
  String bezeichnung;
  float preis;
  boolean verfuegbar;

  Produkt(int ln, String bez, float pr, boolean vf) {
    laufnummer = ln;
    bezeichnung = bez;
    preis = pr;
    verfuegbar = vf;
  }

  JSONObject getJSON() {
    JSONObject json = new JSONObject();
    json.setInt("laufnummer", laufnummer);
    json.setString("bezeichnung", bezeichnung);
    json.setFloat("preis", preis);
    json.setBoolean("verfuegbar", verfuegbar);
    return json;
  }
}

Um ein Objekt zu speichern, müssen wir lediglich saveJSONObject zusammen mit getJSON verwenden.

void setup() {
  Produkt p = new Produkt(31, "SD-Speicherkarte 32 GB",
	            10.99, true);
  saveJSONObject(p.getJSON(), "produkt.json");
}

Die entsprechende Datei "produkt.json" sieht so aus:

{
  "preis": 10.989999771118164,
  "bezeichnung": "SD-Speicherkarte 32 GB",
  "laufnummer": 31,
  "verfuegbar": true
}

Anmerkung: An diesem Beispiel sehen Sie auch, warum man nie float (oder double) verwenden sollte, um Geld zu repräsentieren: es treten immer wieder Rundungsfehler auf. In diesem Artikel (Englisch) steht, wie man es richtig macht.

Abschließend zum Thema Objekte und JSON kann man sagen, dass man Java-Objekte fast 1-zu-1 auf das JSON-Format abbilden kann (und sollte). In unserem Beispiel haben wir eine Methode getJSON in die Klasse eingebettet, wo direkt in der Klasse das JSON-Objekt erzeugt wird.

Analog könnte man für das Laden eine Methode initWithJSON schreiben, die ein JSON-Objekt bekommt und das Java-Objekt mit den dort enthaltenen Werten befüllt.

JSON im Web

Genauso wie Sie Text im Web lesen können, können Sie JSON im Web mit der Funktion loadJSONObject beziehen. Da JSON im Web das Standardformat für den Datenausstausch ist, können Sie verschiedene Dienste nutzen, die Ihre Daten im JSON-Format bereitstellen.

Eine spezielle Form der Datenbereitsstellung im Web funktioniert so, dass Sie über die URL eine Anfrage an einen Server mit bestimmten Parametern schicken. Zurück kommt keine HTML-Seite, sondern Daten. Diese Daten können z.B. im JSON-Format eingerichtet sein (oft auch im XML-Format). Solche Dienste gibt es für das Wetter, für Karten, Verkehrsinfos und vieles mehr. Leider erfordern die meisten Dienste einen sog. Schlüssel, den man vorab einholen muss. Der Dienst wird schließlich i.d.R. verkauft und der Schlüssel definiert, wer die Rechnung bekommt.

Ein Beispiel für einen komplett freien Dienst ist die Open Movie Database (www.omdbapi.com/). Hier können Sie Informationen zu Kinofilmen einholen. Der Titel des Kinofilms wird als Parameter "t" direkt in der URL definiert. Probieren Sie das gern in der Adresszeile Ihres Browsers aus. Hier z.B. für den Film "Minions":

http://www.omdbapi.com/?t=minions

Sie erhalten zurück:

{"Title":"Minions","Year":"2015","Rated":"PG",
"Released":"10 Jul 2015","Runtime":"91 min",
"Genre":"Animation, Action, Adventure",
"Director":"Kyle Balda, Pierre Coffin",
"Writer":"Brian Lynch","Actors":"Sandra Bullock, Jon Hamm, Michael Keaton, Allison Janney",
"Plot":"Minions Stuart, Kevin and Bob are recruited by Scarlet Overkill, a super-villain who, alongside her inventor husband Herb, hatches a plot to take over the world.","Language":"English, Spanish","Country":"USA","Awards":"Nominated for 1 BAFTA Film Award. Another 1 win & 18 nominations.",
"Poster":"https://images-na.ssl-images-amazon.com/images/M/MV5BMTg2MTMyMzU0M15BMl5BanBnXkFtZTgwOTU3ODk4NTE@._V1_SX300.jpg",
"Metascore":"56","imdbRating":"6.4","imdbVotes":"150,001","imdbID":"tt2293640","Type":"movie","Response":"True"}

Wenn Sie genau hinsehen, erkennen Sie, dass es sich um ein Objekt im JSON-Format handelt!

Bei der Parameterübergabe müssen Sie noch beachten, dass keine Leerzeichen vorkommen dürfen. Stattdessen verwenden Sie das Pluszeichen. Hier sehen Sie außerdem, wie man mehrere Parameter definiert. Man trennt Parameter mit einem &-Zeichen. Bei der folgenden Anfrage geben wir an, dass wir eine ausführliche Plot-Beschreibung haben möchten (plot=full) und dass wir unbedingt das JSON-Format für die Rückgabe wollen (r=json).

http://www.omdbapi.com/?t=hunger+games&y=&plot=full&r=json

Wir können jetzt die Informationen auch von Processing aus laden und auslesen.

JSONObject data =
loadJSONObject("http://www.omdbapi.com/?t=hunger+games&y=&plot=full&r=json");

println("Movie: " + data.getString("Title"));
println("\n" + data.getString("Plot"));
Movie: The Hunger Games

Katniss Everdeen voluntarily takes her younger sister's
place in the Hunger Games, a televised competition
in which two teenagers from each of the twelve
Districts of Panem are chosen at random to fight
to the death.

Übungsaufgaben

(a) Zeichnen

Erstellen Sie ein neues Processing-Programm, speichern Sie es und erstellen Sie im Programmverzeichnis die JSON-Datei "config.json" mit:

{
  "x": 75,
  "y": 25,
  "form": "kreis"
}

Zeichnen Sie an den in der Datei angegebenen Koordinaten ein Kreis oder Rechteck, je nachdem, was in "form" steht ("kreis" oder "rechteck"). Sie können das im statischen Modus umsetzen.

Um Ihr Programm zu testen, ändern Sie die Koordinaten und/oder Form in der Textdatei mit einem Editor und starten Sie Ihr Programm neu.

(b) Auto speichern und laden

Verwenden Sie die Klasse Auto. Schreiben Sie die Funktion saveAuto, die ein Auto-Objekt und einen Dateinamen bekommt, und das Objekt als JSON speichert. Schreiben Sie die Funktion loadAuto, die einen Dateinamen bekommt und ein Auto-Objekt zurückgibt.

class Auto {
  String firma;
  int kilometerstand;
  float verbrauchProKm;

  Auto(String f, int km, float verbrauch) {
    firma = f;
    kilometerstand = km;
    verbrauchProKm = verbrauch;
  }
}

Überlegen Sie sich, wie Sie Ihren Code testen.

(c) Bälle speichern und laden

Kopieren Sie sich den Code vom Unterkapitel "Mehrere Objekte speichern (CSV-Format)". Setzen Sie die Funktionen load und save so um, dass eine JSON-Datei gespeichert wird.

12.5 Dateiformat XML [optional]

Wir haben gesehen, dass man mehrere Objekte zeilenweise speichern könnte, wobei die Daten innerhalb einer Zeile mit Komma getrennt werden - jedenfalls im CSV-Format. Unser Beispiel sah so aus:

66.716225,79.98076,-2.7014487,-0.539416
46.550686,56.259285,0.14038557,2.3454807
80.12446,57.08667,-2.5058165,-2.9651637
53.829018,90.70273,-1.2547593,-2.2724154
71.8666,49.218903,-2.9265237,-1.0065991

Aber was bedeuten die Zahlen? Um das herauszufinden, müssten wir uns den Code ansehen. In der Praxis zeigt sich immer wieder, dass es Vorteile hat, wenn die gespeicherten Informationen nicht nur maschinenlesbar, sondern auch menschenlesbar sind.

Ein Schritt zur besseren Lesbarkeit ist es, die Informationen zu markieren (engl. mark-up):

x:66.716225, y:79.98076, xspeed:-2.7014487, yspeed:-0.539416
x:46.550686, y:56.259285, xspeed:0.14038557, yspeed:2.3454807
x:80.12446, y:57.08667, xspeed:-2.5058165, yspeed:-2.9651637
x:53.829018, y:90.70273, xspeed:-1.2547593, yspeed:-2.2724154
x:71.8666, y:49.218903, xspeed:-2.9265237, yspeed:-1.0065991

Eventuell ist sogar die Reihenfolge der Information innerhalb der Zeile egal, da ja jede Information markiert ist. Dies wird aber unterschiedlich gehandhabt. Oft ist die Reihenfolge trotz Markierung fix (da leichter zu programmieren).

Der Nachteil ist direkt offensichtlich. Erstens benötigt die Datei mehr Speicherplatz und zweitens ist die Programmierung der Einleseroutine aufwändiger, da die Markierung rausgefiltert werden muss, Leerzeichen gelöscht werden müssen und evtl. auch mit einer flexiblen Reihenfolge umgegangen werden muss.

XML-Elemente

Das XML-Format ist eine bestimmte Systematik, mit der man Markierungen vornimmt. XML steht für eXtensible Mark-up Language. Hier die ersten zwei Zeilen unseres Beispiels in einem XML-Format:

<gamedata>
  <ball>
    <x>66.716225</x>
    <y>79.98076</y>
    <xspeed>-2.7014487</xspeed>
    <yspeed>-0.539416</yspeed>
  </ball>
  <ball>
    <x>46.550686</x>
    <y>56.259285</y>
    <xspeed>0.14038557</xspeed>
    <yspeed>2.3454807</yspeed>
  </ball>
</gamedata>

In XML werden Information markiert, indem Sie eingeklammert werden von einem Start-Tag und einem End-Tag ("Tag" wird englisch betont wie in Hashtag). Der End-Tag wird mit einem Schrägstrich gekennzeichnet:

<x>66.716225</x>

Das, was zwischen den Tags steht, ist der Inhalt, hier also die Zahl:

<x>66.716225</x>

Die beiden Tags und ihr Inhalt bilden zusammen ein Element. Elemente können verschachtelt sein, so wie hier, wo das ball-Element 4 Elemente enthält:

<ball>
  <x>66.716225</x>
  <y>79.98076</y>
  <xspeed>-2.7014487</xspeed>
  <yspeed>-0.539416</yspeed>
</ball>

Oder das gamedata-Element, das 2 Elemente enthält:

<gamedata>
  <ball>
    <x>66.716225</x>
    <y>79.98076</y>
    <xspeed>-2.7014487</xspeed>
    <yspeed>-0.539416</yspeed>
  </ball>
  <ball>
    <x>46.550686</x>
    <y>56.259285</y>
    <xspeed>0.14038557</xspeed>
    <yspeed>2.3454807</yspeed>
  </ball>
</gamedata>

Die Elemente bilden eine Baumstruktur mit dem gamedata-Element als Wurzel und den 2 ball-Elementen als Kindknoten, welche wiederum die x/y/xspeed/yspeed-Elemente als Kinder (und Blätter) haben.

Wichtig ist, dass es in XML immer genau ein Wurzelelement geben muss.

Natürlich wird die Datei jetzt noch einmal wesentlich größer, jedoch gewinnt man eine hohe Lesbarkeit. Aber erinnern wir uns an den Nachteil, dass das Programmieren von Einleseroutinen jetzt sehr komplex wird. Dies wird durch die Tatsache, dass XML ein weltweiter Standard ist, behoben. Es gibt für praktisch alle bekannte Programmiersprachen XML-Bibliotheken, also vorgefertigte Klassen und Funktionen, die das Lesen, Schreiben und eine Korrektheitsprüfung von XML-Daten erlauben.

Attribute, leere Elemente, Kommentare

Informationen können von Tags eingeklammert sein, sie können aber auch in den Start-Tag als Attribute eingeschrieben werden. Das sieht dann so aus:

<ball color="FF0000">
  <x>66.716225</x>
  <y>79.98076</y>
  <xspeed>-2.7014487</xspeed>
  <yspeed>-0.539416</yspeed>
</ball>

Im Grunde könnten wir auch alle Unterelemente von ball in Attribute stecken. Dies ist eine reine Design-Entscheidung. Hier ein Beispiel nur mit x und y:

<ball x="66.716225" y="79.98076">
</ball>

Im obigen Fall ist gar keine Information zwischen Start-Tag und End-Tag, es handelt sich um ein leeres Element, so dass man den End-Tag weglassen könnte. Man hat also ein Element mit nur einem Tag. Diesen Tag muss man dann mit einem Schrägstrich am Ende kennzeichnen:

<ball x="66.716225" y="79.98076" />

Soll ein Teil der Datei ignoriert werden, so kann man ihn auskommentieren. Kommetare werden mit <!-- und --> eingeklammert und können sich über mehrere Zeilen erstrecken:

<!-- Automatisch generierte Datei -->
<ball>
  <x>66.716225</x>
  <y>79.98076</y>
  <xspeed>-2.7014487</xspeed>
  <yspeed>-0.539416</yspeed>
</ball> <!-- Kommentar
    auf mehreren
Zeilen -->
<ball>
  <x>46.550686</x>
  <y>56.259285</y>
  <xspeed>0.14038557</xspeed>
  <yspeed>2.3454807</yspeed>
</ball>
<!-- Fertig -->

XML in Processing: Laden

In Processing gibt es ein Klasse XML. Ein Objekt dieser Klasse kann den Inhalt einer XML-Datei in Form von verschachtelten Java-Objekten speichern. Diese Objekte kann man dann mit Schleifen durchlaufen, um die Informationen herauszuziehen.

Wir nehmen uns das obige Beispiel und speichern es als Datei foo.xml. In der ersten Zeile steht eine XML-Deklaration, welche die XML-Version und evtl. auch die Zeichenkodierung angibt. Es handelt sich offensichtlich um keinen "normalen" XML-Tag (erkennbar an den Fragezeichen nach bzw. vor der spitzen Klammer) und muss deshalb auch nicht geschlossen werden. Diese Zeile ist optional:

<?xml version="1.0" encoding="UTF-8"?>
<gamedata>
  <ball>
    <x>66.716225</x>
    <y>79.98076</y>
    <xspeed>-2.7014487</xspeed>
    <yspeed>-0.539416</yspeed>
  </ball>
  <ball>
    <x>46.550686</x>
    <y>56.259285</y>
    <xspeed>0.14038557</xspeed>
    <yspeed>2.3454807</yspeed>
  </ball>
</gamedata>

Jedes XML-Element wird von Processing als Objekt vom Typ XML repräsentiert. Schauen wir uns nochmal die Baumstruktur an. So sollten auch unsere XML-Objekte repräsentiert sein:

Zunächst lesen wir die Datei mit loadXML() ein (sollte im Unterverzeichnis /data stehen) und bekommen das Wurzelelement (bei uns: gamedata) zurück. Es handelt sich um ein Objekt vom Typ XML:

XML xml; // Variable vom Typ XML
xml = loadXML("foo.xml");

Jetzt holen uns die Kinder des Wurzelelements mit getChildren(). Wir müssten dann die zwei ball-Elemente erhalten. Um das zu testen, durchlaufen wir alle Kindelemente und schauen uns den Tag an. Den Tag eines XML-Elements bekommt man mit der Methode getName().

  XML xml = loadXML("foo.xml"); // Wurzel (gamedata)
  XML[] elements = xml.getChildren();

  // Alle Kinder des Wurzelknotens durchlaufen
  for (int i = 0; i < elements.length; i++) {
    XML child = elements[i];
    String tag = child.getName();
    println("tag: " + tag);
  }

Sie sehen:

tag: #text
tag: ball
tag: #text
tag: ball
tag: #text

Was ist passiert? XML speichert nicht nur die Elemente, sondern auch Textzeichen. In unserem Fall haben wir Zeilenumbrüche vor und hinter den ball-Elementen:

<gamedata>
  <ball>
    ...
  </ball>
  <ball>
    ...
  </ball>
</gamedata>

Diese Zeilenumbrüche werden bei getChildren() auch als Kindknoten mit einem Tag namens "#text" zurückgegeben. Da uns diese Kindknoten nicht interessieren, filtern wir die Kinder: wir wollen nur Kinder mit dem Tag "ball", im Code schreiben wir daher getChildren("ball"). Diese Kinder stehen dann im ballElements-Array.

  XML[] ballElements = xml.getChildren("ball"); // filtern!

  for (int i = 0; i < ballElements.length; i++) {
    XML ballChild = ballElements[i];
    String tag = ballChild.getName();
    println("tag: " + tag);
  }
tag: ball
tag: ball

Von jedem dieser Ball-Elemente (ballChild) gehen wir wieder durch alle Kinder (x, y, xspeed, yspeed) und schreiben dann von jedem Kindelement den Tag mit getName() und Inhalt mit getContent() auf die Konsole:

    XML[] ballData = ballChild.getChildren();
    for (int k = 0; k < ballData.length; k++) {
      String tagName = ballData[k].getName();
      String content = ballData[k].getContent();
      println("    " + tagName + " = " + content);
    }

Hier der gesamte Code (im aktiven Modus):

void setup() {
  XML xml = loadXML("foo.xml");

  // Kinder der Wurzel einlesen:
  XML[] ballElements = xml.getChildren("ball");

  // Alle Ball-Elemente durchlaufen
  for (int i = 0; i < ballElements.length; i++) {
    println("Ball einlesen"); // zum Testen
    XML ballChild = ballElements[i];
    XML[] ballData = ballChild.getChildren();

    // Alle Kinder eines Ball-Elements durchlaufen
    for (int k = 0; k < ballData.length; k++) {
      String tagName = ballData[k].getName();
      String content = ballData[k].getContent();
      println("    " + tagName + " = " + content);
    }
  }
}

Wieder sehen wir die (unerwünschten) Text-Elemente:

Ball einlesen
    #text =

    x = 66.716225
    #text =

    y = 79.98076
    #text =

    xspeed = -2.7014487
    #text =

    yspeed = -0.539416
    #text =

Ball einlesen
    #text =

    x = 46.550686
    #text =

    y = 56.259285
    #text =

    xspeed = 0.14038557
    #text =

    yspeed = 2.3454807
    #text =

Wir filtern diesmal mit If nur Elemente mit solchen Tags heraus, die uns interessieren, nämlich x, y, xspeed und yspeed:

void setup() {
  XML xml = loadXML("foo.xml");
  XML[] ballElements = xml.getChildren("ball");

  for (int i = 0; i < ballElements.length; i++) {
    println("Ball einlesen");
    XML[] data = ballElements[i].getChildren();
    for (int k = 0; k < data.length; k++) {
      String tagName = data[k].getName();
      String content = data[k].getContent();
      if (tagName.equals("x") || tagName.equals("y") ||
          tagName.equals("xspeed") ||
          tagName.equals("yspeed"))
      {
        println("    " + tagName + " = " + content);
      }
    }
  }
}

Jetzt bekomen wir nur die relevanten Informationen:

Ball einlesen
    x = 66.716225
    y = 79.98076
    xspeed = -2.7014487
    yspeed = -0.539416
Ball einlesen
    x = 46.550686
    y = 56.259285
    xspeed = 0.14038557
    yspeed = 2.3454807

Sie können auch XML speichern. Dazu müssen Sie zunächst ein XML-Objekt herstellen und dies dann mit saveXML() in eine Datei schreiben. Hier ein Beispiel, wo wir unsere Datei einlesen, die Daten alle auf -1 setzen und dann unter neuem Namen (foo2.xml) speichern:

void setup() {
  XML xml = loadXML("foo.xml");
  XML[] ballElements = xml.getChildren("ball");

  for (int i = 0; i < ballElements.length; i++) {
    println("Ball einlesen");
    XML[] data = ballElements[i].getChildren();
    for (int k = 0; k < data.length; k++) {
      String tagName = data[k].getName();
      String content = data[k].getContent();
      if (tagName.equals("x") || tagName.equals("y") ||
          tagName.equals("xspeed") ||
          tagName.equals("yspeed")) {
        data[k].setContent("-1");
      }
    }
  }

  saveXML(xml, "foo2.xml");
}

Die Datei "foo2.xml" sieht dann so aus:

<?xml version="1.0" encoding="UTF-8"?>
<gamedata>
  <ball>
    <x>-1</x>
    <y>-1</y>
    <xspeed>-1</xspeed>
    <yspeed>-1</yspeed>
  </ball>
  <ball>
    <x>-1</x>
    <y>-1</y>
    <xspeed>-1</xspeed>
    <yspeed>-1</yspeed>
  </ball>
</gamedata>

Die erste Zeile besagt lediglich, dass es sich um ein XML-Dokument handelt und welche Zeichen-Enkodierung verwendet wird. Ansonsten sehen Sie, dass unsere veränderten Informationen in die Datei geschrieben wurden.

In unserem Beispiel hat die XML-Datei hat die eigentlichen Daten (z.B. die Zahl 66.716225) ganz kompakt in Tags eingeklammert:

<gamedata>
  <ball>
    <x>66.716225</x>
    <y>79.98076</y>
    <xspeed>-2.7014487</xspeed>
    <yspeed>-0.539416</yspeed>
  </ball>
</gamedata>

Schauen wir uns eine etwas andere, völlig korrekte Formatierung an:

<gamedata>
  <ball>
    <x>
      66.716225
    </x>
    <y>
      79.98076
    </y>
    <xspeed>
      -2.7014487
    </xspeed>
    <yspeed>
      -0.539416
    </yspeed>
  </ball>
</gamedata>

Wenn wir jetzt die Inhalte ausgeben, sehen wir folgendes:

Ball einlesen
    x =
      66.716225

    y =
      79.98076

    xspeed =
      -2.7014487

    yspeed =
      -0.539416

Bei Inhalten bleiben beim Einlesen alle Leerzeichen und Zeilenumbrüche erhalten! Dies ist natürlich notwendig, wenn man z.B. Texte für eine Textverarbeitung speichern will. In unserem Fall ist das hinderlich, weil wir die Zahlen als Strings bekommen und in Integer-Werte umrechnen wollen. Da führen Leerzeichen und Zeilenumbrüche zu Fehlern.

Abhilfe schafft die String-Methode trim(), die alle Leerzeichen und Zeilenumbrüche zu Beginn und am Ende eines Strings abschneidet (siehe auch 17.3):

String content = ballData[k].getContent().trim();

Hier nochmal der ganze Code:

void setup() {
  XML xml = loadXML("foo.xml");
  XML[] ballElements = xml.getChildren("ball");

  for (int i = 0; i < ballElements.length; i++) {
  println("Ball einlesen");
    XML ballChild = ballElements[i];
    XML[] ballData = ballChild.getChildren();
    for (int k = 0; k < ballData.length; k++) {
      String tagName = ballData[k].getName();
      String content = ballData[k].getContent().trim();
      if (tagName.equals("x") || tagName.equals("y") ||
        tagName.equals("xspeed") ||
        tagName.equals("yspeed"))
      {
        println("    " + tagName + " = " + content);
      }
    }
  }
}
Ball einlesen
    x = 66.716225
    y = 79.98076
    xspeed = -2.7014487
    yspeed = -0.539416

Im Beispiel hatten wir keine Attribute. Nehmen wir ein einfaches XML-File mit Attributen:

<gamedata player="yoda" score="1000000" progress="0.33">
  ...
</gamedata>

Sie laden den Wurzelknoten gamedata und speichern ihn. Anschließend greifen Sie auf die Attribute dieses Objekts mit den Methoden getString, getInt oder getFloat zu, je nachdem, welchen Inhalt Ihr Attribut hat. Der Methode müssen Sie jeweils den Namen des Attributs nennen:

XML gamedata = loadXML("foo.xml");
println(gamedata.getString("player"));
println(gamedata.getInt("score"));
println(gamedata.getFloat("progress"));
yoda
1000000
0.33

Mit getString können Sie prinzipiell also jedes Attribut auslesen. Die anderen Funktionen sind zu Ihrem Komfort da und wandeln den Inhalt gleich in den entsprechenden Datentypen um.

XML in Processing: Erstellen & Speichern

Wenn Sie XML erzeugen/schreiben möchten, müssen Sie selbst ein XML-Objekt bauen. Genauer gesagt erzeugen Sie für jedes XML-Element ein eigenes Objekt (vom Typ XML) und legen zusätzlich die Eltern-Kind-Beziehungen, die Inhalte und gegebenenfalls die Attribute fest.

Nehmen wir ein einfaches Beispiel, zunächst ohne Attribute:

Wir beginnen mit der Wurzel (engl. root), also dem gamedata-Element und verwenden den Konstruktor von XML, um das Objekt herzustellen:

XML root = new XML("gamedata");

Anschließend erzeugen Sie den ball-Kindknoten und stellen gleichzeitig die Beziehung zum Elternknoten (gamedata) her. Sie rufen dazu die Methode addChild() auf - diese gibt das neu erzeugte XML-Element zurück:

XML ball = root.addChild("ball");

Jetzt erzeugen Sie analog das x-Element und fügen dem Element mit der Methode setContent() den Wert hinzu. Analog für y.

XML x = ball.addChild("x");
x.setContent("" + 5); // erwartet einen String

XML y = ball.addChild("y");
y.setContent("" + -20);

Mit println(root) können Sie sich das Resultat auf der Konsole ansehen:

<gamedata><ball><x>5</x><y>-20</y></ball></gamedata>

Um Attribute zu setzen verwendet man die Methoden setString(), setInt() und setFloat(), je nachdem welche Art Wert geschrieben werden soll. Möchten wir z.B. unserem Ball einen ID namens "b21" mitgeben und einen Startwinkel von 0.13, dann schreiben wir:

XML ball = root.addChild("ball");

ball.setString("id", "b21"); // Attribut id setzen
ball.setFloat("angle", 0.13); // Attribut angle setzen

Die Ausgabe sieht jetzt so aus:

<gamedata><ball angle="0.13" id="b21"><x>5</x><y>-20</y></ball></gamedata>

Sie könnten übrigens auch immer nur setString() verwenden, da im XML kein Unterschied zwischen den Typen gemacht wird - alles steht in Anführungszeichen. Die Funktionen setInt() und setFloat() sind reine Komfortfunktionen, damit Sie die Zahlen im Processing-Code nicht in Strings umwandeln müssen.

Mit saveXML(root, "foo.xml") können Sie das ganze wieder speichern. Dann sehen Sie dies:

<?xml version="1.0" encoding="UTF-8"?>
<gamedata>
  <ball>
    <x>5</x>
    <y>-20</y>
  </ball>
</gamedata>

Jetzt können Sie XML-Daten sowohl laden als auch neu erstellen und speichern.

Übungsaufgaben

(a) XML lesen und auf Konsole schreiben

Zur Vorbereitung erzeugen Sie zunächst eine neues Processing-Programm und speichern es. Jetzt haben wir ein Verzeichnis für unsere Daten. Erzeugen Sie eine Textdatei "data.xml" (mit einem Texteditor) innerhalb Ihres Processing-Programms mit folgendem Inhalt:

<?xml version="1.0" encoding="UTF-8"?>
<moviedata>
  <movie title="Star Wars 7" year="2015">
    <role>
    	<actor>
    	   Harrison Ford
    	</actor>
    	<character>
    	   Han Solo
    	</character>
    </role>
    <role>
    	<actor>
    	   Carrie Fisher
    	</actor>
    	<character>
    	   Leia
    	</character>
    </role>
  </movie>
  <movie title="Interstellar" year="2014">
    <role>
    	<actor>
    	   Matthew McConaughey
    	</actor>
    	<character>
    	   Cooper
    	</character>
    </role>
    <role>
    	<actor>
    	   Anne Hathaway
    	</actor>
    	<character>
    	   Brand
    	</character>
    </role>
  </movie>
</moviedata>

Schreiben Sie jetzt Code, der data.xml einliest und folgende Ausgabe erzeugt:

Star Wars 7 (2015)
	Harrison Ford --- Han Solo
	Carrie Fisher --- Leia

Interstellar (2014)
	Matthew McConaughey --- Cooper
	Anne Hathaway --- Brand

Tipp: Denken Sie daran, die #text-Elemente wegzufiltern und vielleicht benötigen Sie auch trim()?

(b) Bälle in XML speichern

Schreiben Sie Funktionen, um eine Menge von Bällen zu speichern und zu laden, wie wir es in Abschnitt 17.2 getan haben. Diesmal sollen die Informationen in XML gespeichert werden und zwar auf diese Weise (dies ist natürlich nur ein Beispiel mit zwei Bällen):

<gamedata>
  <ball>
    <x>66.716225</x>
    <y>79.98076</y>
    <xspeed>-2.7014487</xspeed>
    <yspeed>-0.539416</yspeed>
  </ball>
  <ball>
    <x>46.550686</x>
    <y>56.259285</y>
    <xspeed>0.14038557</xspeed>
    <yspeed>2.3454807</yspeed>
  </ball>
</gamedata>

(c) Bälle in XML mit Attributen

Schreiben Sie Funktionen, um eine Menge von Bällen zu speichern und zu laden, verwenden Sie aber dieses Format:

<gamedata>
  <ball x="66.716225" y="79.98076"
        xspeed="-2.7014487" yspeed="-0.539416" />
  <ball x="46.550686" y="56.259285"
        xspeed="0.14038557" yspeed="2.3454807" />
</gamedata>

Im Vergleich zur obigen Aufgabe verwenden Sie also Attribute, um die Informationen für jeden Ball zu speichern. Lassen Sie sich durch den Zeilenumbruch im Beispiel oben nicht irritieren - der ist nur aus layout-technischen Gründen drin.

(d) XML rekursiv auslesen

Schreiben Sie eine Funktion, die alle Elemente einer XML-Datei auf der Konsole ausgibt. Die Funktion prettyPrintXML bekommt ein XML-Objekt und druckt Tag und Inhalt und durchläuft anschließend alle Kind-Knoten und ruft sich selbst auf diesen auf (Rekursion). Wenn keine Kinder vorhanden sind, macht die Funktion nichts (Abbruch-Fall).

Um die Funktion zu testen, rufen Sie sie mit dem Wurzelknoten auf. Sie werden eine Menge leere Inhalte bekommen. Schauen Sie in die Processing-Dokumentation der Klasse XML, wie Sie dies am besten unterbinden.

Zusammenfassung

XML ist eine Meta-Sprache mit einer vorgegebenen Struktur, die es erlaubt, Daten mit "Markierungen" (mark-up) und daher menschenlesbar zu speichern. Eine bekannte XML-Sprache ist HTML.

Ein XML-Dokument besteht aus verschachtelten Elementen. Jedes Element hat einen Start-Tag und einen End-Tag. Zwischen diesen Tags befindet sich der Inhalt. Hier sehen wir das Element foo:

<foo>Mein Inhalt</foo>

XML-Elemente können verschachtelt sein, es muss aber immer genau eine Wurzel geben.

Ein XML-Element kann ferner beliebig viele Attribute haben, die in Start-Tag geschrieben werden. Der Attribut-Wert muss mit Anführungszeichen umgeben sein, auch wenn es sich um eine Zahl handelt.

<foo titel="herr" alter="36">Mein Inhalt</foo>

Processing stellt die Funktionen loadXML und saveXML bereit, um XML zu laden und zu speichern. Nach dem Laden wird der Inhalt der Datei in mehreren Objekten vom Typ XML gespeichert. Jedes Objekt entspricht einem Element, beim Laden wird das Wurzelelement zurückgegeben. Mit der Methode getChildren() kann dann auf die Kinder zugegriffen werden, die selbst wieder XML-Objekte sind. Zu beachten ist, dass zwischen den XML-Elementen evtl. #text-Elemente sind, welche die Leerzeichen und Zeilenumbrüche beinhalten. Möchte man nur Kinder mit Tag "ball", schreibt man getChildren("ball"). Mit den Methoden getName() kann man auf den Tag des Elements, mit getContent() auf den Inhalt zugreifen.

Attribute eines XML-Objekts lesen Sie mit getString(), getInt() und getFloat() und setzen Sie (zum Schreiben) mit setString(), setInt() und setFloat().