Eine chronologisch sortierte Historie zu #aufschrei

Im Januar 2013 wurde auf Twitter unter dem Hashtag #aufschrei eine Aktion gestartet, die eine (nicht nur) deutschlandweite Debatte über Sexismus auslöste. Bisher fehlte eine Historie mit allen Tweets zu diesem Hashtag. Aufgrund der eingeschränkten Möglichkeit von Twitter über die API darauf zuzugreifen, wird dieses nie der Fall sein. Aber zumindest für die ersten zwei Wochen konnte ich ein Archiv erstellen und in einer angenehmen Form darstellen:

http://aufschrei.konvergenzfehler.de/

Scraping

Im Herbst 2012 entwickelte ich ein Script, um die Tweets für das #refugeecamp Berlin (später #rfcamp) einzusammeln. Für mich war es etwas Fingerübung in Python und mit der Twitter-API. Eigentlich hatte ich vor, daraus eine Webseite zu erstellen, die die Aktionen (Spendenaktionen, Temperaturwerte, Bilder, Gespräche mit Politikern und vor allem die Übergriffe der Polizei) darstellt und wichtige Zeitpunkte hervorhebt. Dank Zeitmangel und fehlenden Fähigkeiten wurde daraus nichts.

Ende Januar stieß mich map an und meinte “Du hast da doch mal dieses Script zum Scraping geschrieben – kannst Du die mal auf #aufschrei ansetzen? Uns fehlt der allererste Tweet.” Gefragt, getan. Die Tools lagen noch alle einsatzbereit auf einem Server und eine knappe Stunde später hatte ich etwa 50000 Tweets runtergeladen. Großer Jubel, als wir feststellten, dass der allererste Tweet dabei war.

Das Script sucht mit der Twitter Search API nach dem angegebenen Hashtag. Dabei geht es vom aktuellen Zeitpunkt iterativ immer weiter zurück, bis keine Tweets mehr gefunden werden. Die Search API findet Tweets der vergangenen neun Tage – mehr bietet Twitter nicht an. Außerdem gibt es Beschränkungen auf 200 Tweets pro Request, aber es kann das Datum des letzten gefundene Tweets als Startadresse für eine neue Suche angegeben werden.

Zwei Kleinigkeiten, über die ich schon beim Scraping für das #refugeecamp gestolpert war: Twitter erlaubt sich, Benutzer_innen aus der Search API zu verbannen, die viel mit einem Hashtag twittern. Die Benutzerin ist nicht geblockt, aber wird nicht mehr mittels einer Suche zurück gegeben. Die andere Kleinigkeit ist die Einstellung auf “gib mir alles” und nicht nur die “most trending Tweets”. Letzteres ist der Default, weshalb die Tweets nicht chronologisch ankommen, sondern nach irgendeiner Heuristik von Twitter, die meint, das seien die interessantesten Ergebnisse für diese Suche.

Fast schon unnötig es explizit anzugeben, aber es werden nur Tweets von Accounts erfasst, die zu dem Zeitpunkt auf public geschaltet waren. Wer einen protected Account hat, wird über die Search API nicht gefunden.

JSON-Daten – und nun?

Das Scraping-Script spuckte mir nach Tagen aufgeteilt JSON-Dateien aus. Da lag nun alles drin, aber ansehnlich war es nicht. Da ich eh schon mal ein Projekt mit Django machen wollte und sogar noch ein paar Anfänge zur #refugeecamp-Geschichte rumliegen hatte, begab ich mich an die Datenbankmodellierung. Da Django das Datenbank-Backend ziemlich egal ist, wählte ich PostgreSQL. Doch trotz eines fertigen Models mussten die Daten in die Datenbank kommen. Das klappt bei Django ganz gut mit Fixtures, was JSON-Dumps der Datenbank sind.

Also baute ich ein Script, was mir die JSON-Search-API-Dateien in JSON-Django-Fixtures umwandelte. Damit waren die 93667 Tweets in der Datenbank gespeichert. Recht fix schrieb ich ein paar Django-Views und einfache Templates, die mir eine chronologische Sortierung ausgaben.

Aber es war hässlich.

Das mag daran liegen, dass ich aus der Zeit von HTML2 komme und es seit 1995 gewohnt bin, Webseiten in einem Texteditor zu schreiben. CSS? Gab es nicht. JavaScript? Wurde im Browser abgeschaltet, weil böse. Pures HTML – mehr nicht! Und Webdesign? Ich fahre kein Snowboard.

Irgendwer verwies mich auf Bootstrap. Nachdem ich die Doku drei Mal durchlies, konnte ich es anwenden. So wurde die Ausgabe in Django immer schöner und ganz langsam gefiel mir mein Werk.

Weniger ist mehr

So ganz im klaren darüber, was die Webseite eigentlich können sollte, war ich mir nicht. Aber es gab ein paar Ideen. So war eine der Ideen, eine interaktive Oberfläche zu bieten, in der mit Crowd-Working die Tweets mit Tags versehen werden (beispielsweise Spam, KackscheisseVorfall, etc.). Das hätte Interaktion benötigt und Schreibzugriff auf die Datenbank. Oder ich wollte coole Auswertungen machen, so mit Graphen und Zeitleiste. Alles automatisiert aus den Daten in der Datenbank. Immer wieder überlegte ich, wie ich das machen sollte. Zwischendurch lag deswegen das Projekt monatelang in der Schublade (oder eher auf dem Server) und ich packte es nicht mehr an.

Aber bevor ich gar nichts mache und die Daten vergammeln, fällte ich die Entscheidung, nur eine Timeline anzuzeigen. Einfach und simpel.

Lokal ist besser als Remote

Mit meiner ersten Darstellung einer Timeline fiel mir auf, dass Links in Tweets vom Twitter-eigenen URL-Shortener t.co zum “Zwecke der Qualitätssteigerung” gekürzt werden. Dadurch lässt sich nicht erkennen, wohin verlinkt wird, geschweige eine Ahnung vom Inhalt zu erhalten.

Also, noch einen Scraper bauen, der alle Links durchgeht, die Original-URL wiederherstellt und am besten sogar noch die HTML-Titelzeile abholt und in der Datenbank speichert. Das machte ich mit der Python Bibliothek BeautifulSoup. Zwischendurch rannte ich in einen nicht lösbaren Speicherkiller-Bug von BeautifulSoup, weshalb das Scraping immer wieder neu angestoßen werden musste. Nach ein paar Stunden hatte ich 13992 Links zusammen und bewahrte sie wertvoll in der Datenbank auf.

Dieses Scraping lief erst im Mai und Juni 2013. Etliche Links werden als Fehler angezeigt, da die Webseite nicht mehr vorhanden war – ob nun tote Webseiten, gelöscht oder vom öffentlich-rechtlichen Rundfunk depubliziert worden.

Außerdem checkte ich die Links, ob es Bilder sind. Nicht jeden Webseiten-Bilder-Dienst habe ich unterstützt und bei manchen ist es auch verdammt schwer an die Original-URL eines Bilds zu kommen. Aber ein paar große sind dabei. Diese Bilder speicherte ich auch weg. 783 Bilder mit 80MB liegen lokal vor.

Und noch etwas wollte weggespeichert werden: Die Avatare der 25888 Twitter-Benutzer_innen, die an der Aktion mitgewirkt haben. Der ein oder andere Account war inzwischen gelöscht oder gesperrt, aber viele Avatare sind zusammen gekommen. Diese stellen einen alten Stand dar – aktuelle Avatare lade ich nicht runter.

Alles wegwerfen und neu anfangen

Ein paar Django Views waren zusammen gekommen, die Timeline konnte angezeigt werden, etwas Statistik war auch möglich. Aber alles sah zusammengestückelt aus. Nicht wie aus einem Guss. Da hilft nur eins: mit dem gesammelten Wissen noch mal neu machen.

Aber das geht dann recht fix. An einem Wochenende ist in etwa zehn Stunden die jetzige Seite mit allen Views entstanden. Direkt auf Bootstrap 3 aufgesetzt, mit diversen Templates, die einfach nur included werden, und nicht all zu viel Schnickschnack. Als kleine und binnen weniger Minuten umgesetzte Fingerübung kamen noch die reine Bilder– und die Linklisten-Timeline hinzu. Der “zufällige Tweet” war auch keine große Sache, musste nur etwas durchdacht werden.

Fehlte noch eine Startseite in einem etwas anderen Layout. Mit Bootstrap kein großes Problem.

Deploy

Bisher lief die Seite nur im Django Development Modus. Das ist kein Setup, was viele Requests abfrühstücken kann. Mit etwas Suche entschied ich mich für eine Kombination aus Nginx und uWSGI. Dieses Setup aufzusetzen ist nicht ganz einfach, aber es läuft sehr stabil.

Und was ich bei meinem aktuellen Job gelernt habe: Caches einsetzen. Viele Caches. Neben dem Datenbank-Cache gibt es noch einen Memcached und Nginx cached auch noch mal die uWSGI-Requests. Die Bilder werden aktuell nicht gecached, da sie im Laufe der Zeit vom Betriebssystem in die RAM-Caches geladen werden statt jedesmal auf die Festplatte zuzugreifen. Lägen sie Remote, würde auch hier ein Cache wieder Sinn machen.

Meine Freude war echt groß, als ich die Seite http://aufschrei.konvergenzfehler.de/ über dieses Setup aufrufen konnte. Und dass sie sogar auf einem Smartphone dank Bootstrap ohne Code-Änderung sehr gut aussieht!

Weiterentwicklung

Für mich ist das Projekt an dieser Stelle erstmal beendet, damit ich mich wieder anderen Dingen zuwenden kann. Aber wer gerne mit den Daten arbeiten oder die vorher genannten Ideen umsetzen möchte, kann das Projekt bei GitHub clonen. Das Fixture für die Datenbank liegt mit drin. Die Mediendateien (130MB) habe ich separat gepackt. Die Installation sollte “Standard-Django 1.5” sein.

Ich freue mich, wenn ich irgendwann in Zukunft noch tolle Projekte daraus entstehen sehe. Vielleicht eine schön aufgewertete Timeline. Oder sogar ein etwas größeres Projekt, was automatisch Hashtags mitschreibt und anzeigt, sodass in Zukunft nicht so viel Arbeit notwendig ist.