Sitzung 11 Web scraping

11.1 Lernziele dieser Sitzung

Sie können…

  • HTML in seiner Grundstruktur interpretieren.
  • gezielt einzelne Elemente einer Seite mit R auslesen.

11.2 Vorbereitung

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

library(tidyverse)
library(rvest)

11.3 Exkurs: HTML

Wenn man eine Webseite ganz normal in einem Browser aufruft, erscheint sie als eine Mischung aus formatiertem Text, Bildern, Designelementen, ggf. Videos, usw. Was aber im Hintergrund eigentlich vom Server an den Browser übertragen wird, ist eine Textdatei in einem bestimmten Format – HTML (Hyptertext Markup Language). Darin wird Text auf eine genau festgelegte Art und Weise annotiert, damit der Browser weiß, wie er ihn anzeigen soll. Im HTML-Dokument kann auch stehen: Lade ein Bild von einer bestimmten Stelle und zeig es an dieser Stelle an.

Einen brauchbaren Überblick über die HTML-Elemente und die Struktur einer HTML-Datei gibt es hier: https://www.tutorialspoint.com/de/html/

An dieser Stelle ist wichtig ist zu wissen: HTML-Elemente (Tags oder Nodes) sind streng hierarchisch angeordnet. Sie bestehen oft aus einem Anfangs- und einem End-Tag in spitzen Klammern:

<html>
  <head>
    <title>Titel meiner Webseite</title>
  </head>
  <body>
    <h1>Überschrift</h1>
    <p>Erster Absatz mit <b>fettem Text</b></p>
    <p>Zweiter Absatz mit <i>kursivem Text</i></p>
    <img src="path/to/image.jpg alt="Ein Bild" />
  </body>
</html>

In diesem Beispiel ist das Bild mit <img /> das einzige Element, das nicht geöffnet und wieder geschlossen wird. Außerdem hat dieses Element Attribute (src und alt) mit bestimmten Werten. Eine echte Webseite ist weitaus komplexer und unübersichtlicher.

Dem Browser kann man sagen: Zeig mir nicht wie üblich die „gerenderte“ Seite an, sondern die zu Grunde liegende HTML-Datei. Das geht mit „Quelltext anzeigen“ / „View Source“ o.ä.

Viele Browser (hier seien Chrome und Firefox empfohlen) haben auch einen Modus namens „Entwicklertools“ / „Developer tools“, in dem die HTML-Elemente hierarchisch geordnet sind.

11.4 Seite laden

Beim so genannten Web Scraping ist die Grundidee, dass wir eine Webseite nicht im Browser öffnen, sondern den HTML-Quelltext direkt in R laden. R kann dann aus dem Quelltext bestimmte Elemente extrahieren.

In der letzten Sitzung haben wir schon gesehen, wie Tabellen nach genau diesem Prinzip von einer Webseite direkt in R geladen werden können. Jetzt soll es darum gehen, noch präziser zu sagen, welche Elemente wir von einer bestimmten Webseite ziehen wollen.

Als Beispiel soll die Infoseite eines Wohnheims des Studentenwerks Frankfurt dienen. Die Adresse ist: https://www.swffm.de/wohnen/wohnheime/frankfurt-am-main/kleine-seestrasse-11

Zunächst laden wir den Quelltext in R und nennen ihn quelltext:

quelltext <- read_html("https://www.swffm.de/wohnen/wohnheime/frankfurt-am-main/kleine-seestrasse-11")

quelltext
## {html_document}
## <html lang="de" dir="ltr" class="no-js">
## [1] <head>\n<meta http-equiv="Content-Type" content="text/html; charset=UTF-8 ...
## [2] <body id="p187" class="page-187 pagelevel-4 language-0 backendlayout-wohn ...

In diesem Schritt hat R den HTML-Quelltext schon geparsed, d.h. ihn nicht nur als Text gespeichert, sondern als hierarchische Konstruktion mit den beiden Grundelementen head und html.

11.5 Elemente suchen

Uns soll jetzt das Baujahr interessieren. Auf der Seite stehen die Informationen rechts neben dem Bild. Mit den Entwicklertools können wir schauen, wie die Elemente in HTML genau heißen. Ein geeigneter Ausgangspunkt wäre das <div>-Element mit dem Attribut id="c599".

In R können wir dieses einzelne Element ansprechen mit:

quelltext %>%
  html_node("div#c599")
## {html_node}
## <div id="c599" class="frame frame-default frame-type-text frame-layout-0 frame-background-none frame-no-backgroundimage frame-space-before-none frame-space-after-none">
## [1] <div class="frame-container"><div class="frame-inner">\n<p>Kleine Seestra ...

Dann gehen wir in der Hierarchie drei <div>- Elemente „tiefer“. (<div>- Elemente sind abstrakte Container und werden im Webdesign oft angewendet.)

quelltext %>%
  html_node("div#c599") %>%
  html_node("div") %>%
  html_node("div")
## {html_node}
## <div class="frame-inner">
## [1] <p>Kleine Seestraße 11<br>60486 Frankfurt am Main\n</p>\n
## [2] <p>25 Wohnheimplätze</p>\n
## [3] <ul class="list-normal">\n<li>5 Wohnküchen</li>\n<li>Wintergärten</li>\n< ...
## [4] <p>Baujahr<strong> </strong>1995\n</p>\n
## [5] <p><strong></strong></p>

Alternativ könnten wir auch sagen: Darin das div mit class="frame-inner":

quelltext %>%
  html_node("div#c599") %>%
  html_node("div.frame-inner")
## {html_node}
## <div class="frame-inner">
## [1] <p>Kleine Seestraße 11<br>60486 Frankfurt am Main\n</p>\n
## [2] <p>25 Wohnheimplätze</p>\n
## [3] <ul class="list-normal">\n<li>5 Wohnküchen</li>\n<li>Wintergärten</li>\n< ...
## [4] <p>Baujahr<strong> </strong>1995\n</p>\n
## [5] <p><strong></strong></p>

mit html_nodes() (Mehrzahl) werden alle Unterelemente eines Typs (hier <p> = Paragraph) angesprochen. Davon dann den Textinhalt (html_text()) gibt uns die relevanten Informationen:

quelltext %>%
  html_node("div#c599") %>%
  html_node("div.frame-inner") %>%
  html_nodes("p") %>%
  html_text()
## [1] "Kleine Seestraße 1160486 Frankfurt am Main\n"
## [2] "25 Wohnheimplätze"                           
## [3] "Baujahr 1995\n"                              
## [4] ""

11.6 Elemente reinigen

Jetzt ließe sich der dritte Eintrag dieses Ergebnisvektors reinigen und als Ergebnis speichern.

Der Befehl str_extract() wendet dabei einen Regulären Ausdruck an, der nach einer Folge von vier Zahlen sucht.

baujahr <- quelltext %>%
  html_node("div#c599") %>%
  html_node("div.frame-inner") %>%
  html_nodes("p") %>%
  html_text() %>%
  .[3] %>%
  str_extract("[0-9]{4}") %>%
  as.numeric()

baujahr
## [1] 1995

Wie diese Technik automatisiert auf eine Reihe von Seiten angewendet werden kann, wird zu einem späteren Zeitpunkt besprochen.

11.7 Aufgaben

  1. Lesen Sie die aus der obigen Webseite die Anzahl der Wohnheimplätze aus. (Resultat:)
anzahl_plaetze <- quelltext %>%
  html_node("div#c599") %>%
  html_node("div.frame-inner") %>%
  html_nodes("p") %>%
  html_text() %>%
  .[2] %>%
  str_extract("[0-9]+") %>%
  as.numeric()

anzahl_plaetze
## [1] 25
  1. Lesen Sie Baujahr und Anzahl der Wohnheimplätze von einem anderen Wohnheim aus. Was muss angepasst werden?
# z.B.: https://www.swffm.de/wohnen/wohnheime/wiesbaden/max-kade-haus-adolfsallee-49-53

url_wi <- "https://www.swffm.de/wohnen/wohnheime/wiesbaden/max-kade-haus-adolfsallee-49-53"

quelltext_wi <- read_html(url_wi)

# Baujahr nicht vorhanden!
baujahr_wi <- NA

anzahl_plaetze_wi <- quelltext_wi %>%
  html_node("div#c1563") %>%
  # Auf dieser Seite ist es eine andere div-ID!
  # Außerdem: Appartment = Platz? Scheint aber so zu sein...
  html_node("div.frame-inner") %>%
  html_nodes("p") %>%
  html_text() %>%
  .[2] %>%
  str_extract("[0-9]+") %>%
  as.numeric()
  1. Ändern Sie das Script so, dass es auf beiden (allen?) Wohnheimseiten funktioniert.

# Hier kann (hoffentlich) auch jede andere Wohnheim-URL eingesetzt werden.
url_x <- "https://www.swffm.de/wohnen/wohnheime/frankfurt-am-main/kleine-seestrasse-11"

quelltext_x <- read_html(url_x)

items <- quelltext_x %>%
  # So funktioniert es unabhänging von ID:
  html_node("div.wohnheim-2 > div:nth-child(2)") %>%
  html_nodes("p") %>%
  html_text()

baujahr_x <- items[3] %>%
  str_extract("[0-9]{4}") %>%
  as.numeric()

anzahl_plaetze_x <- items[2] %>%
  # Funktioniert leider nicht bei der getrennten Angabe mehrerer Kategorien:
  str_extract("[0-9]+") %>%
  as.numeric()
  1. Lesen Sie die Adresse eines Wohnheims aus. Speichern Sie dabei Straße, Hausnummer, Postleitzahl und Ort getrennt.
# Wie in Aufgabe 5 ersichtlich lassen sich die beiden Adresszeilen eigentlich
# recht einfach aus der Übersichtsseite auslesen.

adresse <- quelltext_x %>%
  html_node("div.wohnheim-2 > div:nth-child(2)") %>%
  html_nodes("p") %>%
  html_text2() %>%
  .[1] %>%
  trimws() %>%
  str_split("\n") %>%
  .[[1]]

# Die Hausnummer sind die Zahlen am Ende der ersten Zeile:
hausnummer_x <- adresse[1] %>%
  str_extract("[-0-9]+$")
# Funktioniert aber nicht bei 1b u.ä...

# Die Straße ist der Rest:
strasse <- adresse[1] %>%
  str_remove(" [0-9]+$")

# Die PLZ sind die 5 Zahlen am Anfang der zweiten Zeile:
plz <- adresse[2] %>%
  str_extract("^[0-9]{5}")

# Der Ort ist der Rest:
ort <- adresse[2] %>%
  str_remove("^[0-9]{5} ")
  1. Sammeln Sie eine Liste aller Wohnheime mit Link.
items <- "https://www.swffm.de/wohnen/wohnheime" %>%
  read_html() %>%
  html_nodes("div.thumbnail")

links <- items %>%
  html_node("a") %>%
  html_attr("href")

strasse_nr <- items %>%
  html_node("h4") %>%
  html_text()

plz_ort <- items %>%
  html_node("p") %>%
  html_text()

index <- tibble(links, strasse_nr, plz_ort)
  1. Sammeln Sie einen Datensatz (tibble) aller Nutzungsentgelte mit Wohnheim, Baujahr, Anzahl Wohneinheiten und Adresse. (Besonders elegante Lösungen vermeiden Copy-Paste-Strategien.)
# Hierfür gibt es einige Strategien. Meine präferierte Variante erfordert
# zunächst eine eigene Funktion, die eine URL nimmt und die gewünschten
# Informationen ausgibt:

scrape <- function(url) {
  tabellen <- url %>%
    read_html() %>%
    html_table()

  # Falls es keine Tabelle gibt, gib ein leeres Tibble zurück:
  if (length(tabellen) == 0) {
    return(tibble())
  }

  nutzungsentgelte <- tabellen %>%
    last() %>%
    # Der folgende Befehl säubert Leerzeichen in den Spaltennamen:
    select_all(trimws) %>%
    # Die Größe soll immer ein String sein, weil es sonst später Probeme gibt:
    mutate(`Größe m²` = as.character(`Größe m²`))

  items <- quelltext %>%
    html_node("div.wohnheim-2 > div:nth-child(2)") %>%
    html_nodes("p")

  adresse <- items[[1]] %>%
    html_text2() %>%
    trimws() %>%
    str_split("\n") %>%
    .[[1]]

  nutzungsentgelte$strasse <- adresse[1]
  nutzungsentgelte$ort <- adresse[2]

  nutzungsentgelte$baujahr <- items[3] %>%
    html_text() %>%
    str_extract("[0-9]{4}") %>%
    as.numeric()

  nutzungsentgelte$anzahl_plaetze <- items[2] %>%
    html_text() %>%
    str_extract("[0-9]+") %>%
    as.numeric()

  return(nutzungsentgelte)
}

# Dann kann ich die eigene Funktion "scrape" auf alle Links anwenden. Das
# Ergebnis ist eine Liste von Tibbles. Die lässt sich schließlich noch
# kombinieren:

ergebnis <- index$links %>%
  paste0("https://www.swffm.de", .) %>%
  map(scrape) %>%
  bind_rows()