Der Basler Wahlkampf 2016 ist geprägt vom Bürgerlichen Schulterschluss mit der SVP. Die Bürgerlichen betonen, dass die Basler SVP nichts mit den nationalen Entgleisungen ihrer Mutterpartei zu tun habe – die Linken behaupten das Gegenteil. Unter dem Titel „Fakten statt Wahlversprechen“ habe ich am 19. September eine datenjournalistische Analyse des Wahlverhaltens der Parteien im Grossen Rat seit 2013 publiziert. Dafür habe ich die 1502 Abstimmungen der laufenden Legislatur analysiert. Als Gedankenstütze für mich, als Inspiration für Kollegen sowie für Transparenz gegenüber Lesern und Politikern veröffentliche ich hier die Anleitung:
Wie alle meine datenjournalistischen Projekte in letzter Zeit habe ich den Website-Scraper mit PHP als WordPress-Template erstellt. Das erleichtert den Umgang mit Datenbanken enorm, zudem können kleinere Anpassungen via Web-Backend von WordPress gemacht werden, was praktisch ist, wenn am Büro-Computer keine Programme installiert werden können.
Die Rohdaten liegen auf dem Server von bs.ch, geordnet nach Amtsjahr. Mit einem Loop (Jahr+1) und den beiden PHP-Libraries scraperwiki.php und simple_html_dom.php habe ich die URLS der einzelnen PDF-Dateien (hier ein Beispiel) erzeugt. Eine grosse Hilfe beim Umwandeln der PDF-Dateien in Text war der Smalot PDF-Parser. Den ganzen Text-Inhalt der PDFs habe ich danach in eine MySQL-Datei abgespeichert; das brachte viel Zeitersparnis, denn während das Herunterladen und Umwandeln der PDFs rund fünf Sekunden pro Dokument und entsprechend für alle Dokumente mehr als eine Stunde dauert, geschieht die Analyse aus der MySQL-Datenbank in Sekundenbruchteilen, auch wenn darin der ganze Text-Inhalt gespeichert ist.
Der Quellcode fürs Einlesen der PDF-Inhalte in die Datenbank lautet:
/*Links zu pdf-Dateien in Datenbank require_once($_SERVER['DOCUMENT_ROOT'].'/wp-content/themes/twentysixteen/inc/scraperwiki.php'); require_once($_SERVER['DOCUMENT_ROOT'].'/wp-content/themes/twentysixteen/inc/simple_html_dom.php'); set_time_limit(30000); //dieser teil geht die website grosserrat.bs.ch durch und genertiert einträge in die datenbank gr_import $jahr = 2013; while($jahr<2017){//muss 2017 sein! $jahrplus1 = $jahr+1; $url = "http://abstimmungen.grosserrat-basel.ch/index_archiv2_v2.php?path=archiv/Amtsjahr_".$jahr."-".$jahrplus1; $jahr++; $html_content = scraperwiki::scrape($url); $html = str_get_html($html_content); $i = 0; foreach ($html->find("a") as $el) { if($i > 1){ $suburl = "http://abstimmungen.grosserrat-basel.ch/".$el->href; $subhtml_content = scraperwiki::scrape($suburl); $subhtml = str_get_html($subhtml_content); $subi = 0; foreach($subhtml->find("a") as $subel){ if($subi > 3 ){ //pdf holen und als text in die db include 'autoload.php'; // Parse pdf file and build necessary objects. $parser = new \Smalot\PdfParser\Parser(); $pdf = $parser->parseFile("http://abstimmungen.grosserrat-basel.ch/".$subel->href); $text = $pdf->getText(); $wpdb->insert( 'gr_import', array( 'url' => "http://abstimmungen.grosserrat-basel.ch/".$subel->href, 'content' => $text ), array( '%s', '%s' ) ); $durchzaehler++; echo $durchzaehler."<br/>"; } $subi++; } } $i++; //echo $suburl."<br>"; } } */
Für die Analyse der einzelnen PDFs habe ich, kurz zusammengefasst, zwei Durchgänge gemacht. Beim ersten Durchgang habe ich anhand der einzelnen Grossräte das Ergebnis der einzelnen Fraktionen gezählt. Dies wäre zwar auch im PDF erfasst, jedoch schwieriger auslesbar als die einzelnen Stimmen der Grossräte. Zudem lässt sich mit der gewählten Methode auch das Abstimmungsverhalten der einzelnen Grossräte (oder weitere Verhaltensweisen wie das Abwesenheitsverhalten) eruieren. Die Resultete der Fraktionen habe ich in einem mehrdimensionalen Array gespeichert (php gibt zwar einen Fehler aus, lässt dies aber zu – für mich eine super Hilfe beim Analysieren solcher Daten)
$parteien_hilfsarray["N"][$partei]++;
Danach als Doppel-Loop analysiert, welche Fraktionen einander widersprochen haben:
foreach($fraktionen as $fraktion2){ //doppel-loop für matrix //=wenn die aktuelle partei (erster loop) "N" gesagt hat if ($parteien_hilfsarray["N"][$fraktion] > $parteien_hilfsarray["J"][$fraktion]) { //=wenn die zweite-loop-partei "J" gesagt hat if ($parteien_hilfsarray["J"][$fraktion2] > $parteien_hilfsarray["N"][$fraktion2]) { //dann "umstritten" zwischen diesen zwei parteien Nein - Ja $parteienmatrix[$fraktion][$fraktion2]++; } } }
Und dies schliesslich ausgegeben:
echo "<table border=1><tr><td>Uneinigkeit</td>"; foreach($fraktionen as $fraktion){ echo "<td>".$fraktion."</td>"; } echo "</tr>"; foreach($fraktionen as $fraktion){ echo "<tr><td>".$fraktion."</td>"; foreach($fraktionen as $fraktion2){ $parteienabweichung = $parteienmatrix[$fraktion][$fraktion2]+$parteienmatrix[$fraktion2][$fraktion]; echo "<td>".$parteienabweichung."</td>"; } echo "</tr>"; } echo "</table>";
Was folgende Ausgabe ergibt:
Uneinigkeit | CVP/EVP | SP | LDP | FDP | SVP | GB | GLP |
CVP/EVP | 0 | 450 | 200 | 207 | 338 | 504 | 210 |
SP | 450 | 0 | 548 | 573 | 712 | 144 | 384 |
LDP | 200 | 548 | 0 | 139 | 251 | 617 | 277 |
FDP | 207 | 573 | 139 | 0 | 234 | 618 | 266 |
SVP | 338 | 712 | 251 | 234 | 0 | 745 | 385 |
GB | 504 | 144 | 617 | 618 | 745 | 0 | 417 |
GLP | 210 | 384 | 277 | 266 | 385 | 417 | 0 |
aufgearbeitet für Online: