c't 9/2021
S. 160
Praxis
Inzidenz-Ampel
Bild: Albert Hulm

Corona-Ampel

Inzidenzen vom RKI abrufen und automatisch auswerten

Die 7-Tage-Inzidenz ist einer der Fachbegriffe der Epidemiologie, die vor einem Jahr kaum einer kannte. Die Politik nutzt ihn immer wieder, um Regeln fürs Öffnen und Schließen von ­Geschäften, Dienstleistern und Schulen zu erlassen. Da der Wert so wichtig ist, soll Software ­automatisch auf diesen Wert ­zugreifen – zum Beispiel für eine PHP-Inzidenzampel auf der Homepage.

Von Jan Mahn

Machen wir uns nichts vor: Die gespannten Blicke auf die Inzidenz­zahlen des Robert-Koch-Instituts werden viele Berufsgruppen noch einige Zeit begleiten. Ob man seinem Beruf am nächsten Tag wie gewohnt nachgehen, die Kinder in die Schule schicken und sich die Haare schneiden lassen kann, hängt ja aktuell davon ab, ob in der Region Schwellwerte für die 7-Tage-Inzidenz (Anzahl der Fälle in den letzten sieben Tagen pro 100.000 Einwohner) unterschritten wurden. Auf vielen Homepages von Unternehmen, Vereinen und Schulen findet man daher immer öfter von Hand aktualisierte Hinweise, welche Regeln aktuell gelten.

Damit die Hinweise nicht ständig veralten, sollte lieber ein Computer diese Information auswerten und die geltenden Regeln automatisch darstellen. Eigentlich keine komplexe Aufgabe, wenn man erst mal die Daten in maschinenlesbarer Form zur Verfügung hat. Mitte März landete diese Frage durch Zufall auch in meinem Mailpostfach. Kann ja so schwer nicht sein, eine kleine Inzidenzampel für eine Homepage zu bauen, dachte ich mir, und begab mich auf die Suche nach Datenmaterial.

Datensuche

Erste Anlaufstelle für Inzidenzzahlen ist das Robert-Koch-Institut. Zum Glück hat das RKI recht früh beschlossen, Darstellung und Auslieferung der Daten an Profis für genau diese Aufgabe zu delegieren – konkret an das Unternehmen Esri mit dem Produkt ArcGis Online. Herausgekommen ist das Corona-Dashboard, das fast jeder Internetnutzer in den vergangenen zwölf Monaten schon mal geöffnet haben dürfte (zu erreichen über corona.rki.de).

Gefüttert wird diese Webanwendung von einem JSON-API, das ebenfalls von Esri betrieben und vom RKI mit Daten versorgt wird. Ein erster Blick hinter die Kulissen mit den Entwicklerwerkzeugen des Browsers ist aber eher demotivierend: Um das ganze Dashboard zusammenzubauen, werden Dutzende HTTP-Anfragen ans API abgeschickt – und die Abfrageparameter sind alles andere als selbsterklärend. Zumindest kommt man so an die Adresse des API und hat damit eine Spur, um nach einer zugehörigen Dokumentation zu forschen. Diese findet man unter der sperrigen Adresse npgeo-corona-npgeo-de.hub.arcgis.com. Das RKI hat darunter gleich mehrere APIs abgelegt – interessant für die Aufgabe ist das API „RKI Corona Landkreise“, das Daten nach Landkreisen sortiert ausgeben kann. Die Dokumentation und einen URL-­Generator finden Sie über ct.de/yw1c.

Für das API des RKI gibt es einen Generator, um eine maßgeschneiderte URL zu bekommen. Ganz fehlerfrei ist der nicht, erleichtert aber die Arbeit.

Um eine URL zusammenzubauen, die die gewünschten Daten für die eigene Region ausgeben kann, braucht man erst einmal die ID des Landkreises (oder der kreisfreien Stadt) im System. Diese findet man heraus, wenn man in der Dokumentation oben in der Deutschlandkarte nach der Region sucht und diese anklickt. Dann erscheint eine Tabelle mit der OBJECTID in der ersten Zeile.

Mit dieser Information wechselt man oben auf den Reiter „API-Explorer“. Der vermittelt einen Eindruck, welche Datenfelder das API zurückgibt, womit man die Abfrage auf die interessanten Werte eingrenzen kann. Die 7-Tage-Inzidenz verbirgt sich hinter dem Attribut cases7_per_100k. Die meisten anderen Haken kann man also deaktivieren, wenn man keine weiteren Werte darstellen möchte. Alle Attribute in Großbuchstaben beschreiben den Landkreis selbst (GEN enthält den Namen, BEZ verrät, ob es ein Landkreis oder eine kreisfreie Stadt ist). Corona-relevant sind alle Attribute in Kleinbuchstaben. Enthält ein Attribut die Zeichenkette _bl, kann man dahinter einen Wert für das zugehörige Bundesland erwarten. So gibt es zum Beispiel death7_bl (Tote der letzten sieben Tage im Bundesland) und death7_lk (Tote im gewählten Landkreis).

Rechts neben dem URL-Generator entsteht parallel die URL, die man für die weitere Arbeit kopieren kann. Es fehlt aber noch ein Filter auf den konkreten Landkreis. Eigentlich sollte der API-Explorer auch Filter erzeugen können, jedoch ist diese Funktion voller Fehler. Ein einfaches = funktioniert nicht. Schlimm ist das aber nicht, weil man die Abfrage auch selbst per Hand in die fertige URL einbauen kann. Dafür ändert man einfach den voreingestellten Filter ?where=1%3D1 auf ?where=OBJECTID=<die ID>. Heraus kommt also eine längliche URL, die in etwa wie folgt aussieht:

https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_Landkreisdaten/FeatureServer/0/query?where=OBJECTID=<IHRE ID>&outFields=Shape__Length,cases,deaths,cases_per_population,last_update,cases7_per_100k,cases7_bl,death7_lk,cases7_per_100k_txt,cases_per_100k,cases7_lk,recovered,cases7_bl_per_100k,GEN,BEZ&outSR=4326&f=json

Der erste Teil des Pfads mOBPykOjAyBO2ZKk ist dabei kein Kennwort und auch kein temporäres Element, sondern eine Art Kunden- oder Projektnummer des RKI beim Anbieter Esri. Sie müssen also keine Angst haben, dass sich dieser Pfad ständig ändert. Ersetzen Sie <IHRE ID> durch eine für Ihre Region und schauen Sie sich das Ergebnis in einem Browser (oder besser einem API-Werkzeug wie Postman) an. Eine Authentifizierung oder strenges Rate-­Limiting gibt es übrigens nicht, weil das Dashboard ja von jedem ständig einsehbar sein soll.

Leider nicht abbestellen kann man die Koordinaten für die Außengrenzen des Landkreises am Ende des JSON-Blocks. Die wären nur dann nützlich, wenn man eine Karte der Region zeichnen wollte.

Alarmstufe Rot

Ausgerüstet mit der URL für die JSON-­Daten ist der nächste Schritt zu einer ­Online-Ampel nicht mehr groß. Weil es keine dokumentierten Limits und keine Authentifizierung gibt, könnte man die Ampel theoretisch auch clientseitig in Java­Script entwickeln. Dann wäre die Seite aber immer von einem externen HTTP-­Aufruf abhängig. Schöner ist es, wenn eine serverseitige Programmiersprache die Daten beim RKI herunterlädt, in einen Zwischenspeicher (Cache) legt und dem Browser fertiges HTML liefert.

Die prädestinierte Programmier­sprache für einen solchen Schnipsel für die eigene Homepage ist PHP, das auch in den meisten Webhosting-Paketen installiert und ursprünglich für genau solche Projekte gedacht war.

Möchte man mit PHP nur schnell Inhalte aus dem Web herunterladen, ist die Funktion file_get_contents() die einfachste Option. Sie erwartet als Parameter die URL und gibt den Inhalt als String zurück. Fehlerbehandlung und Optionen sind aber mäßig. Mehr Kontrolle hat man mit der PHP-Integration von cURL. Damit kann man jedes Detail von Anfrage und Rückgabe steuern.

Bevor man den HTTP-Aufruf abschickt, sollte man alle Bestandteile der URL in Variablen schreiben, die man später leicht ändern möchte. Die Regions-ID und die Liste der anzufragenden Attribute sind gute Kandidaten dafür:

$fields = [
 'OBJECTID',
 'GEN',
 'BEZ',
 'cases',
 'deaths',
 'cases_per_population',
 'cases7_per_100k',
 'cases7_lk',
 'death7_lk',
 'cases7_bl_per_100k',
 'cases7_bl',
 'death7_bl',
 'last_update'
];
$fieldstr = implode(",", $fields);
$region_id = 27; //Hannover

Aus dem Array mit den abzurufenden Feldern macht implode() den mit Kommas verbundenen String $fieldstr. Diesen kann man dann in der URL einsetzen und die URL per cURL abrufen:

$c = curl_init();
curl_setopt($c,
 CURLOPT_URL,
 'https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_Landkreisdaten/FeatureServer/0/query?where=OBJECTID=' . $region_id . 
 '&outFields=' . $fieldstr . 
 '&outSR=4326&f=json'
);
curl_setopt(   $c, CURLOPT_RETURNTRANSFER, 1);
$result = curl_exec($c);
if (curl_errno($c)) {
 echo "could not contact rki server");
 curl_close($c);
 exit;
}
curl_close($c);

Sollte etwas nicht klappen, liefert curl_errno() einen Fehler und das Programm bricht ab – an dieser Stelle darf man später noch etwas Fehlerbehandlung ergänzen. Sofern der Download der Daten erfolgreich war, liegen die Daten als JSON-String in der Variablen $result. Die Funktion json_decode() macht daraus ein Array, wenn der zweite Parameter true ist:

$data = json_decode($result, true);

Unter dem Schlüssel $data['features'][0]['attributes'] sollten jetzt die gewünschten Zahlen für die 7-Tage-Inzidenz liegen. Falls die geladenen Daten unvollständig sein sollten, empfiehlt sich vorher noch etwas Fehlerbehandlung:

if (!isset($data['features'][0]
                    ['attributes'])) {
  // Fehlerbehandlung ...
}

In den Speicher

Die Daten des RKI ändern sich nicht sekündlich, sondern werden nur einmal am Tag hochgeladen. Daher muss sie das Ampel-Skript auch nicht bei jedem Seitenaufruf neu herunterladen. Es reicht aus, die Werte für jeden Tag in einer Textdatei abzulegen. Wenn es für den aktuellen Tag schon Daten gibt, soll das Skript diese aus dem Cache holen, ansonsten einen Download probieren. Weil das API keine historischen Daten ausgibt, muss man schlimmstenfalls ein paar Tage warten, bis man einen Verlauf anzeigen kann. Wer hat, kann für das Projekt eine Datenbank zum Zwischenspeichern nutzen [1]. Damit die Ampel ohne weitere Abhängigkeiten läuft, entschied ich mich für einen Zwischenspeicher per Textdatei. Voraussetzung ist nur ein Ordner, in den das Skript schreiben darf – das sollte nicht das Verzeichnis sein, in dem die Website selbst liegt.

Das letzte Problem, das es zu lösen gilt, ist das Datumsformat, das sich das RKI überlegt hat und für das Feld last_update nutzt. Ein Datum aus dem Datensatz sieht zum Beispiel folgendermaßen aus:

"19.03.2021, 12:00 Uhr"

Damit im Zwischenspeicher für jeden Tag ein Eintrag liegt, soll daraus ein Schlüssel im Format 20210319 werden. Um Zeit­angaben in ein anderes Format zu bringen, sollte man von Bastellösungen mit regulären Ausdrücken unbedingt absehen – Ärger bei Randfällen ist vorprogrammiert. Wesentlich eleganter ist es, für solche Auf­gaben die PHP-eigene Klasse DateTime zu nutzen. Deren Methode createFromFormat() nimmt eine Formatbeschreibung des einzulesenden Datums und den zu lesenden String entgegen und macht daraus ein Datums-Objekt. Die Zeichenkette Uhr entfernt man vorher per str_replace().

Aus dem PHP-Datums-Objekt erzeugt dann die Methode format() einen neu formatierten String. So einfach wird aus dem oben stehenden String die Zeichenkette 20210319, die später als Schlüssel für den Zwischenspeicher dient:

$date = DateTime::createFromFormat(
"d.m.Y, H:i", str_replace(" Uhr", "",
               $data['last_update']));
$key = $date->format("Ymd");

Die Logik, um die RKI-Zahlen für jeden Tag in einem Objekt zu speichern und dieses als JSON-Päckchen im Cache abzulegen, ist schnell implementiert. Die Daten liegen weiter in der Variable $data:

$f = @file_get_contents('./rki.json');
if ($f == false) {
 $old = [];
} else {
 $old = json_decode($f, true);
}
$old[$key] = $data;
$new = json_encode($old)
file_put_contents('./rki.json', $new);

Das @ vor file_get_contents() unterdrückt alle Fehler, die von der Funktion geworfen werden – die eigene Fehlerbehandlung der Funktion macht nämlich keine Freude. Daher prüft man besser, ob der Inhalt == false ist und legt in dem Fall ein leeres Array an. Liegen schon Daten in der Datei, wandelt json_decode() sie in ein Array.

Dann legt man die neuen Daten unter dem Schlüssel $key ab, enkodiert das Objekt wieder per json_encode() und schreibt die Datei mit file_put_contents() auf die Festplatte.

In schön

Schnell hatte ich eine unansehnliche Datei mit herrlichem Spaghetticode zusammengeschrieben, der all diese Teilprobleme löste. Um damit anständig arbeiten zu können, überführte ich die Fragmente in eine Klasse Incidence. Wenn Sie die Ampel selbst nutzen wollen, finden Sie diese Klasse (zusammen mit einem Anwendungsbeispiel) über ct.de/yw1c. Die Klasse übernimmt alle Arbeiten im Hintergrund. Legen Sie einfach die Datei Incidence.php auf den Server und binden Sie diese per include() ein. Die Anwendung ist dann einfach:

include('Incidence.php');
$id = 27;
$cache_file = './data.json';
$incidence = new Incidence($id, 
                         $cache_file);
$today = $incidence->getDaily(0);

Die Klasse Incidence erwartet beim Instanziieren einen Pfad für die Cache-Datei und die Regions-ID. Die Methode getDaily() gibt dann die Zahlen des RKI zurück. Der Parameter bestimmt, wie viele Tage in die Vergangenheit geblickt werden soll. $incidence->getDaily(2) gibt etwa die Werte von vorgestern aus.

Sofern die aktuell geltenden Regeln in Code umsetzbar sind, kann ein kleiner Schnipsel auf der Homepage automatisch anzeigen, welche Regeln gelten.

In den Code können Sie Ihre eigenen Logiken einbauen, etwa wie folgt:

$today = $incidence->getDaily(0);
if($today['cases7_per_100k'] < 100) {
 echo "Heute geöffnet. Maskenpflicht";
}
if($today['cases7_per_100k'] == 0) {
 echo "Pandemie beendet!";
}

Die bunte Ampel selbst ist dann eine reine CSS-Übung – oben sehen Sie, wie das aussehen kann. Den fertigen Code finden Sie im Ordner examples über ct.de/yw1c zusammen mit der Klasse zum Download. Denken Sie bei allen Implementierungen daran, Schwellwerte und Ausgabetexte möglichst flexibel zu halten – die Regeln haben sich in den letzten Monaten ja nicht gerade als langlebig erwiesen. (jam@ct.de)

PHP-Klasse und Beispiel: ct.de/yw1c

Kommentare lesen (34 Beiträge)