Bahnprojekt Brieselang - Big Data on the Village

Bahnfahren ist wieder in. Wegen der Umwelt. Knapp 4,7% der Berufspendler fahren jeden Tag mit Bahn oder S-Bahn. Es sollen noch viel mehr werden, denn knapp 70% der Pendler fahren derzeit mit dem Auto. Wie stehts also mit der Qualitaet des Bahnverkehrs

Posted by eumel8 on February 15, 2020 · 13 mins read

Bahnfahren ist wieder in. Wegen der Umwelt. Knapp 4,7% der Berufspendler fahren jeden Tag mit Bahn oder S-Bahn. Es sollen noch viel mehr werden, denn knapp 70% der Pendler fahren derzeit mit dem Auto. Wie stehts also mit der Qualitaet des Bahnverkehrs in Deutschland?

Die Bahn ist auch im Internet moderner geworden. Die Portale https://developer.deutschebahn.com/ und https://data.deutschebahn.com/ bieten eine Menge APIs und Datensaetze zu Bahnanlagen wie Stationsdaten an. Fahrplaene kann man aucb ueber APIs im Json-Format abrufen, aber es handelt sich immer um den SOLL-Fahrplan. Fuer den IST-Plan gibt es dort derzeit keine Schnittstellen, da muss man die Webseiten https://reiseauskunft.bahn.de/ oder https://mobile.bahn.de bemuehen.

Bahn-API Bevor es richtig losgeht, brauchen wir erstmal die EVA Nummer vom Bahnhof Brieselang. Die gibts auf https://developer.deutschebahn.com. Nach der Registrierung muss man die StaDa-Station_Data - v2 API abonnieren. Mit einem erstellten Bearer-Token kann man die Daten abrufen:

curl -X GET --header "Accept: application/json" --header "Authorization: Bearer xxxxxxxxxxx" "https://api.deutschebahn.com/stada/v2/stations?searchstring=Brieselang"
...
 "evaNumbers": [
 {
 "number": 8013472,
 "geographicCoordinates": {
 "type": "Point",
 "coordinates": [
 13.001394,
 52.582549
 ]
 },
 "isMain": true
 }
 ],
...

Der EVA Code ist 8013472,

Python

Die Reise beginnt bei Python Requests. in Zeile 80 von schiene2.py

def departures(origin, dt=datetime.now()):

 query = {
 'si': origin,
 'bt': "dep",
 'date': dt.strftime("%d.%m.%y"),
 'ti': dt.strftime("%H:%M"),
 'p': '1111101',
 'rt': 1,
 'max': 4,
 'use_realtime_filter': 1,
 'start': "yes",
 'L': 'vs_java3'
 }
 rsp = requests.get('http://mobile.bahn.de/bin/mobil/bhftafel.exe/dox?', params=query)
 return parse_departures1(rsp.text,dt)

origin = EVA
bt = Departure (Abfahrt)
date/ti = Date/Timestamp
p = Verkehrsmittel (alle Zuege, kein Bus)
max = maximale Anzahl Fahrten (4...8)
rt/use_realtime = Echtzeitplan
start = incl .bereits gestartete Zuege
L = Ausgabeformat (xml) 

Tatsaechlich sieht die Seite so aus:

Als naechstes muss der Output geparsed werden in parse_departures. Dazu verwenden wir die Bibliothek BeautifulSoup.

def parse_departures1(html,dt):

 soup = BeautifulSoup(html, "html.parser")
 date = dt.strftime("%d.%m.%y")

 for row in soup.find_all('div', attrs={'class': 'sqdetailsDep trow'}):
 trainnumber = row.span.contents[0]
 trainurl = row.a['href']
 traintarget = row.br.previous_element.strip()[3:]
 traindeparture = (row.find_all('span', attrs={'class': 'bold'})[-1].get_text())
 traindelay, traintext, trainnote = traindetails(date,trainnumber,trainurl)
 gspread_data(date,trainnumber,traindeparture,traintarget,traindelay,traintext,trainnote)
 return

Es ist sehr gut zu sehen, wie man mit find_all nach bestimmten Tags suchen kann, um da die Informationen rauszuziehen. Die brauchen wir auch, denn um Informationen ueber einen Zug zu bekommen, brauchen wir eine weitere Abfrage.

def traindetails(date,trainnumber,trainurl):

 trainurl = trainurl.replace('dox','dn')
 rsp = requests.get(trainurl)
 traindelay, traintext, trainnote = parse_train(rsp.text)
 return traindelay,traintext, trainnote

Also fast dieselbe Abfrage, aber auf einem anderen Endpunkt. Die Seite sieht dann so aus:

Wir sind interessiert an den Besonderheiten des Zuges. Das sind entweder fehlende Wagen, fehlendes Mehrzweckabteil, fehlende Einsteigehilfe fuer Rollstuhlfahrer … es ist ein Freitext, dort kann alles moegliche drinstehen. Wir definieren das spaeter einfach als Stoerung:

Auch diese Ausgabe wird mit BeautifulSoup geparsed. Wir haben jetzt auch alle Informationen zusammen und koennen diese in einer Google-Exceltabelle abspeichern. Die Tabelle koennen wir bei Google Drive von Hand erstellen. Dann brauchen wir einen Service-Account von der Google Spreadsheet API, der Schreibrechte in die Tabelle hat.

def gspread_data(date,trainnumber,traindeparture,traintarget,traindelay,traintext,trainnote):
 row = [date,trainnumber,traindeparture,traintarget,traindelay,traintext,trainnote]
 scope = ['https://spreadsheets.google.com/feeds','https://www.googleapis.com/auth/drive']
 creds = ServiceAccountCredentials.from_json_keyfile_name('gspread-secret.json', scope)
 client = gspread.authorize(creds)
 sheet = client.open('trainqa').sheet1
 sheet.insert_row(row)
 removeDuplicates(sheet)
 return

Eigentlich ganz einfach. Der Service-Account meldet sich bei der API an. Das Secret-File mit den Credentials liefert Google fuer cut&paste. Ins Arbeitsblatt 1 der Exceltabelle werde die Daten geschrieben.

Nun ist es aber so, dass wir den Fahrplan ohne festen Bezugspunkt aufrufen. Wenn wir das Programm stuendlich laufen lassen, wissen wir nur ungefaehr, wieviel Datensaetze als Fahrten zurueckgeliefert werden. Die Gefahr fuer doppelte Datensaetze ist also ziemlich gross. Diese koennen wir durch removeDuplicates loeschen:

def removeDuplicates(sheet):
 data = sheet.get_all_values()
 datalen = len(data)
 newData = []

 for i in range(len(data)):
 row = data[i]
 if row not in newData:
 newData.append(row)
 else:
 sheet.delete_row(i)
 return

Das vollstaendige Programm befindet sich hier: https://github.com/eumel8/trainqa/blob/master/apps/schiene2.py

Android Studio

Die technischen Daten, also der Echtzeitfahrplan, ist natuerlich nur die eine Seite, um die Qualitaet der Zugverbindung zu messen. Was sagen aber die Fahrgaeste? “Eine App muesste man haben”; kam schnell der Wunsch auf. Wie kann man sowas bewerkstelligen? Mit Android Studio geht das relativ einfach. Das Entwicklertool fuer Android-App erinnert ein bischen an Visual Basic, es handelt sich aber tatsaechlich um Java-Code. Es gibt auch eine Menge Anleitungen im Netz. Am besten gefaellt mir https://abhiandroid.com/, um so Grundfunktionen auszuprobieren. Auch ein interessantes Projekt ist hier beschrieben. Der Plan waere also eine App, mit der man den Zug auswaehlen kann und diesen nach Puenktlichkeit, Auslastung und Platzangebot bewerten kann. Die Daten sollen in eine Excel-Tabelle geschrieben werden. Da kommt auch schon das erste Problem: Damit die App Daten in die Exceltabelle schreiben kann, braucht sie den Serviceaccount mit Schreibrechten. Nun wird die App, wenn sie erstmal im AppStore zur Verfuegung gestellt wird, auf jedermanns Handy installiert, der diese App haben moechte - incl. dem Service-Account nebst Credentials. Auch wenn Java-Klassen kompiliert sind, koennen sie wieder dekompiliert werden, um solche Informationen rauszuziehen. Dazu bietet Google Webformulare an, die die Antworten in Exceltabellen abspeichert. Die Formulare sind oeffentlich zugaenglich, nix Service-Account. Wir brauchen nur einen Webservice in der App, der dieses Formular mit den Appdaten fuer uns ausfuellt

public interface QuestionsSpreadsheetWebService {

 @POST("1FAIpQLSeyBr1tJ3y7SjX2c107B92yv2P600WRxmlJyAWzrPt59Bvf7g/formResponse")
 @FormUrlEncoded
 Call<void> completeQuestionnaire(
 @Field("entry.1476348758") String feld1t,
 @Field("entry.679257558") String feld2,
 @Field("entry.1485087550") Float feld4,
 @Field("entry.617878135") Float feld5,
 @Field("entry.1543695788") Float feld6,
 @Field("entry.51849126") Boolean feld7
 );

Die Feld-ID bekommen wir aus dem HTML-Sourccode des Webformulars. Die Post-ID ist die Formular-ID aus der URL desselbigen. Aequivalente Felder bekommen wir in Android mit dieser xml. Dazu gehoert dann eine onCreate-class, in der dann auch das onClick des Webformulars verarbeitet wird und zu dem Webservice oben weiterleitet. Nachtraeglich reingefummelt ist noch eine Liste von Zuegen, die zur Bewertung ausgewaehlt werden koennen. Dazu gibts einen zweiten Click-Button, mit der eine ListView aufgerufen wird. Jetzt braeuchten wir wieder die ServiceAccount Credentials zum Zugriff auf die Excel-Tabelle, wenn wir auf dieselben Daten zugreifen wollen die wir oben im Python-Script erstellt haben. Hier hilft uns ein Json-Parser, welches als App bei Google gehostet ist. Man gibt der Excel-Tabelle Lesezugriff fuer die Welt und verfuettern die Excel-ID aus der URL als Parameter an das Script. Als Ausgabe erhalten wir die Excell-Tabelle als Json, was wir in der App als Liste verarbeiten koennen. Mhm, alles bischen tricky und schwer zu erklaeren. Es gibt aber eine sehr grosse Community und so ziemlich jedes Problem ist im Netz schon mal beschrieben und geloest. Vielleicht leistet der Blog auch einen kleinen Beitrag. Die App wird im Android Studio kompiliert, signiert und dann im https://play.google.com hochgeladen. Dazu muss man sich als Entwickler registrieren, 25 Dollar einwerfen und paar Tage warten, bis die App freigeschalten ist.

Public Tableau

Public Tableau ist ein Dienst, mit dem man Daten visualisieren kann. Das Programm gibts auch als Serverinstallation, mit derm man bestimmte Datenbanken auslesen kann, es gibt aber auch den Onlinedienst, der sich mit Google Excel verbinden kann, um die Daten von dort abzugreifen. Es gibt auch einen Tableau-Client, den man sich runterladen kann, und eine Menge Vorlagen an Tabellenformationen und Diagrammen. Die Verbindung zu Google wird ueber einen Auth-Token aufrecht erhalten. Sicherheitshalber koennte man noch einen extra Google-Account erstellen, um nicht seine persoenlichen Credentials an dieser Stelle zu verwenden. Aber etwa einmal am Tag werden die Daten automatisch aktualisiert. Fallstrick: Man darf bei dieser Methode nicht mehrere Google Sheet Tabellennblaetter mischen und auch keine zwei Tabellen miteinander verbinden - auch nicht in einem gemeinsamen Dashboard in einem Projekt! Sonst werden die Daten nicht mehr automatisch aktualisiert.

Ausblick Mit der Zeit (Stand 02/2020) werden sich eine Menge Daten dort ansammeln. So lassen sich laengerfristige und belastbare Statistiken erstellen ueber Qualitaet des Bahnverkehrs.

Projektlinks

  • [https://github.com/eumel8/trainqa](https://github.com/eumel8/trainqa)
  • [https://bahn-brieselang.github.io/](https://bahn-brieselang.github.io/)

Credits:

  • [Python Bahn API](https://github.com/posixpascal/optimusbahn)
  • [Android Tutorials](https://abhiandroid.com)
  • [Google Excel Json](https://www.crazycodersclub.com/android/using-google-spread-sheet-as-database-for-android-application-part-1/)
  • [Github Pages](http://jmcglone.com/guides/github-pages/)