Sitzung 14 Serialisierung
14.2 Zielsetzung
Auf https://www.wg-gesucht.de/ finden sich Anzeigen für WGs. In der Listenansicht werden pro Seite 20 Angebote überblicksartig angezeigt. Bei Click auf ein Angebot erscheinen Details der Anzeige, für die wir uns als Rohdaten interessieren.
Wir wollen…
- … für die ersten drei Überblicksseiten automatisiert alle URLs der einzelnen Anzeigen auslesen (was sich aber in der Praxis erweitern ließe).
- … für diese 60 URLs automatisiert die folgenden Details auslesen
- Gesamtmiete
- Zimmergröße
- Wer wohnt dort?
Beide Zielsetzungen können in folgende Schritte unterteilt werden:
- An einem Beispiel konkret ausführen
- Abstrahieren (hier: als Funktion)
- Testen an weiteren einzelfällen
- Serialisiert ausführen (hier: mit
map
o.ä.)
14.3 URLs der Anzeigen auslesen
14.3.1 Schritt 1: An einem Beispiel konkret ausführen
In Sitzung 11 haben wir gelernt, wie mit dem Paket rvest
HTML-Seiten in R geladen und einzelne Elemente angesporchen werden können.
Wenn man sich die erste Listenansicht genau anschaut (mit Developer Tools / Inspect Element vom Browser), wird deutlich, dass die <tr>
-Tags mit class=offer_list_item
die einzelnen Anzeigen enthalten. Ebenfalls im <tr>
-Element enthält das Attribut adid
den entscheidenden Teil der Anzeigen-URL.
Deshalb können wir schreiben:
"https://www.wg-gesucht.de/wg-zimmer-in-Frankfurt-am-Main.41.0.0.0.html" %>%
read_html() %>%
html_nodes("tr.offer_list_item") %>%
html_attr("adid")
## [1] "wg-zimmer-in-Frankfurt-am-Main-Westend-Nord.8414807.html"
## [2] "wg-zimmer-in-Frankfurt-am-Main-Westend-Nord.7693038.html"
## [3] "wg-zimmer-in-Frankfurt-am-Main-Sachsenhausen.9188814.html"
## [4] "wg-zimmer-in-Frankfurt-am-Main-Westend-Nord.7695351.html"
## [5] "wg-zimmer-in-Frankfurt-am-Main-Nordend-Ost.9978425.html"
## [6] "wg-zimmer-in-Frankfurt-am-Main-Bahnhofsviertel.9890616.html"
## [7] "wg-zimmer-in-Frankfurt-am-Main-Bahnhofsviertel.9769842.html"
## [8] "wg-zimmer-in-Frankfurt-am-Main-Westend-Nord.8838371.html"
## [9] "wg-zimmer-in-Frankfurt-am-Main-Westend-Nord.10009459.html"
## [10] "wg-zimmer-in-Frankfurt-am-Main-Innenstadt.8591977.html"
## [11] "wg-zimmer-in-Frankfurt-am-Main-Westend-Nord.8687634.html"
## [12] "wg-zimmer-in-Frankfurt-am-Main-Gutleutviertel.9578685.html"
## [13] "wg-zimmer-in-Frankfurt-am-Main-Westend-Nord.8523043.html"
## [14] "wg-zimmer-in-Frankfurt-am-Main-Nordend-West.1098997.html"
## [15] "wg-zimmer-in-Frankfurt-am-Main-Westend-Nord.8555199.html"
## [16] "wg-zimmer-in-Frankfurt-am-Main-Westend-Sued.9129828.html"
## [17] "wg-zimmer-in-Frankfurt-am-Main-Bahnhofsviertel.9966249.html"
## [18] "wg-zimmer-in-Frankfurt-am-Main-Nordend-West.7418667.html"
## [19] "wg-zimmer-in-Frankfurt-am-Main-Westend-Nord.9104792.html"
## [20] "wg-zimmer-in-Frankfurt-am-Main-Frankfurt-Gallus.8646284.html"
14.3.2 Schritt 2: Abstrahieren
Der obige Code lässt sich als Funktion abstrahieren, die eine URL als Input hat (s. Sitzung 12):
<- function(list_url) {
get_url_list %>%
url read_html() %>%
html_nodes("tr.offer_list_item") %>%
html_attr("adid")
}
14.3.4 Schritt 4: Serialisiert ausführen
Schließlich können wir die Funktion auf eine Reihe von Inputs anwenden. Einen Vektor mit den gewünschten Input-URLs können wir erstellen mit:
paste0("https://www.wg-gesucht.de/wg-zimmer-in-Frankfurt-am-Main.41.0.0.",0:4,".html")
## [1] "https://www.wg-gesucht.de/wg-zimmer-in-Frankfurt-am-Main.41.0.0.0.html"
## [2] "https://www.wg-gesucht.de/wg-zimmer-in-Frankfurt-am-Main.41.0.0.1.html"
## [3] "https://www.wg-gesucht.de/wg-zimmer-in-Frankfurt-am-Main.41.0.0.2.html"
## [4] "https://www.wg-gesucht.de/wg-zimmer-in-Frankfurt-am-Main.41.0.0.3.html"
## [5] "https://www.wg-gesucht.de/wg-zimmer-in-Frankfurt-am-Main.41.0.0.4.html"
Diese Liste ließe sich natürlich erweitern.
Mit map()
aus dem purrr
-Paket (Teil von tidyverse
) lässt sich dann unsere Funktion get_url_list()
auf alle Elemente dieses Vektors anwenden. Das vorläufige Resultat ist eine Liste der Länge 3, wobei jedes Element wiederum ein Vektor mit 20 Elementen ist.
Der Befehl map_chr()
dampft das Ergebnis dann direkt auf einen einfachen Character-Vektor ein.
Am Ende wird das Resultat mit paste0()
an den Domainnamen gehängt und dem Objektnamen anzeige_urls
zugewiesen.
<-
anzeige_urls "https://www.wg-gesucht.de/wg-zimmer-in-Frankfurt-am-Main.41.0.0." %>%
paste0(0:2,".html") %>%
map(get_url_list) %>%
flatten_chr() %>%
paste0("https://www.wg-gesucht.de/", .)
str(anzeige_urls)
## chr [1:60] "https://www.wg-gesucht.de/wg-zimmer-in-Frankfurt-am-Main-Westend-Nord.8414807.html" ...
14.4 Informationen der Anzeigen auslesen
Jetzt ginge es darum, für jede dieser 60 URLs die relevanten Informationen rauszusuchen:
- Gesamtmiete
- Zimmergröße
- Wer wohnt dort?
Auch hier gehen wir für die Automatisierung in den drei Schritten vor.
14.4.1 Schritt 1: An einem Beispiel konkret ausführen
Zunächst eine Beispielanzeige laden und zwischenspeichern:
<-
site "https://www.wg-gesucht.de/wg-zimmer-in-Frankfurt-am-Main-Nordend-West.1098997.html" %>%
read_html()
Dann lassen sich Quadratmeterzahl und Gesamtmiete recht einfach auslesen, weil beide in einer h2
-Überschrift mit class="headline-key-facts"
stecken:
<-
key_facts %>%
site html_nodes("h2.headline-key-facts") %>%
html_text() %>%
trimws()
<- key_facts[1]
qm <- key_facts[2]
eur
qm## [1] "20m²"
eur## [1] "250€"
Die Information, wer dort wohnt, steckt in einem span title:
<-
bewohnerinnen %>%
site html_node("h1#sliderTopTitle") %>%
html_node("span") %>%
html_attr("title")
bewohnerinnen## [1] "6er WG (0w,4m,0d)"
Geballt lassen sich die Daten für eine Anzeige so ausgeben:
c(qm, eur, bewohnerinnen)
## [1] "20m²" "250€" "6er WG (0w,4m,0d)"
14.4.2 Schritt 2: Abstrahieren
Wir abstrahieren die obigen Schritte als Funktion:
<- function(anzeige_url) {
get_anzeige_details
# "Pausiert" die Abfrage für 2 Sekunden
Sys.sleep(2)
<-
site %>%
anzeige_url read_html()
<-
key_facts %>%
site html_nodes("h2.headline-key-facts") %>%
html_text() %>%
trimws()
<- key_facts[1]
qm <- key_facts[2]
eur
<-
bewohnerinnen %>%
site html_nodes("h1#sliderTopTitle") %>%
html_nodes("span") %>%
html_attr("title")
# Der letzte Befehl ist immer die "return value"
c(qm, eur, bewohnerinnen)
}
Der Befehl Sys.sleep(2)
sorgt dafür, dass die Funktion bei jeder Ausführung erst mal zwei Sekunden „schläft“. Das ist leider nötig um zu verhindern, dass unsere IP automatisch gesperrt wird. Dadurch verlängert sich die Ausführung natürlich enorm.
14.4.3 Schritt 3: Testen
Wir führen die Funktion probeweise für eine (andere) Anzeige aus:
"https://www.wg-gesucht.de/wg-zimmer-in-Frankfurt-am-Main-Nordend-West.1098997.html" %>%
get_anzeige_details## [1] "20m²" "250€" "6er WG (0w,4m,0d)"
Klappt!
14.4.4 Schritt 4: Serialisiert ausführen
Jetzt noch der Trick, diese Funktion mit map()
auf alle 60 URLs auszuführen. Damit das hier aber klappt, ohne dass wir gesperrt werden, beschränke ich vorab auf die ersten zehn Einträge mit head()
:
<-
results %>%
anzeige_urls head(10) %>%
map(get_anzeige_details)
results## [[1]]
## [1] "20m²" "1000€" "3er WG (1w,1m,0d)"
##
## [[2]]
## [1] "20m²" "900€" "3er WG (1w,1m,0d)"
##
## [[3]]
## [1] "15m²" "310€" "9er WG (0w,6m,0d)"
##
## [[4]]
## [1] "20m²" "950€" "3er WG (1w,1m,0d)"
##
## [[5]]
## [1] "19m²" "770€" "2er WG (0w,0m,0d)"
##
## [[6]]
## [1] "20m²" "900€" "3er WG (1w,1m,0d)"
##
## [[7]]
## [1] "15m²" "900€" "3er WG (1w,1m,0d)"
##
## [[8]]
## [1] "12m²" "850€" "3er WG (1w,1m,0d)"
##
## [[9]]
## [1] "15m²" "900€" "3er WG (1w,1m,0d)"
##
## [[10]]
## [1] "15m²" "900€" "3er WG (1w,1m,0d)"
Mit map_chr()
können wir aus dem Ergebnis direkt einen tibble basteln:
<-
wgs tibble(
link = head(anzeige_urls, 10),
flaeche = map_chr(results, 1),
preis = map_chr(results, 2),
bewohnerinnen = map_chr(results, 3)
)
wgs## # A tibble: 10 × 4
## link flaeche preis bewohnerinnen
## <chr> <chr> <chr> <chr>
## 1 https://www.wg-gesucht.de/wg-zimmer-in-Frankfurt… 20m² 1000€ 3er WG (1w,1…
## 2 https://www.wg-gesucht.de/wg-zimmer-in-Frankfurt… 20m² 900€ 3er WG (1w,1…
## 3 https://www.wg-gesucht.de/wg-zimmer-in-Frankfurt… 15m² 310€ 9er WG (0w,6…
## 4 https://www.wg-gesucht.de/wg-zimmer-in-Frankfurt… 20m² 950€ 3er WG (1w,1…
## 5 https://www.wg-gesucht.de/wg-zimmer-in-Frankfurt… 19m² 770€ 2er WG (0w,0…
## 6 https://www.wg-gesucht.de/wg-zimmer-in-Frankfurt… 20m² 900€ 3er WG (1w,1…
## 7 https://www.wg-gesucht.de/wg-zimmer-in-Frankfurt… 15m² 900€ 3er WG (1w,1…
## 8 https://www.wg-gesucht.de/wg-zimmer-in-Frankfurt… 12m² 850€ 3er WG (1w,1…
## 9 https://www.wg-gesucht.de/wg-zimmer-in-Frankfurt… 15m² 900€ 3er WG (1w,1…
## 10 https://www.wg-gesucht.de/wg-zimmer-in-Frankfurt… 15m² 900€ 3er WG (1w,1…
14.5 Aufbereiten
Um mit den Daten sinnvoll weiterzuarbeiten müssen sie in ein numerisches Format gebracht werden. Das funktioniert z. T. am besten mit regulären Ausdrücken (regex
), denen wir uns in der nächsten Sitzung widmen werden.
$bewohnerinnen
wgs## [1] "3er WG (1w,1m,0d)" "3er WG (1w,1m,0d)" "9er WG (0w,6m,0d)"
## [4] "3er WG (1w,1m,0d)" "2er WG (0w,0m,0d)" "3er WG (1w,1m,0d)"
## [7] "3er WG (1w,1m,0d)" "3er WG (1w,1m,0d)" "3er WG (1w,1m,0d)"
## [10] "3er WG (1w,1m,0d)"
%>%
wgs mutate(
flaeche = parse_number(flaeche),
preis = parse_number(preis),
bw_gesamt = parse_number(bewohnerinnen),
bw_w = str_extract(bewohnerinnen, "[0-9]+w") %>%
parse_number(),
bw_m = str_extract(bewohnerinnen, "[0-9]+m") %>%
parse_number(),
bw_d = str_extract(bewohnerinnen, "[0-9]+d") %>%
parse_number())
## # A tibble: 10 × 8
## link flaeche preis bewohnerinnen bw_gesamt bw_w bw_m bw_d
## <chr> <dbl> <dbl> <chr> <dbl> <dbl> <dbl> <dbl>
## 1 https://www.wg-gesuc… 20 1000 3er WG (1w,1… 3 1 1 0
## 2 https://www.wg-gesuc… 20 900 3er WG (1w,1… 3 1 1 0
## 3 https://www.wg-gesuc… 15 310 9er WG (0w,6… 9 0 6 0
## 4 https://www.wg-gesuc… 20 950 3er WG (1w,1… 3 1 1 0
## 5 https://www.wg-gesuc… 19 770 2er WG (0w,0… 2 0 0 0
## 6 https://www.wg-gesuc… 20 900 3er WG (1w,1… 3 1 1 0
## 7 https://www.wg-gesuc… 15 900 3er WG (1w,1… 3 1 1 0
## 8 https://www.wg-gesuc… 12 850 3er WG (1w,1… 3 1 1 0
## 9 https://www.wg-gesuc… 15 900 3er WG (1w,1… 3 1 1 0
## 10 https://www.wg-gesuc… 15 900 3er WG (1w,1… 3 1 1 0