11 Exkurs: Web Scraping und APIs

Wenn ihr für ein Forschungsprojekt ein großes Korpus von Texten zusammenstellen müsst, kann das ohne die richtigen Werkzeuge sehr lange dauern. Wenn ihr beispielsweise 200 Plaintext-Dateien von wikisource.org manuell herunterladen wollt, müsstet ihr jeden Text erst suchen, dann jedes Mal auf den “Herunterladen”-Button klicken und hier die Parameter für den Download jedes Mal erneut auswählen. Es gibt jedoch verschiedene Methoden, um diese Arbeit zu erleichtern:

  1. API der Website abrufen: Vielleicht verfügt die Website über eine Schnittstelle, eine sogenannte API, die ihr anzapfen könnt, um direkt die Dateien herunterzuladen. API steht für Application Programming Interface und wird verwendet, um anderen den Zugriff auf Daten oder Funktionalitäten einer Anwendung zu ermöglichen, ohne dabei den Quellcode zu offenbaren. Die Inhalte können mit einer simplen Abfrage (“GET-Request”) über das Internet-Protokoll HTTP aus R heraus abgerufen werden. Solche Schnittstellen finden sich auf den Seiten vieler Bibliotheken, Institutionen, Archive, Forschungsprojekte, Social Media Plattformen, Medien und Unternehmen unter Menüpunkten wie “For Developers”, “API”, “Tools”, “Services”, “Dokumentation” o.Ä.
  1. Web Scraping: Wenn es keine API gibt, können Dateien oder der Textinhalt einer oder mehrerer Webseiten mithilfe von Web Scraping-Strategien extrahiert und heruntergeladen werden. Um diese Strategien anzuwenden, müsst ihr euch mit dem Quellcode der Website vertraut machen. Webseiten sind im Grunde einfach HTML-Dokumente, die irgendwo auf einem Server liegen, also auf einem Computer, auf der Webhosting-Software läuft. HTML ist eine spezielle Sprache, welche zur Beschreibung der Seitenstruktur verwendet wird. Sogenannte HTML-Elemente werden verwendet, um zu beschreiben, wo der Textinhalt der Webseite, aber auch Links zu Unterseiten und zu Ressourcen wie Bilddateien angezeigt werden sollen. Sobald ihr verstanden habt, welche HTML-Elemente die Inhalte beschreiben, die euch interessieren, könnt ihr aus R heraus den Inhalt dieser Elemente extrahieren.

Manchmal stellen Institutionen keine API bereit, sondern bieten direkt ganze Korpora zum Download als “Data Dump”. In diesem Fall sind natürlich weder Web Scraping noch eine API notwendig, um an die Daten zu gelangen, aber es muss beachtet werden, dass diese Daten nicht unbedingt tagesaktuell sind. Bevor ihr anfangt, ein Skript zum Scrapen einer Seite zu schreiben, solltet ihr immer überprüfen, ob die gesuchten Daten nicht vielleicht auch über eine API oder als Data Dump verfügbar sind. Eine Sammlung solcher Volltext-Datadumps findet ihr hier.

11.1 Beispiel API: DraCor

Die Dramen-Datenbank DraCor verfügt über eine gut dokumentierte API, welche den Zugriff auf verschiedene Inhalte ermöglicht, beispielsweise auf Metadaten zu Dramen oder ganzen Korpora, auf den Sprechtext der Figuren im Plaintext-Format und auch auf ganze, in XML-TEI (-> nächste Woche!) ausgezeichnete Dramentexte. Die Abfrage dieser Daten für einzelne Dramen ist direkt über das Webinterface möglich. Um Daten zu mehreren Dramen zu bekommen, müssen wir ein Skript schreiben, das die API abruft und die URL für die API-Abfrage iterativ (also in mehreren Durchläufen) für jedes der uns interessierenden Dramen anpasst.

Für eine Abfrage aller Sprechtexte in Dramen von Goethe könnte ein Skript beispielsweise so aussehen:

install.packages(c("jsonlite", "httr"))
library(jsonlite)
library(httr)

corpusname <- "ger"
base_url <- "https://dracor.org/api/v1"
author <- "goethe"

# Informationen zu allen Stücken aus dem deutschsprachigen DraCor Korpus abfragen
# Abfrage-URL für das ausgewählte Korpus vorbereiten
full_url <- paste(base_url, "/corpora/", corpusname, sep="")
full_url #zum Überprüfen in der Konsole ausgeben lassen
# API call 
api_response <- httr::GET(full_url)
?GET #Informationen zur Funktion GET anzeigen lassen
# API response ansehen
api_response$status_code
api_response$content
httr::content(api_response, "text", encoding = "UTF-8")
# API Antwort lesbar machen und in einem Dataframe (~Tabelle) speichern
api_df <- jsonlite::fromJSON(content(api_response, "text", encoding = "UTF-8"), simplifyDataFrame = TRUE)
# Output überprüfen
View(api_df)
View(api_df$plays)

# Sprechtexte weiblich und männlich codierter Charaktere aus allen Dramen eine:r bestimmten Autor:in abrufen
# Aufgrund der verschachtelten Struktur des Dataframes ist der direkte Zugriff auf die Metadaten zu den Dramen etwas umständlich
# Alternativ zum Direktzugriff können erst die Namen aller Dramen aus ger-Dracor in einer neuen Variable gespeichert werden...
plays <- api_df$plays$name
plays #überprüfen
# ... und dann kann mithilfe von Regex nach Dramen der gewählten Autor:in gefiltert werden. Als pattern wird dann die Variable author eingesetzt, die wir am Anfang erstellt haben.
plays_selected <- grep(pattern=author, plays, value=TRUE) 
plays_selected

# URLs für die Abfrage vorbereiten
first_url <- paste0(full_url, "/plays/")
second_url <- paste0(plays_selected, '/spoken-text')
full_urls <- mapply(paste0, first_url, second_url, USE.NAMES=FALSE)
?mapply
full_urls

# neues Verzeichnis anlegen: in diesem Ordner werden die Textdateien gespeichert
dir.create("spokentext_corpus")
setwd(paste0(getwd(), "/spokentext_corpus"))

# API calls durchführen und Output speichern
# Zuerst ohne Angabe des codierten Geschlechts als Query-Parameter
for(i in seq_along(full_urls)){
  api_response <- httr::GET(full_urls[i])
  spokentext <- httr::content(api_response, "text", encoding = "UTF-8")
  filename <- paste0(plays_selected[i], ".txt")
  writeLines(spokentext, filename)
}

# Mit query nach dem codierten Geschlecht
gender_codes <- c("FEMALE", "MALE", "UNKNOWN")
for(i in seq_along(full_urls)){
  for(j in seq_along(gender_codes)){
    api_response <- httr::GET(full_urls[i], query=list(gender=gender_codes[j]))
    spokentext <- content(api_response, "text", encoding = "UTF-8")
    filename <- paste(plays_selected[i], gender_codes[j], ".txt", sep="_")
    writeLines(spokentext, filename)
  }
}

list.files() # Dateien im Arbeitsverzeichnis auflisten

Verständnisfrage:

Das Projekt DraCor bietet neben der API auch ein eigenes R Paket, welches Nutzer:innen die Datenabfrage über die API erleichtern soll.

11.2 Beispiel Web Scraping: Wikisource

Die Open Source - Volltextdatenbank Wikisource stellt keine API bereit. Dafür ist der Aufbau der Seite unkompliziert und auch mit nur rudimentärem Verständnis von HTML intuitiv verständlich.

Wir machen uns zunächst über die Nutzeroberfläche mit der Funktionalität der Website vertraut. Für uns sind zwei Seiten wichtig: Zum einen die Übersichtsseite über alle Kinder- und Hausmärchen der Gebrüder Grimm. Diese könnten wir verwenden, um die Links zu allen Märchenseiten zu extrahieren. Zum anderen die Seiten der einzelnen Märchen, hier zum Beispiel die Seite des Märchens “Katz und Maus in Gesellschaft” in der Version von 1812. Beim Vergleich der verschiedenen Märchenseiten fällt auf, dass manche Märchenseiten einen Anhang haben und manche nicht.

Anschließend wollen wir uns den Quelltext der beiden relevanten Seiten ansehen. Dazu öffnen wir die Seiten entweder in Firefox oder Chrome und öffnen die Entwicklertools:

  • Chrome: Anzeigen -> Entwickler -> Elemente untersuchen

  • Firefox: ein Seitenelement markieren -> Rechtsklick -> Element untersuchen

Verständnisfragen:

  • Welche HTML-Elemente seht ihr? Wo befindet sich der Text, den wir brauchen?
  • Sucht das HTML-Element, das die Tabelle mit den Märchentexten repräsentiert. Wie heißt es? Wo befinden sich die Links zu den Unterseiten? Wie nennt sich der Bestandteil des HTML-Elements, das den Link beinhaltet? Konsultiert zur Beantwortung dieser Frage diesen Beitrag zur HTML-Syntax.

Zur Übung schreiben wir ein Skript, das den Fließtext aller Märchen der Gebrüder Grimm von der Seite wikisource.org extrahiert und als Plaintext-Datei im Arbeitsverzeichnis speichert.

# Vorbereitung
install.packages("rvest")
install.packages("stringi")
library(rvest)
library(stringi)

url <- "https://de.wikisource.org/wiki/Kinder-_und_Hausm%C3%A4rchen"

# Website HTML-Baum parsen und Links der einzelnen Märchen extrahieren
suburls <- url %>%
  read_html() %>% # bracket optional
  html_nodes(xpath = "//div[1]/table[2]/tbody/tr/td[position()>1]/a") %>% # position()>1 filtert das erste <td> Element heraus
  html_attr("href") %>%
  stri_paste("https://de.wikisource.org", .) # . ist ein Platzhalter, es wird verwendet um die Reihenfolge der Funktionsargumente umzukehren 
suburls # überprüfen

# Eine andere Möglichkeit, nicht benötigte Listenelemente herauszufiltern
# suburlsNew <- suburlsNew[which(regexpr("\\d{4}", suburlsNew) >=1)] 

for(i in seq_along(suburls)) {    
  
  maerchen <- suburls[i] 
  
  # Text aus den paragraph-Elementen extrahieren (<p></p>)
  maerchentext <- maerchen %>%
    read_html() %>% 
    html_nodes(xpath = "//p[not(preceding::h2)]") %>% # Alles nach dem <h2>-Tag ausschließen
    html_text()
  
  # Dateinamen erstellen
  # URLdecode dekodiert die URLs (z.B. %C3%A4 für ä) und gibt Umlaute aus. Diese können später jedoch 
  # Probleme verursachen, wenn die Dateien eingelesen werden sollen. Es ist deswegen empfehlenswert, 
  # Umlaute in Dateinamen zu vermeiden.
  filename <- suburls[i] %>%
    basename() %>% # Anfang der URL entfernen
    URLdecode() %>% # URL dekodieren
    stri_trans_general("de-ASCII; Latin-ASCII") # ä,ö,ü zu ae, oe, ue 
  
  # Noch eine Möglichkeit, Dateinamen zu erstellen
  # filename <- substring(suburls[i], first = 32)
  
  # maerchentext in eine txt-Datei schreiben und im Arbeitsverzeichnis speichern
  write.table(maerchentext, 
              file = paste(filename, ".txt", sep=""), 
              quote=FALSE,
              col.names=FALSE,
              row.names=FALSE)
  
}

Anfang des Semesters haben wir bereits besprochen, dass Schleifen und andere Kontrollstrukturen in Programmiersprachen wie Python eine große Rolle spielen, in R jedoch als weniger elegante Lösung gelten. In unserem ersten Wikisource-Scraper haben wir eine for-Schleife verwendet, um die Ausführung von unserem Code zu kontrollieren: Unsere Schleife ist über eine Liste von suburls iteriert und jedes Mal wurden Befehle aus dem Schleifenkörper ausgeführt. Während for-Schleifen und andere Kontrollstrukturen in Programmiersprachen wie Python sehr wichtig sind, ist R eigentlich nicht für diesen Programmierstil konzipiert. Das bedeutet, dass Schleifen zwar verwendet werden können, aber das resultiert oft in einer längeren Laufzeit und wird allgemein als eine weniger elegante Lösung angesehen.

Der folgende Code illustriert, wie das Webscraping-Skript mithilfe von Funktionen vereinfacht werden kann. Dazu wird mehrfach die Funktion map() aus dem Paket purrr verwendet, um die Schleife zu ersetzen. Für mehr Informationen zu purrr und map() siehe die purrr-Dokumentationsseiten und das Kapitel “Functional Programming” aus Bruno Rodrigues’ Kurs “Modern R with the tidyverse”.

install.packages("rvest")
install.packages("stringi")
install.packages("purrr")
library(rvest)
library(stringi)
library(purrr)

url <- "https://de.wikisource.org/wiki/Kinder-_und_Hausm%C3%A4rchen"

# HTML parsen und Links extrahieren
suburls <- url %>%
  read_html() %>% # Klammer optional
  html_nodes(xpath = "//div[1]/table[2]/tbody/tr/td[position()>1]/a") %>% 
  html_attr("href") %>%
  stri_paste("https://de.wikisource.org", .)
head(suburls)

# Text zwischen paragraph-tags (<p></p>) extrahieren
# map() nimmt eine Liste und eine Funktion als Input und wendet die Funktion auf jedes Listenelement an. 
# In diesem Fall nimmt beispielsweise map(html_nodes, .. ) eine Liste HTML-Dokumente als Input und gibt eine Liste zurück, bei der jedes Element wiederum eine Liste von <p>-Elementen aus dem Input-HTML-Dokument enhält. 
maerchentexte <- suburls %>%
  map(read_html) %>%
  map(html_nodes, xpath = "//p[not(preceding::h2)]") %>%
  map(html_text) 
typeof(maerchentexte) # output ist Liste von Listen
maerchentexte[1]

# Dateinamen erstellen, wieder mithilfe der map()-Funktion
filenames <- suburls %>%
  map(basename) %>% # Anfang der URL entfernen
  map(URLdecode) %>% # URL dekodieren
  stri_trans_general("de-ASCII; Latin-ASCII") %>% # ä,ö,ü zu ae, oe, ue konvertieren
  stri_paste(".txt") 
head(filenames) 


# Jedes Element der maerchentexte-Liste in eine txt-Datei schreiben und im Arbeitsverzeichnis speichern. 
# Die Funktion walk2() ist ähnlich wie map(), aber nimmt zwei Elemente anstatt nur einem sowie eine Funktion als Input. 
# Sie greift parallel auf beide Listen zu und wendet die Funktion, in diesem Fall write.table() auf beide Listen an. 
# Das heißt, dass in jedem Schritt ein Element maerchentexte[i] und ein Element filenames[i] an die Funktion übergeben wird. 
# Beide Listen müssen deswegen dieselbe Länge haben. 
walk2(maerchentexte, filenames, write.table, quote=F, col.names=F, row.names=F)