Python scraping: Daten aus Webseiten herausziehen

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:

  1. Kantonsweite Adresslisten aus Lotse-Seiten (Beispiel) herausziehen
  2. Angebote der Stadt Zürich von der Stadt-Homepage holen
  3. Angebote von Winterthur aus PDFs auf der städtischen Kinderbetreuungsseite extrahieren
  4. 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:

Beteilige dich an der Unterhaltung

3 Kommentare

  1. 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…

  2. 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?

  3. @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.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.