Sitzung 14 Serialisierung

14.1 Vorbereitung

Für diese Lektion werden die Pakete benötigt:

library(tidyverse)
library(rvest)

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…

  1. … für die ersten drei Überblicksseiten automatisiert alle URLs der einzelnen Anzeigen auslesen (was sich aber in der Praxis erweitern ließe).
  2. … 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:

  1. An einem Beispiel konkret ausführen
  2. Abstrahieren (hier: als Funktion)
  3. Testen an weiteren einzelfällen
  4. 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):

get_url_list <- function(list_url) {
  url %>%
    read_html() %>%
    html_nodes("tr.offer_list_item") %>%
    html_attr("adid")
}

14.3.3 Schritt 3: Testen

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()

qm <- key_facts[1]
eur <- key_facts[2]

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:

get_anzeige_details <- function(anzeige_url) {

  # "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()

  qm <- key_facts[1]
  eur <- key_facts[2]

  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.

wgs$bewohnerinnen
##  [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