Das erste Mal unterwegs mit Python – Ergebnis: Ein experimenteller Datascraper für Kinderbetreuungsangebote im Kanton Zürich.
Die Idee entstand eigentlich mehr per Zufall: Auf der Suche nach granularen Daten – und solchen, die sich mit Adressen auf eine Karte umlegen liessen – stiess ich auf den Kinderbetreuungsindex, eine Auswertung der Betreuungsangebote im Kanton Zürich (Krippen, Horte und so weiter). Von da aus gelangte ich auf lotse.zh.ch, das Portal des Amts für Jugend- und Berufsberatung, das Kontaktadressen für Eltern bereitstellt.
Der dabei entstandene Plan: der wenig hilfreichen Imagemap auf dieser Seite eine Alternative zur Seite zu stellen. Das schien mir eine gute Gelegenheit zu einer ersten, richtigen Fingerübung zu sein. Auf mehrere Webseiten verteilt finden sich hier nämlich Adresslisten von Betreuungsangeboten, die in einer gemeinsamen Kartenübersicht gut aufgehoben wären. Darum sammle ich die Adressdaten aller Zürcher Angebote und mache sie anschliessend in einer Google Map verfügbar.
Auch wenn es das alles auf Anfrage wohl sehr viel einfacher gäbe – der Übung halber versuche ich, mir die Daten selbst zu holen. Und zwar mit einem Scraper, also einem eigens zu diesem Zweck geschriebenen Programm. Ein anschauliches (Code-)Beispiel zum Prinzip gibt es übrigens in Nathan Yaus Flowing Data.
Mein Plan dabei:
- Kantonsweite Adresslisten aus Lotse-Seiten (Beispiel) herausziehen
- Angebote der Stadt Zürich von der Stadt-Homepage holen
- Angebote von Winterthur aus PDFs auf der städtischen Kinderbetreuungsseite extrahieren
- Alle Adressdaten zusammenführen und auf eine Google Map umlegen
Für die erste Etappe greife ich ungeachtet fehlender Erfahrung auf Python zurück. Das bietet eine einfache Syntax und einige Komfortfeatures, die Programmieranfängern viel Arbeit abnehmen. Vor allem hat Python eine gewisse Verbreitung bei Datenarbeitern und hält verschiedene Module bereit, die einem beim Scraping (also dem computergestützten Zusammensuchen) von Daten unter die Arme greifen. Zum Beispiel das nett benannte BeautifulSoup, das sich gerade für das Auslesen von HTML-Dokumenten gut eignet.
Das Lernen von Python geht übrigens mit den vielen verfügbaren Online-Tutorials recht einfach von der Hand. Erst im Anschluss bin ich auf Tobias Kuts sehr hübsche Sammlung von Pythonressourcen gestossen.
Hier der etwas ungelenkte Code meines ersten Python-Experiments. Dazu ist zu sagen, dass es die Website Datensammlern nicht besonders einfach macht. Die einzelnen Angaben finden sich zwar übersichtlich auf einer Seite, sind aber nicht sehr einheitlich und im HTML-Code kaum sinnvoll ausgezeichnet. Etwas Gewurstel war darum nötig, um die Einträge zu erhalten.
"""This is a data scraper for the crawling and searching of child care facilities in the canton of Zürich, Switzerland, from the website http://www.lotse.zh.ch. Code: Jan Rothenberger, CC 2.0 BY NC""" import os import sys import csv import re #reguläre Ausdrücke, brauchen wir später from bs4 import BeautifulSoup #BeautifulSoup: unser Werkzeug der Wahl import urllib.request webliste = [] #Liste mit den zu scrapenden URLs, Typen webliste.append(("kita","http://www.lotse.zh.ch/service/detail/500076/from/service?q=kinderbetreuung&qID=k500502")) webliste.append(("kihu","http://www.lotse.zh.ch/service/detail/500101/from/service?q=kinderbetreuung&qID=k500502")) webliste.append(("mita","http://www.lotse.zh.ch/service/detail/500078/from/service?q=kinderbetreuung&qID=k500502")) webliste.append(("hort","http://www.lotse.zh.ch/service/detail/500077/from/service?q=kinderbetreuung&qID=k500502")) def lotse_scrapen(): alles = [] zeile = "" for unterseite in webliste: #läuft die kategorieseiten in der webliste ab und wendet datenholen darauf an Typ_angebot = unterseite[0] seite = urllib.request.urlopen(unterseite[1]) alles.extend(datenholen(seite,Typ_angebot)) typen = {"kita" : "Kinderkrippen oder Kindertagesstätte","kihu" : "Kinderhütedienst", "mita" : "Mittagstisch", "hort" : "Hort"} f = open("datadump.csv", 'wt', newline='\n') try: fieldnames = ['type','name', 'contact', 'address', 'tel','web','infos'] writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter=';', extrasaction='raise') writer.writerow({fn:fn for fn in fieldnames}) for entry in alles: try: writer.writerow(entry) except: print(entry) finally: f.close() # print open(sys.argv[1], 'rt').read() def datenholen(response,typ_Angebot): #nimmt kategorieseite und typ, gibt listenabschnitt mit einträgen zurück datensatz = [] #dictionary-liste, wird gefüllt und zurückgegeben eintraege = [] #webadressen, die von der unterfunktion gescrapet werden listen_soup = BeautifulSoup(response) results = listen_soup.findAll('a', attrs={'class' : 'mehr'}) #links der Klasse "mehr" finden for result in results: eintraege.append(result['href']) #link-url in liste eintragen y = 0 for eintrag in eintraege: #unterfunktion auf elementen der url-liste aufrufen datensatz.append(eintrag_machen(("http://www.lotse.zh.ch" + eintrag),typ_Angebot)) return datensatz def eintrag_machen(eintrag,typ_angebot): entry = {} entry['type'] = "" entry['type'] = typ_angebot entry['name'] = "" entry['contact'] = "" entry['address'] = "" entry['tel'] = "" entry['web'] = "" entry['infos'] = "" try: page = urllib.request.urlopen(eintrag) soup = BeautifulSoup(page) entry['name'] = soup.b.string liste = [word.strip() for word in soup.b.string.find_all_next(text=True)] #von \n gesäuberte Liste while '' in liste: liste.remove('') #alle leere Elemente (strings) werden entfernt i = 0 #schluss abschneiden for teil in liste: if teil == "Zurück" or teil == "#BeginCopy'": del liste[i:] #schneidet nach i ab, vor "Zurück" break else: i = i + 1 for teil in liste: #Tel und URLs hinzufügen if teil.startswith("Tel."): entry['tel'] = teil elif teil.startswith("www.") or teil.startswith("www."): entry['web'] = teil elif teil.startswith("var"): liste.remove(teil) i = 0 while ((not re.search(r"[0-9]",liste[i])) and (not liste[i].endswith("strasse"))): entry['contact'] = liste[i] + " " + entry['contact'] i = i + 1 try: entry['address'] = liste[i] # zusätzliche Adressoder PLZ hinzufügen plz = (re.search(r"^[0-9]{4} [A-Za-z]*", liste[i])) #4-stellige Zahl im Listeneintrag finden if not plz: #plz nicht auf dieser zeile if re.search(r"^[0-9]{4} [A-Za-z]*", liste[i + 1]): #plz ist auf der folgezeile entry['address'] = entry['address'] + ", " + liste[i + 1] # Noch nicht PLZ, weitere Adresszeile, darum PLZ hinzufügen else: return False #keine plz gefunden, kein mapping möglich -> abbruch except: print("an der url-geschichte liegts") i = 1 #anfang abschneiden for teil in liste: if teil.startswith("Lageplan") or teil.startswith("Informationen"): del liste[:i] #schneidet mit i ab, nach "Lageplan" else: i = i + 1 entry['infos'] = ",".join(liste) return entry except: print("doof, das") return False
Die resultierenden Daten gibt es in der folgenden Tabelle oder hier: Spreadsheet Kinderbetreuung
Das Programm generiert übrigens eine CSV-Datei, die sich aber leicht zu Google hochladen lässt. Hier das Resultat als Spreadsheet:
Danke für den Artikel. Und zwar nicht nur wegen dem netten Backlink (freut mich, wenn die Resourcensammlung gefällt. Sollte ich eigentlich mal wieder updaten…), sondern vor allem dem praktischen Codebeispiel. Anwendungsfälle sind immer gern gesehen.
Spontan hätte ich jetzt eigentlich auch +1 klicken wollen… nur der Button, der fehlt…
Hi,
google hat mich hierher gebracht. Ich versuche etwas ähnliches zu bewerkstelligen. Ich habe versucht deinen Code 1:1 zu kopieren und zu verwenden. Ich verwende Eclipse Mars mit PyDev. Aber dein Code läuft hier nicht. Es scheitert bereits bei der ersten Zeile import. Hast du evtl. ein paar Tips?
@Sebastian, die meisten Codes, die veröffentlicht werden funktionieren nicht, da die „Programmierer“ fast jedes Mal vergessen, die Programmiersprache SOWIE DIE VERSION (Python 2.7, 3.x usw) anzugeben. Hier ist aber etwas anderes das Problem. Wenn Du diesen Code kopierst und ihn in Deinen Editor einfügst, sind die Codezeilen um einen Tabulator-Schritt nach rechts verschoben. Markiere am besten den ganzen Code in Deinem Editor und rücke ihn um einen Tabulator-Schritt nach links, – dann sollte es funktionieren.