ImmobilienScout24 Mining Tutorial: Der Web Scraper

Der Code genau erklärt

Jetzt gehen wir detailliert jeden Arbeitsschritt des Scrapers durch. Generell muss man sagen, dass ein manuell gebauter Web Scraper für jede Internetseite neu justiert werden muss. Selbst für die Seite von AutoScout24  (die der von ImmobilienScout24 sehr ähnlich ist) kann man den hier gezeigten Web Scraper nicht 1:1 verwenden, sondern muss einige Änderungen vornehmen. Für ImmobilienScout24 kannst du den Code aber kopieren und er sollte einwandfrei funktionieren.

Importe

Zuerst werden die Module in Python importiert, auf die der Web Scraper zurückgreift.

Input [1]:

import bs4 as bs

Mit BeautifulSoup kann der gesamte Quellcode einer Seite in einem Objekt erfasst werden. Innerhalb dieses Objektes kann man z.B. nach HTML-Tags suchen und deren Inhalt auslesen. BeautifulSoup ist das zentrale Modul, wenn es um das Auslesen von HTML geht.

import urllib.request 

Mit diesem Modul – bzw. seiner request-Funktion – öffnen wir die URL, deren Inhalt wir in einem BeautifulSoup-Objekt speichern wollen.

import time 

Mit diesem Modul kann unter anderem ein laufendes Programm für beliebige Zeit unterbrochen werden.

from datetime import datetime 

Dieses Modul und dessen gleichnamige Funktion benötigen wir, um die gesammelten Daten mit Timestamps zu versehen. So können wir nachvollziehen, zu welchem Zeitpunkt welches Angebot gespeichert wurde.

import pandas as pd

Pandas ist der Eckpfeiler für die Manipulation von tabellarisch vorliegenden Daten. Genau dafür werden wir es auch brauchen.

import json

Hiermit können Objekte im JSON-Format in Python eingelesen werden. Diese werden in unserem Fall in Dictionaries umgewandelt

import os

Dieses Modul brauchen wir, um auf die Ordnerstruktur in Windows zugreifen zu können.

Hauptskript

Nachdem die nötigen Module importiert wurden, kommt nun der Hauptteil des Web Scrapers.

Input [2]:

Zuerst werden ein paar Variablen definiert.
max_time = 84600

Legt eine Zeit (in Sekunden) fest, mit der später die Laufzeit des Programms definiert wird. 84.600 Sekunden entsprechen 24 Stunden. Falls du den Scraper länger – zum Beispiel eine Woche lang – laufen lassen willst, kannst du diesen Wert entsprechend anpassen.

start_time = time.time()

Misst die Zeit in Sekunden, die vom 01.01.1970 bis jetzt vergangen ist. Die Funktion ist nützlich, da ihr Wert kontinuierlich ansteigt und sich leicht mit ihr rechnen lässt. Sie ist ein weiterer Bestandteil zur Bestimmung der Laufzeit des Scrapers.

count = 1

Ein Zähler wird definiert, der die Anzahl der abgeschlossenen Durchgänge speichert.

while (time.time() - start_time) < max_time:

Der Großteil des Programms läuft in einem while-Loop ab, weil der Scraper so oft wir wollen dieselben Arbeitsschritte wiederholen soll. Die hier definierte Schleife wird so lang durchlaufen, wie die Differenz zwischen der Startzeit und der jetzigen Zeit kleiner ist als die oben festgelegte Laufzeit.

print("Loop " + str(count) + " startet.")

Zeit im Output, der wievielte Loop startet.

df = pd.DataFrame()

Initiiert mit einen leeren DataFrame, ein Tabellenobjekt ähnlich einer Matrix. Fast immer werden Rohdaten, welche in Python analysiert werden sollen, zuerst in einen DataFrame geladen. Quasi eine Excel-Tabelle auf Steroiden.

l=[]

Initiiert eine leere Liste, die später mit URLs gefüllt wird.

try:
    soup =    bs.BeautifulSoup(urllib.request.urlopen('https://www.immobilienscout24. de/Suche/S-2/Wohnung-Miete').read(),'lxml')

Definiert ein BeautifulSoup-Objekt innerhalb eines try-Blocks. Zu dieser Art der Fehlererkennung komme ich später noch mal. “soup” enthält jetzt den gesamten Quellcode der angegebenen URL.

for paragraph in soup.find_all("a"):
    if r"/expose/" in str(paragraph.get("href")): 
        l.append(paragraph.get("href").split("#")[0]) 
        l = list(set(l))

Durchsucht “soup” nach <a>-Tags, also nach Hyperlinks. Jeder Hyperlink, der den Text “/expose/” enthält, wird in der Liste “l” gespeichert. Da an manchen Hyperlinks noch ein “#” gefolgt von weiterem Text hängt, habe ich diesen Teil jeweils abgeschnitten. Abschießend werden alle Duplikate aus “l” entfernt, da im Quelltext der durchsuchten Seite fast alle relevanten URLs mehrmals zu finden sind. Auf diese soll jedoch nur einmal zugegriffen werden.

for item in l:
    try:
        soup = bs.BeautifulSoup(urllib.request.urlopen('https://www.immobilienscout24.de'+item).read(),'lxml')

Hier geht der Scraper Schritt für Schritt die Liste “l” durch und besucht jede der URLs. Die folgenden Schritte werden bei jeder URL in der Liste durchgeführt.

data = pd.DataFrame(json.loads(str(soup.find_all("script")).split("keyValues = ")[1].split("}")[0]+str("}")),index=[str(datetime.now())])

Der Quellcode wird nach <script>-Tags durchsucht. Innerhalb dieser Tags sucht der Scraper nach dem Wort “keyValues”. Hier befinden sich nämlich alle relevanten Daten zur Wohnung. Im Quelltext von ImmobilienScout24 sieht das ungefähr so aus:

keyValues = {"obj_regio1":"Sachsen",
"obj_serviceCharge":"545",
"obj_heatingType":"central_heating",
"ga_cd_via_qualified":"true",
"obj_telekomTvOffer":"ONE_YEAR_FREE",
"obj_cId":"923504","obj_newlyConst":"n",
"obj_balcony":"y",
"obj_picture":"https://pic.immobilienscout24.de/pic/orig04/N/638/449/619/638449619-0.jpg/ORIG/resize/118x118%3E/extent/118x118/format/jpg/quality/80",
"obj_electricityBasePrice":"90.76",
"obj_picturecount":"15",
"obj_pricetrend":"5.56",
"obj_telekomUploadSpeed":"40 MBit/s",
"obj_totalRent":"1800",
"obj_telekomTrackingGroup":"telekom_layer_magenta_l",
"obj_telekomInternetTechnology":"über VDSL",
"obj_yearConstructed":"1910",

[...]

"obj_noRoomsRange":"5",
"obj_garden":"n",
"obj_barrierFree":"n",
"obj_regio3":"Zentrum_Nord",
"obj_objectnumber":"7710",
"obj_livingSpaceRange":"7",
"obj_regio2":"Leipzig"}

Solche Codeblöcke muss man natürlich erst mal entdecken. Deshalb kommt man um eine genaue Durchleuchtung des Quellcode fast nie herum.

Anschließend wird der gesamte Inhalt zwischen den geschwungenen Klammern als json-Objekt eingelesen und im DataFrame “data” gespeichert.

data["URL"] = str(item)

Zum Dataframe wird ein weiteres Merkmal, nämlich die URL der Wohnung, hinzugefügt.

beschreibung = [] 

for i in soup.find_all("pre"): 
    beschreibung.append(i.text)

data["beschreibung"] = str(beschreibung)

Ein Liste “beschreibung” wird definiert. In dieser Liste wird der Inhalt aller <pre>-Tags gespeichert. Dies umfast den gesamten Fließtext, der zusätzlich zu den Eckdaten in einem Wohnungsangebot zu lesen ist. Dort stehen dann Dinge wie “großer Balkon mit Aussicht auf gepflegten Garten” oder “ruhige Lage”. Mal schauen, ob sich diese Beschreibung sinnvoll analysieren lassen. Aber erst mal haben ist besser als einfach darauf zu verzichten.

df = df.append(data)

Der DataFrame “df” wird um den DataFrame “data” ergänzt. Das heißt mit jeder Wohnung wird “df” eine Zeile länger.

except Exception as e: 

    print(str(datetime.now())+": " + str(e)) 
    l = list(filter(lambda x: x != item, l))
    print("ID " + str(item) + " entfernt.")

Mir ist es oft passiert, dass ein Wohnungsangebot wieder verschwand, nachdem dessen URL schon die Liste geschrieben wurde.  Fall dieser oder ein anderer Fehler auftaucht, wird er vom Scraper ausgegeben. Anschließend wird die betreffende URL aus der Liste gelöscht und übersprungen.

df.to_csv(".../Rohdaten/"+str(datetime.now())[:19].replace(":","").replace(".","")+".csv",sep=";",decimal=",",encoding = "utf-8",index_label="timestamp")

Die 20-zeilige Tabelle wird in eine csv-Datei geschrieben und in einem Ordner abgelegt. Dabei wird der Name jeder Datei mit einem Timestamp versehen. Zum einen ist die Datei damit leichter identifizierbar und zum anderen vermeidet man so, dass es zwei Dateien mit demselben Namen gibt.

print("Loop " + str(count) + " endet.")  
count+=1 
time.sleep(60)

Gibt die Nummer des Loops aus, der gerade endet. Erhöht den Wert des Counters um 1 und schläft dann 60 Sekunden, damit bei einem erneuten Besuch die Angebote bei ImmobilienScout24 aktualisiert wurden.

except Exception as e: 
    print(str(datetime.now())+": " + str(e)) 
    time.sleep(60)
Das ist der Schlussteil des ersten try-Blocks. Sollte also das ganze Programm zwischenzeitlich nicht funktionieren – weil zum Beispiel die Internetverbindung unterbrochen wurde – dann schläft der Scraper für eine Minute und startet danach erneut. Außerdem wird die Fehlersuche umgemein dadurch erleichtert, dass auch die Art des Fehlers vom Scraper ausgegeben wird.
print("FERTIG!")
Der Scraper ist fertig!

 Output :

Loop 1 startet.
Loop 1 endet.
Loop 2 startet.
Loop 2 endet.
Loop 3 startet.
Loop 3 endet.
Loop 4 startet.
Loop 4 endet.
[...]
Loop 416 startet.
Loop 416 endet.
Loop 417 startet.
2017-11-18 21:39:24.489641: HTTP Error 404: Not Found
ID /expose/100998482 entfernt.
Loop 417 endet.
[...]
Loop 1161 startet.
Loop 1161 endet.
Loop 1162 startet.
Loop 1162 endet.
Loop 1163 startet.
Loop 1163 endet.
FERTIG!

So sieht dann der Output des Scrapers aus, nachdem er 24 Stunden lang gelaufen ist. Er zeigt also den Start und das Ende jedes Loops an sowie die Fehler, die zwischenzeitlich auftauchen, wie zum Beispiel in Loop 417. Dort wurde die URL nicht mehr gefunden und daher gelöscht.

Dateien zusammenführen

Jetzt haben wir einen Ordner mit 1163 Dateien mit Wohnungsdaten. Um diese zu analysieren, brauchen wir eine Tabelle mit allen Daten. Und das funktioniert wie folgt.

Input[3]:

df = pd.DataFrame()

count=1

for i in os.listdir(".../Rohdaten/"):
    print(str(count)+". Datei: "+str(i))
    count+=1
    df = df.append(pd.read_csv(".../Rohdaten/" + str(i),sep=";",encoding="utf-8",decimal=","))

Ein leerer DataFrame “df” wird definiert, der später alle Daten enthalten wird. Die for-Schleife iteriert über alle im Ordner befindlichen Dateien. In jedem Schritt wird der DataFrame “df” um die Daten der CSV-Datei ergänzt, die gerade eingelesen wird.

Input[4]:

df.shape

Output[4]:

(22332, 89)

Mit diesem Befehl sehen wir die Dimensionen der Tabelle. Vor dem löschen der Duplikate zeigt der Output 22332 Zeilen und 89 Spalten.

Input[5]:

df = df.drop_duplicates(subset="URL")

Zum löschen der doppelten Einträge nutzen wir die Spalte “URL”, welche die ID jedes Angebotes enthält. Das Merkmal wird in jeder Zeile überprüft und kommt dieselbe URL schon weiter oben vor, dann wird die komplette Zeile gelöscht.

Input[6]:

df.shape

Output[6]:

(2326, 89)

Wenn wir den vorigen Befehl noch einmal ausführen, dann sehen wir, dass die Dimensionen der Tabelle nun extrem zusammengeschrumpft sind. Bei meinem 24h-Testlauf wurden 90% der Zeilen gelöscht. Aber lieber viele Duplikate als viele Wohnungsangebote, die nicht vom Web Scraper erfasst wurden.

Input[7]:

df.to_csv("D:/Immobilienscout24/Final.csv",sep=";",encoding="utf-8",decimal=",")

Jetzt können wir die fertige Tabelle wieder auf die Festplatte exportieren, damit sie uns bei jeder neuen Analyse sofort zur Verfügung steht und wir nicht jedes Mal erneut die einzelnen Dateien neu zusammenführen müssen. Vor allem, wenn du über einen längeren Zeitraum scrapen willst, solltest du diesen Rat befolgen. Nach einem Monat über 30.000 Dateien zu vereinigen kann nämlich einige Stunden dauern!

Fertig!

Wir haben ein Programm erstellt, das in von uns festgelegten Intervallen ImmobilienScout24 aufsucht, die neusten Einträge speichert und ablegt. Dann haben wir die heruntergeladenen Dateien in Python zusammengeführt und die Duplikate entfernt. Die fertige Tabelle haben wir wieder in einem Ordner abgelegt, um jederzeit auf unsere gesäuberte Datenbasis zurückgreifen zu können.

Im nächsten Teil der Artikelreihe wird es um explorative Datenanalyse gehen, aber jetzt erst mal viel Spaß beim scrapen!

7 Gedanken zu „ImmobilienScout24 Mining Tutorial: Der Web Scraper

  • 11. Februar 2018 um 13:43
    Permalink

    Hallo,

    danke für die detaillierte Erklärung der einzelnen Codezeilen. Beim nachbauen habe ich allerdings die Orientierung verloren, welche COdezeile zu welchem Block (Try, For, etc.) gehört. Könntest Du vielleicht nochmal den kompletten Code posten, damit man sieht, wie die Einrückungen sind?

    Danke
    ein Pythonanfänger

    Antwort
    • 11. Februar 2018 um 13:49
      Permalink

      … jetzt habe ich es selbst gefunden. Sorry.

      Antwort
      • 15. Februar 2018 um 20:02
        Permalink

        Kein Ding! Hat’s funktioniert? Bei weiteren Fragen kannst du auch gern eine Mail schreiben.

        Antwort
  • 13. August 2018 um 00:26
    Permalink

    Danke für das Tutorial! Sehr gut erklärt.
    Ich möchte mit der gesamelten Data noch ein paar Rechnungen innerhalb des Skriptes einbauen.
    Wie könnte ich z.B. Python jetzt sagen “mit obj_purchasePrice und obj_livingSpace rechne mir den m² -Preis”? Leider bin ich Anfänger und komme hier nicht weiter.

    Antwort
    • 13. August 2018 um 10:20
      Permalink

      Hi Matthias,

      danke für das Feedback!

      Zu deiner Frage: Wenn du das Tutorial abgeschlossen hast, liegen ja alle Daten gesammelt in einem DataFrame vor. Mit den Spalten des DataFrames kannst du natürlich auch weitere Berechnungen durchführen. Um mit Hilfe der Merkmale “obj_purchasePrice” und “obj_livingSpace” eine neue Spalte mit dem Quadratmeterpreis zu erstellen, machst du folgendes:

      df[“qm_preis”] = df.obj_purchasePrice/df.obj_livingSpace

      oder

      df[“qm_preis”] = df[“obj_purchasePrice”]/df[“obj_livingSpace”]

      Beide Befehle äquivalent zueinander. In derselben Weise kannst du mit “*”,”+” und “-” auch Spalten miteinander multiplizieren, addieren und voneinander subtrahieren.

      Sag Bescheid, wenn’s geklappt hat! 🙂

      Antwort
  • 25. September 2018 um 08:54
    Permalink

    Hallo Cris
    ist es auch möglich nicht nur aktuelle Daten auszulesen, oder besteht die Möglichkeit auch historische
    Immobiliendaten beim Scout auszulesen.
    Finde deine Projekte extrem spannend.

    LG

    Klaus

    Antwort
    • 30. September 2018 um 11:44
      Permalink

      Hi Klaus,

      danke für dein Feedback! 🙂
      Natürlich kannst du generell nur das scrapen, was sich auch auf der Seite finden lässt. Allerdings gibt es die Möglichkeit, nicht nur immer die neuen Angebote auf der ersten Seite zu scrapen, sondern alle Angebote auszulesen, die im Moment online sind. Genau das habe ich im Beitrag zu Häusern auf ImmobilienScout24 gemacht.

      Der Web Scraper für Häuser

      Bei Fragen schreib mir gern auf meiner Facebookseite eine Nachricht. -> StatisQuo auf Facebook

      Viele Grüße

      Chris

      Antwort

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.