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]:
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)
print("FERTIG!")
Output :
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!
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
… jetzt habe ich es selbst gefunden. Sorry.
Kein Ding! Hat’s funktioniert? Bei weiteren Fragen kannst du auch gern eine Mail schreiben.
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.
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! 🙂
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
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
Hallo Chris,
vielen Dank für die gute Vorlage und die entsprechenden Analysen, wirklich sehr spannend!
Eine Frage zu dem Code: nachdem die keyValues ja doch recht umfangreich sind, wie würde man denn den Code dahingehend anpassen, dass nur einige der interessanteren features ausgelesen werden (e.g. „obj_yearConstructed“, „obj_totalRent“, …)?
Viele Grüße,
Simon
Hallo,
was eigentlich noch fehlt wäre das auslesen, auf welcher Seite man sich auf immobilienscout befindet. Bei mehr Treffer als 20 gibt es ja mehrere Seiten, auf denen die Ergebnisse angezeigt werden.
Hi Patrick,
man befindet sich immer auf Seite 1.
Hallo Chris,
danke erst mal für super spannenden Blogeinträge! Mir ist aufgefallen, dass über die “keyValues” leider keine Variable für das Hausgeld gefunden wird. Hast du eine Idee woran das liegen könnte und wie ich die Variable zu dem Scraper hinzufügen kann? Bei den meisten Postings auf Immobilienscout 24 scheint das Hausgeld auf jeden Fall angegeben zu sein und brauche diese Variable sehr dringend.
Beste Grüße
Jannis
Hallo alle,
ich scrape seit einigen Monaten regelmäßig die Immoscout seite. Dabei mache ich eine Umkreis-Suche. Die Seite mit den Suchergebnissen lässt sich seit etwa einer Woche nicht mehr scrapen. Habt ihr ähnliche Probleme und könnte es dafür eine Lösung geben?
Danke
Laura
Hi Laura,
Immoscout scheint Scraper jetzt zu blockieren, siehe die unteren Kommentare unter dem Beitrag zu den Häuserdaten.
Die Alternative dazu ist entweder mit Selenium einen Browser zu simulieren oder die API zu nutzen. Hättest du Interesse an einem Tutorial für die API? Vorweg: Um die zu nutzen, braucht man einen Developer Account bei Immoscout.
Viele Grüße
Chris
Auch falls jemand anderes eine Idee hat, wie ich das Hausgeld zu den „keyValues“ hinzufügen kann, wäre ich sehr dankbar. 🙂
Beste Grüße
Jannis
Hallo Chris,
ich wollte mich auch gerade ans scrapen machen und musste das gleiche feststellen. Also habe ich mir mal die API Geschichte angesehen und auch einen Zugang bekommen. Für mich sieht es aber danach aus als könnte man nur grundlegende Infos Abfragen zu konkreten Exposés und keine generelle Suche starten. Oder geht das doch?
Ansonsten gehe ich das Thema mit selenium an.
Viele Grüße,
Marius
Hi Marius,
zu Selenium: Hab ich letztens ausprobiert, man wird quasi direkt als Bot erkannt. Ich konnte einmal in der Suche ein paar Exposé IDs abgreifen, aber dann nicht weiter machen…
Also kannst du die API auch als nicht-Content-Partner benutzen? Was sind denn so die Infos zu den Exposés? Weniger als beim Scraper?
Echt schade, dass Immoscout uns das Leben so schwer macht.
Viele Grüße
Chris
Das ist ja eine sehr geniale Darstellung. Danke dafür!
Ich hätte aber irgendwie ein Problem am Anfang:
Wenn ich den Loop ausführe, dann kommt eine für mich nicht klare EOF parcing – Fehlermeldung
File „“, line 9
soup = bs.BeautifulSoup(urllib.request.urlopen(‚https://www.immobilienscout24.de/Suche/S-2/Wohnung-Miete‘).read(), ‚lxml‘)
^
SyntaxError: unexpected EOF while parsing
Kannst Du bitte einen Tipp geben, woran das liegen kann?
Hi Roman,
den Fehler kann ich leider nicht reproduzieren. Bist du sicher, dass du den Code 1:1 kopiert hast? Ist eigentlich ein einfacher Syntaxfehler. Könnte sein, dass eine Zeile fehlt.
Aber da Immobilienscout jetzt Scraper blockt, dürfte der Code eh nicht mehr funktionieren leider, siehe Kommentaren unter dem anderen Beitrag…
Viele Grüße
Chris
Hallo Chris,
ich habe deinen Code bei immowelt.de verwendet. Dazu musste ich ihn bei links etc. anpassen.
Dabei habe ich festgestellt, dass ich immer nur den Inhalt der ersten Seite bekomme.
Wenn ich dem Link aus der Abfrage folge, dann wird auch immer nur die erste Seite angezeigt.
Kannst Du mir eine Empfehlung geben, wie ich das Problem behebe?
Unten der Codeteil, an dem ich jetzt arbeite.
import bs4 as bs
import urllib.request
import time
from datetime import datetime
import pandas as pd
import json
for seite in range(1,5):
print(„Loop “ + str(seite) + “ startet.“)
df = pd.DataFrame()
l=[]
soup = bs.BeautifulSoup(urllib.request.urlopen(„https://www.immowelt.de/gewerbeliste/bl-bayern/renditeobjekte/kaufen?sd=DESC&sf=RELEVANCE&sp“+str(seite)).read(),’lxml‘)
print(„Aktuelle Seite: „+“https://www.immowelt.de/gewerbeliste/bl-bayern/renditeobjekte/kaufen?sd=DESC&sf=RELEVANCE&sp“+str(seite))
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))
Ich ziehe meine Frage zurück. Es funktioniert. Es war nur etwas tricky, weil die das letzte Zeichen in dem Link sich änderte. 🙂
woran ich aber jetzt tatsächlich gescheitert bin, ist der Fehler unten:
ID https://www.immowelt.de/expose/2w7m84g entfernt.
2020-09-04 00:10:31.919064: could not broadcast input array from shape (22) into shape (1)
es liegt wohl an der Beschreibung der Merkmale, die in Form einer Liste einzeln gespeichert werden.
Wenn das Programm versucht, diese zu parsen, schafft es das nicht, weil die Länge anders ist.
Was könnte man hier machen? Kann mir jemand einen Tipp geben?
Hallo Chris,
verstehe ich das nun richtig, dass es damit gerade keine Lösung gibt?
API kriegt ich keinen Zugriff. Selenium und BS wird beides blockiert.
Weist du noch eine Alternative oder hab ich etwas übersehen?
Viele Grüße
Dominik
Hi Chris,
ich wurde wegen dem Scraping blockiert.
Obwohl ich für jeden Loop einen anderen Proxy benutze, habe ich keinen Zugang zur Webseite mehr.
Hast du einen Tipp für mich?
Danke
Nicola
Hi, wenn ich scrape komm keine „a“ mit expose. Also die Liste „l“ ist leer!