Fabian Gülzau (HU Berlin)
16 Januar 2019
“If programming is magic, then web scraping is wizardry” (Mitchell 2015: vii)
install.packages(pacman) # Installation nur einmal notwendig
library(pacman)
p_load(tidyverse, rvest, httr, robotstxt, eurostat, lubridate)
“…something big is going on” (Salganik 2018: 2)
“‘computational social science’ (CSS) is occurring. The question is whether it happens with or without social scientists” (Heiberger & Riebling 2016: 1)
Exemplarische Studien:
King et al. (2013):
Quelle: King et al. (2013: 335)
Quelle: Salganik 2018: 17-41
Infrastruktur des Internets im Alltag irrelevant.
Unser Browser übernimmt:
Um Informationen gezielt abzufragen, benötigen wir allerdings basale Kenntnisse der zugrundeliegenden Technologien.
Hypertext Transfer Protocol (HTTP)
Übertragungsprotokoll, welches aus zwei Teilen besteht:
GET
-Abfrage mit httr::GET
und Antwort des Servers in R#install.packages("httr")
library(httr)
response <- GET("https://www.wiso.uni-hamburg.de/fachbereich-sozoek/professuren/lohmann/team/hertel-florian.html") %>%
print()
## Response [https://www.wiso.uni-hamburg.de/fachbereich-sozoek/professuren/lohmann/team/hertel-florian.html]
## Date: 2019-01-16 08:48
## Status: 200
## Content-Type: text/html; charset=utf-8
## Size: 30.9 kB
## <!DOCTYPE html><html class="v4" lang="de" xml:lang="de" xmlns="http://ww...
## <meta name="csrf-token" content="q7cZCDWLVdlnp6YkwuRgJIwWG4OHaidCLliAuh4...
## <p>Anmeldungen bitte per Mail.</p><h3>Kontakt</h3><div class="telefon">T...
## <p><strong>Seit 09/2016<br /></strong>Wissenschaftlicher Mitarbeiter, Pr...
## <p><strong>09/2015-08/2016<br /></strong>Max Weber Postdoctoral Fellow, ...
## <p><strong>09/2010-10/2015<br /></strong>Affiliated Fellow, Bremen Inter...
## <p><strong>11/2009-07/2015<br /></strong>Wissenschaftlicher Mitarbeiter,...
## <p><strong>05/2009-10/2009<br /></strong>Wissenschaftlicher Mitarbeiter,...
## <p><strong>03/2009<br /></strong>Diplom Soziologie, Freie Universität Be...
## <li>Soziale Ungleichheit und Stratifikation</li>
## ...
#install.packages("rvest")
library(rvest)
pubs <- response %>%
read_html() %>%
html_nodes("#4128883 li") %>%
html_text() %>%
tibble(pub = .) %>%
mutate(year = as.numeric(
str_extract(pub, pattern = "[:digit:]{4}"))) %>%
group_by(year) %>%
summarise(number = n())
year | number |
---|---|
2009 | 1 |
2010 | 2 |
2011 | 1 |
2014 | 1 |
2015 | 2 |
2016 | 1 |
2017 | 2 |
2018 | 1 |
Hypertext Markup Language (HTML)
Einfache Seiten über Editor/Notepad++ erstellbar:
<!DOCTYPE html>
<html>
<head>
<title>Workshop Web Scraping</title>
</head>
<body>
<h1> Web Scraping mit R, Universität Hamburg</h1>
<p>Dieser Kurs führt in das "Web Scraping" mit R ein. Er wird durch <a href="https://fguelzau.rbind.io/">Fabian Gülzau</a> geleitet.</p>
</body>
</html>
Der Browser interpretiert den Code und zeigt eine Internetseite an.
HTML:
Cascading Style Sheet (CSS)
JavaScript: Programmiersprache des Internets
Quelle: The Pudding (2018)
Web Scraping-Projekte folgen einem generellen Schema:
Quelle: Wickham & Grolemund (2018)
Das generelle Schema lässt sich in R über das Paket rvest
umsetzen:
robotstxt
)read_html
: Import von HTML/XML-Dateienhtml_nodes
: Extrahiert Teile des HTML-Dokument mit Hilfe von XPath/CSS-Selektorenhtml_text
/ html_attr
/ html_table
: Extrahiert Text, Attribute und Tabellenmap
(package: purrr): Iteration (loops)Ziel: Aktuelle Umfragen aller Institute herunterladen und kombinieren.
Quelle: Tagesschau.de
Fragen, die beantwortet werden sollten:
Hierzu gehört:
paths_allowed(
paths = c("/index.htm","/allensbach.htm"),
domain = c("wahlrecht.de")
)
##
wahlrecht.de No encoding supplied: defaulting to UTF-8.
## [1] TRUE TRUE
Internetseiten müssen in ein Format übersetzt werden, welches von R gelesen und bearbeitet werden kann (z.B. Baumstruktur).
Benötigt wird:
Über rvest::read_html
:
(html.page <- read_html("https://www.wahlrecht.de/umfragen/allensbach.htm"))
## {xml_document}
## <html lang="de-DE">
## [1] <head>\n<meta http-equiv="expires" content="0">\n<meta http-equiv="c ...
## [2] <body>\n\n<div class="head">\n<table class="title" width="100%"><tr> ...
Zur Extraktion von Informationen nutzen wir die Baumstruktur von HTML:
Quelle: selfhtml.de
Wir konstruieren Selektoren selten manuell, da Anwendungen dies übernehmen.
Tools:
HTML-Tabellen lassen sich oft besonders leicht identifizieren, da sie das Tag “table” tragen:
(html.node <- html_nodes(html.page, css = ".wilko"))
## {xml_nodeset (1)}
## [1] <table class="wilko" align="center" cellpadding="2" cellspacing="3" ...
Wir sind selten am HTML-Tag interessiert, sondern an dem Inhalt:
html.table <- html.node %>%
html_table(header = TRUE, fill = TRUE) %>%
.[[1]] %>% # body
.[4:nrow(.), c(1, 3:9)] %>% # subsetting
glimpse()
## Observations: 67
## Variables: 8
## $ `` <chr> "20.12.2018", "30.11.2018", "18.10.2018", "18.09.201...
## $ `CDU/CSU` <chr> "29,0 %", "28,0 %", "29,0 %", "31,5 %", "31,0 %", "3...
## $ SPD <chr> "16,5 %", "17,0 %", "19,0 %", "19,5 %", "20,0 %", "2...
## $ GRÜNE <chr> "19,0 %", "19,0 %", "15,0 %", "12,5 %", "12,5 %", "1...
## $ FDP <chr> "8,5 %", "9,5 %", "8,5 %", "8,5 %", "9,0 %", "9,5 %"...
## $ LINKE <chr> "9,0 %", "9,0 %", "9,0 %", "9,0 %", "9,0 %", "9,0 %"...
## $ AfD <chr> "14,0 %", "13,5 %", "15,0 %", "15,0 %", "14,5 %", "1...
## $ Sonstige <chr> "4,0 %", "4,0 %", "4,5 %", "4,0 %", "4,0 %", "4,0 %"...
Regular Expression
stringr
str_view(string = "Wir benötigen nicht den gesamten Text,
sondern nur die Zahl, welche sich im Text verbirgt: 42.",
pattern = "[:digit:]+")
Die Umfrageergebnisse liegen als “strings” vor, sodass wir sie für die Datenanalyse in numerische Werte umwandeln müssen.
str_view(string = html.table$`CDU/CSU`[1:5],
pattern = "[:digit:]+,?[:digit:]?")
Wir ersetzen zudem die Kommata durch Punkte und wandeln die Daten in ein “long”-Format um:
allensbach.df <- html.table %>%
rename("Zeitpunkt" = 1) %>% # 1. Variable benennen
mutate(Zeitpunkt = parse_datetime(Zeitpunkt, # 2. als Datum
format = "%d.%m.%Y")) %>%
mutate_if(is.character, str_extract, # 3a. Zahl entnehmen
pattern = "[:digit:]+,?[:digit:]?") %>%
mutate_if(is.character, str_replace, # 3b. Komma als Punkt
pattern = ",", replacement = ".") %>%
mutate_if(is.character, as.numeric) %>% # 3c. als Zahl
gather(party, vote, -Zeitpunkt) %>% # 4. long format
glimpse() # 5. ausgeben
## Observations: 469
## Variables: 3
## $ Zeitpunkt <dttm> 2018-12-20, 2018-11-30, 2018-10-18, 2018-09-18, 201...
## $ party <chr> "CDU/CSU", "CDU/CSU", "CDU/CSU", "CDU/CSU", "CDU/CSU...
## $ vote <dbl> 29.0, 28.0, 29.0, 31.5, 31.0, 30.5, 33.0, 34.0, 34.0...
Zuletzt können wir die Ergebnisse der Sonntagsfrage visualisieren (Paket: ggplot2
):
ggplot(subset(allensbach.df, party != "Sonstige"),
aes(x = Zeitpunkt, y = vote, colour = party)) +
geom_line(size = 1.5) +
scale_color_manual(values = c("#009EE0", "#000000", "#FFED00", "#64A12D",
"#BE3075", "#EB001F"),
guide = guide_legend(title = NULL)) +
scale_y_continuous(labels = function(x) paste0(x, "%")) +
labs(title = "Sonntagsfrage: Wenn am nächsten Sonntag Bundestagswahl wäre...",
x = "", y = "", caption = "Quelle: wahlrecht.de (Allensbach)") +
theme_minimal()
Wird in den weiteren Anwendungen besprochen (Code)
Spezielle Möglichkeiten & Herausforderungen:
Application Programming Interface (API)
Dynamische Webseiten & bot blocking:
Quelle: Eigene Darstellung (Code)
Eurostat bietet eine API für Datenbankabfragen an (Paket: eurostat
)
Es gibt ein “Cheatsheet” für das Paket, welches das folgende Vorgehen beschreibt:
search_eurostat
)get_eurostat
)tidyverse
)Arbeitslosenquote:
content <- search_eurostat(pattern = "unemployment rate", type = "dataset") %>%
glimpse()
## Observations: 7
## Variables: 8
## $ title <chr> "Harmonised unemployment rates (...
## $ code <chr> "ei_lmhr_m", "lfst_r_lmdur", "lf...
## $ type <chr> "dataset", "dataset", "dataset",...
## $ `last update of data` <chr> "10.01.2019", "22.10.2018", "22....
## $ `last table structure change` <chr> "10.01.2019", "18.07.2018", "18....
## $ `data start` <chr> "1983M01", "1999", "1999", "1995...
## $ `data end` <chr> "2018M12", "2017", "2017", "2017...
## $ values <chr> NA, NA, NA, NA, NA, NA, NA
unem.df <- get_eurostat(id = content$code[1], # Variable
time_format = "date", # Datumsformat
filters = list(s_adj = "NSA", # Datenfilter
indic = "LM-UN-T-TOT",
geo = "EU28"),
stringsAsFactors = FALSE)
# 3. Datenaufbereitung
unem.df %>%
filter(time >= "2018-01-01") %>%
ggplot() +
geom_line(mapping = aes(x = time, y = values), stat = "identity") +
scale_y_continuous(labels = function(x) paste0(x, "%")) +
theme_minimal() +
labs(title = "Monatliche Arbeitslosenquote (EU28), 2018",
caption = "Quelle: eurostat (LM-UN-T-TOT)",
x = "",
y = "")
“Big data? Cheap. Lawyers? Not so much.” (Pete Warden zit. in Mitchell 2015: 217)
Ziel: “friendly scraping”
Sys.sleep
einbauen (~5-10 Sekunden)Weitere Beispiele: