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
:
<- read_html("https://www.swffm.de/wohnen/wohnheime/frankfurt-am-main/kleine-seestrasse-11")
quelltext
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.
<- quelltext %>%
baujahr 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
- Lesen Sie die aus der obigen Webseite die Anzahl der Wohnheimplätze aus. (Resultat:)
<- quelltext %>%
anzahl_plaetze 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
- 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
<- "https://www.swffm.de/wohnen/wohnheime/wiesbaden/max-kade-haus-adolfsallee-49-53"
url_wi
<- read_html(url_wi)
quelltext_wi
# Baujahr nicht vorhanden!
<- NA
baujahr_wi
<- quelltext_wi %>%
anzahl_plaetze_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()
- Ändern Sie das Script so, dass es auf beiden (allen?) Wohnheimseiten funktioniert.
# Hier kann (hoffentlich) auch jede andere Wohnheim-URL eingesetzt werden.
<- "https://www.swffm.de/wohnen/wohnheime/frankfurt-am-main/kleine-seestrasse-11"
url_x
<- read_html(url_x)
quelltext_x
<- quelltext_x %>%
items # So funktioniert es unabhänging von ID:
html_node("div.wohnheim-2 > div:nth-child(2)") %>%
html_nodes("p") %>%
html_text()
<- items[3] %>%
baujahr_x str_extract("[0-9]{4}") %>%
as.numeric()
<- items[2] %>%
anzahl_plaetze_x # Funktioniert leider nicht bei der getrennten Angabe mehrerer Kategorien:
str_extract("[0-9]+") %>%
as.numeric()
- 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.
<- quelltext_x %>%
adresse 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:
<- adresse[1] %>%
hausnummer_x str_extract("[-0-9]+$")
# Funktioniert aber nicht bei 1b u.ä...
# Die Straße ist der Rest:
<- adresse[1] %>%
strasse str_remove(" [0-9]+$")
# Die PLZ sind die 5 Zahlen am Anfang der zweiten Zeile:
<- adresse[2] %>%
plz str_extract("^[0-9]{5}")
# Der Ort ist der Rest:
<- adresse[2] %>%
ort str_remove("^[0-9]{5} ")
- Sammeln Sie eine Liste aller Wohnheime mit Link.
<- "https://www.swffm.de/wohnen/wohnheime" %>%
items read_html() %>%
html_nodes("div.thumbnail")
<- items %>%
links html_node("a") %>%
html_attr("href")
<- items %>%
strasse_nr html_node("h4") %>%
html_text()
<- items %>%
plz_ort html_node("p") %>%
html_text()
<- tibble(links, strasse_nr, plz_ort) index
- 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:
<- function(url) {
scrape <- url %>%
tabellen read_html() %>%
html_table()
# Falls es keine Tabelle gibt, gib ein leeres Tibble zurück:
if (length(tabellen) == 0) {
return(tibble())
}
<- tabellen %>%
nutzungsentgelte 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²`))
<- quelltext %>%
items html_node("div.wohnheim-2 > div:nth-child(2)") %>%
html_nodes("p")
<- items[[1]] %>%
adresse html_text2() %>%
trimws() %>%
str_split("\n") %>%
1]]
.[[
$strasse <- adresse[1]
nutzungsentgelte$ort <- adresse[2]
nutzungsentgelte
$baujahr <- items[3] %>%
nutzungsentgeltehtml_text() %>%
str_extract("[0-9]{4}") %>%
as.numeric()
$anzahl_plaetze <- items[2] %>%
nutzungsentgeltehtml_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:
<- index$links %>%
ergebnis paste0("https://www.swffm.de", .) %>%
map(scrape) %>%
bind_rows()