16 APIs

APIs (Application Programming Interface, zu deutsch Anwendungsprogrammierschnittstelle oder nur Programmierschnittstelle) sind Schnittstellen, mit denen Software-Anwendungen mit anderen Anwendungen kommunizieren und Daten austauschen können. Wenn wir im Webkontext von APIs sprechen, meinen wir damit in der Regel sogenannte RESTful Web APIs41, die auf eine HTTP-Anfrage mit definierten Parametern Daten zurückgeben. Die Webseite ProgrammableWeb bietet einen umfassenden Überblick über solche APIs.

16.1 Grundlagen

Jede API funktioniert anders, nimmt eigene Parameter an, gibt Daten eigens strukturiert zurück und erfordert die Einarbeitung in die jeweilige Dokumentation der API; zugleich ist das Grundprinzip aber gleich:

  • wir senden eine HTTP-Anfrage (siehe Kapitel 14.1) an die URL der API, wobei wir mittels Query-Parametern spezifieren, was wir wissen möchten
  • die API gibt als Antwort die Daten in einem Textformat (häufig JSON, XML oder CSV) zurück.

Wir setzen uns daher zunächst mit diesen Grundprinzipien auseinander.

16.1.1 URLs, Querys und Parameter

Die Nutzung einer API unterscheidet sich zunächst nicht wesentlich davon, eine URL in einen Webbrowser einzugeben: in beiden Fällen senden wir (bzw. eine Software, also z. B. der Webbrowser oder RStudio)42 eine Anfrage (meistens einen GET-Request) an einen Server und erhalten daraufhin eine Datei zurück – z. B. eine HTML-Datei, die dann vom Browser intepretiert und angezeigt wird.

Server lassen sich so konfigurieren, dass die URL auch Parameter beinhalten kann, die die Anfrage spezifizieren. Sehen wir uns das an einem Alltagsbeispiel an: wenn wir nach “ifkw” googeln, dann sollte die Adresszeile im Browser in etwa so aussehen: https://www.google.de/search?safe=off&...q=ifkw.... Wir können diese URL in ihre Bestandteile aufteilen:

  • das https bezeichnet das Schema und gibt in diesem Fall an, dass wir das Netzwerkprotokoll HTTPS (eine sicherere Variante von HTTP) verwenden möchten
  • www.google.de ist der Host, also letztlich der Computer, der die Ressourcen, die wir anfragen möchten, beherbergt
  • /search gibt den Pfad an, unter dem die Ressource zu finden ist
  • das ? schließlich leitet den Query-String ein, der benannte Parameter enthält (in der Form name=wert und verbunden durch &), die vom Server im Rahmen der Anfrage verarbeitet werden

Im Falle unserer Google-Suche sehen wir unter anderem den Parameter q=ifkw – das q steht in diesem Fall für Query, also Suchanfrage, und ist auf unsere spezifische Suchanfage ifkw gesetzt. Wir können diesen Paramter daher nutzen, um auch ohne die Suchmaske direkt über die Adresszeile des Browsers eine Google-Suchanfrage zu starten, indem wir beispielsweise https://www.google.de/search?q=ifkw eingeben. Wenn wir direkt die zweite Ergebnisseite anzeigen möchten, können wir den Parameter start hinzufügen und auf 10 setzen, Google also mitteilen, dass wir erst beim zehnten Suchresultat beginnen möchten: https://www.google.de/search?q=ifkw&start=10.

Google verarbeitet also die Parameter, die wir in der URL angeben, und gibt basierend darauf die entsprechende Ressource zurück – in diesem Fall also eine HTML-Datei mit der zugehörigen Suchergebnisseite. Nicht anders funktionieren auch APIs – wir senden eine Anfrage und definieren über Parameter genauer, was wir erhalten bzw. machen möchten.

16.1.2 JSON

In der Regel geben Web-APIs keine HTML-Dateien zurück, sondern nutzen andere Datenformate. Eines der am häufigsten verwendeten ist JSON (JavaScript Object Notation und gesprochen wie der englische Vorname Jason), ein flexibel einsetzbares, als reine Textdatei speicherbares und zugleich sehr einfach lesbares Datenformat. Vermutlich reicht bereits die Beispieldatei im Wikipedia-Eintrag, um die Grundzüge zu verstehen:

{
  "Herausgeber": "Xema",
  "Nummer": "1234-5678-9012-3456",
  "Deckung": 2e+6,
  "Waehrung": "EURO",
  "Inhaber":
  {
    "Name": "Mustermann",
    "Vorname": "Max",
    "maennlich": true,
    "Hobbys": ["Reiten", "Golfen", "Lesen"],
    "Alter": 42,
    "Kinder": [],
    "Partner": null
  }
}

Wir erkennen zum einen die unterschiedlichen Objekttypen (String, Numerisch, Logisch; siehe auch Kapitel 2.2), die ganz ähnlich wie in R definiert werden (Strings durch "", Zahlen durch rein numerische Werte, logische Werte durch true/false); zum anderen sehen wir, dass wir Werte benennen und beliebig tief ineinander verschachteln können.

In R könnten wir die obige Beispieldatei als Liste speichern, die benannte Vektoren ebenso wie weitere Listen enthält – und genau das erledigen dann auch Packages für uns, die JSON-Dateien (bzw. Strings, die wie JSON aussehen) automatisch in R-Listen umwandeln.

16.1.3 Zugangsvoraussetzungen und Rate Limits

Nicht jede API lässt sich von jedem nutzen: wirklich offene APIs sind in der Minderheit, in der Regel ist zumindest ein Account beim jeweiligen Anbieter – also z. B. ein Twitter-Account für die Twitter-API – erforderlich. Viele Plattformen und Anbieter gewähren nur über einen Entwickler-Account, für den man sich extra registrieren, bewerben oder auch zahlen muss, Zugang zu ihrer API. Auch dies wird von Anbieter zu Anbieter unterschiedlich gehandhabt. Tatsächlich ist es so, dass insbesondere Social-Media-Plattformen den Zugang zu ihren APIs für die Wissenschaft in den vergangenen Jahren erschwert haben.43

Zudem begrenzen die meisten Anbieter den Zugang zu ihren APIs aus Sicherheitsgründen über sogenanntes Rate Limiting. Das bedeutet, dass ein Account in einem bestimmten Zeitintervall nur eine bestimmte Anzahl an Anfragen stellen darf (z. B. 15 Anfragen alle 15 Minuten), um eine Überlastung des Servers oder missbräuchlichen Datenabruf zu verhindern. Entsprechend sollte beim Schreiben von API-Anfragen darauf geachtet werden, dass durch diese keine Rate Limits überschritten werden, da sonst lediglich der HTTP-Code 429 Too Many Requests und eine Fehlermeldung zurückgegeben werden.

16.2 API-Anfragen in R ausführen mit httr

Um eigene API-Anfragen in R zu stellen, benötigen wir vorrangig das Package httr. Dieses gehört zum erweiterten Tidyverse, sollte also bereits installiert sein, muss aber separat geladen werden. Zudem laden wir das Package jsonlite, das den Umgang mit JSON-Dateien in R erleichtert. Auch dieses Package wird über das Tidyverse mitinstalliert, muss aber separat geladen werden:

library(tidyverse)
library(httr)
library(jsonlite)

Als Beispiel nutzen wir die Pushshift Reddit API, ein privates Projekt, das offenen API-Zugang zu Reddit ermöglicht. Unter obigem Link ist die API beschrieben. Das wichtigste in Kürze:

  • Die Stamm-URL der API ist https://api.pushshift.io
  • Die API bietet zwei Endpoints, /reddit/search/comment und /reddit/search/submission; wir können uns Endpoints einfach als unterschiedliche Pfade vorstellen, die in Kombination mit der Stamm-URL für unterschiedliche Funktionen der API zuständig sind. Um Submissions (also Beiträge) auf Reddit abzurufen, nutzen wir also die URL https://api.pushshift.io/reddit/search/submission, für Kommentare unter diesen Submissions die URL https://api.pushshift.io/reddit/search/comment
  • Parameter, die wir für die API verwenden können, unterscheiden sich je Endpoint und sind daher einmal für Kommentare und einmal für Submissions
  • Wir erfahren außerdem noch weitere Details, z. B. dass standardmäßig die 25 neuesten Submissions bzw. Kommentare zurückgegeben werden

Mit diesem Wissen können wir unsere erste API-Anfrage schreiben. Hierzu rufen wir die 10 neuesten Beiträge des Subreddits r/politics (einem Subreddit für politische Nachrichten aller Art) ab, die im Beitragstitel das Wort “Corona” enthalten. Aus der API-Dokumentation für Submissions erfahren wir, dass wir über die Parameter subreddit, title und size das zu durchsuchende Subreddit bzw. Begriffe, die im Titel vorkommen müssen, sowie die Anzahl der Beiträge angeben können.

httr umfasst Funktionen für alle typischen HTTP-Anfragetypen. In den meisten Fällen möchten wir Daten abrufen, stellen also eine GET-Anfrage. Hierzu nutzen wir die gleichnamige Funktion GET(), für die wir:

  • mit dem Argument url als String die Stamm-URL angeben, hier also "https://api.pushshift.io"
  • mit dem Argument path als String den Pfad zu unserem gewünschten Endpoint angeben, hier also "/reddit/search/submission"
  • mit dem Argument query eine Liste mit Query-Parametern übergeben können, hier also die Angaben zu subreddit, title und size44

Und natürlich sollten wir das Resultat einem R-Objekt zuweisen, um damit weiterarbeiten zu können – ich verwende den Namen resp (für Response):

resp <- httr::GET(url = "https://api.pushshift.io",
            path = "/reddit/search/submission",
            query = list(
              subreddit = "politics",
              title = "corona",
              size = 10
              ))

Ein nächster sinnvoller Schritt ist die Funktion stop_for_status(), die den HTTP-Code der Response überprüft und eine Fehlermeldung ausgibt, wenn etwas schiefgelaufen ist – zur Erinnerung, der Code 200 bedeutet “Alles okay”, und in diesem Fall bleibt die Funktion ohne sichtbares Ergebis.

stop_for_status(resp)

Im Environment-Bereich von RStudio sehen wir, dass es sich bei unserem neuen Objekt resp um eine Liste handelt. Wir sollten uns also zunächst die Struktur dieser Liste mit der str()-Funktion ansehen. Das max.level-Argument gibt an, dass wir in diesem Fall nur die erste Listenebene betrachten möchten

str(resp, max.level = 1)
## List of 10
##  $ url        : chr "https://api.pushshift.io/reddit/search/submission?subreddit=politics&title=corona&size=10"
##  $ status_code: int 200
##  $ headers    :List of 16
##   ..- attr(*, "class")= chr [1:2] "insensitive" "list"
##  $ all_headers:List of 1
##  $ cookies    :'data.frame': 1 obs. of  7 variables:
##  $ content    : raw [1:63195] 7b 0a 20 20 ...
##  $ date       : POSIXct[1:1], format: "2020-11-19 10:45:31"
##  $ times      : Named num [1:6] 0 0.0413 0.0656 0.1158 1.3012 ...
##   ..- attr(*, "names")= chr [1:6] "redirect" "namelookup" "connect" "pretransfer" ...
##  $ request    :List of 7
##   ..- attr(*, "class")= chr "request"
##  $ handle     :Class 'curl_handle' <externalptr> 
##  - attr(*, "class")= chr "response"

Die Response enthält also 10 Unterpunkte, darunter die gesamte URL, mit der wir unsere API-Anfrage getätigt haben, den bereits überprüften HTTP-Status-Code, eine Liste mit headers-Informationen usw. Über den Eintrag content-type in den headers können wir z. B. den Inhaltstyp der Antwort ausgeben – in diesem Fall wie erwartet eine JSON-Datei.

resp$headers$`content-type`
## [1] "application/json; charset=UTF-8"

Für uns von Interesse ist natürlich der Inhalt, content, der Anfrage. Diesen können wir der Funktion content() extrahieren, wobei wir zusätzlich angeben, dass der Inhalt als Text ausgeben werden soll.

resp_content <- content(resp, "text")
str_sub(resp_content, 1, 200) # Aus Anzeigegründen nur die ersten 200 Zeichen ausgeben
## [1] "{\n    \"data\": [\n        {\n            \"all_awardings\": [],\n            \"allow_live_comments\": false,\n            \"author\": \"PatriotCrypto905\",\n            \"author_flair_css_class\": null,\n            \""

Das Resultat ist ein sehr langer Textstring, der, wie wir bereits wissen, im JSON-Standard ist, mit dem wir aber vorerst nicht viel anfangen können. Hier kommt nun das Package jsonlite ins Spiel, mit dessen Funktion fromJSON wir JSON-Textdateien in R-Objekte umwandeln können (man nennt dies auch Parsing):

parsed_content <- fromJSON(resp_content)

Auch hierbei handelt es sich wieder um eine Liste, deren Struktur wir mit str() untersuchen können:

str(parsed_content, max.level = 1)
## List of 1
##  $ data:'data.frame':    10 obs. of  71 variables:

In diesem Fall haben wir nur einen weiteren Eintrag unter data, der einen data.frame enthält. 10 obs. deutet darauf hin, dass die Fälle wohl unsere 10 Reddit-Beiträge darstellen. Extrahieren wir also diesen Dataframe aus dem Objekt (und wandeln ihn in ein Tibble um):

reddit_tibble <- parsed_content$data %>% 
  as_tibble()

Mit names() können wir uns nun einen Überblick über die enthaltenen Variablen verschaffen:

names(reddit_tibble)
##  [1] "all_awardings"               "allow_live_comments"         "author"                      "author_flair_css_class"      "author_flair_richtext"      
##  [6] "author_flair_text"           "author_flair_type"           "author_fullname"             "author_patreon_flair"        "author_premium"             
## [11] "awarders"                    "can_mod_post"                "contest_mode"                "created_utc"                 "domain"                     
## [16] "full_link"                   "gildings"                    "id"                          "is_crosspostable"            "is_meta"                    
## [21] "is_original_content"         "is_reddit_media_domain"      "is_robot_indexable"          "is_self"                     "is_video"                   
## [26] "link_flair_background_color" "link_flair_css_class"        "link_flair_richtext"         "link_flair_text"             "link_flair_text_color"      
## [31] "link_flair_type"             "locked"                      "media_only"                  "no_follow"                   "num_comments"               
## [36] "num_crossposts"              "over_18"                     "parent_whitelist_status"     "permalink"                   "pinned"                     
## [41] "pwls"                        "removed_by_category"         "retrieved_on"                "score"                       "selftext"                   
## [46] "send_replies"                "spoiler"                     "stickied"                    "subreddit"                   "subreddit_id"               
## [51] "subreddit_subscribers"       "subreddit_type"              "thumbnail"                   "title"                       "total_awards_received"      
## [56] "treatment_tags"              "upvote_ratio"                "url"                         "url_overridden_by_dest"      "whitelist_status"           
## [61] "wls"                         "post_hint"                   "preview"                     "thumbnail_height"            "thumbnail_width"            
## [66] "crosspost_parent"            "crosspost_parent_list"       "media"                       "media_embed"                 "secure_media"               
## [71] "secure_media_embed"

Und tatsächlich, wir haben Informationen über die zehn aktuellsten Beiträge im Subreddit r/politics, die den Begriff “Corona” im Titel enthalten, abgerufen und können nun z. B. den Titel des Beitrags und den zugehörigen Link anzeigen:

reddit_tibble %>% 
  select(title, url)
## # A tibble: 10 x 2
##    title                                                                                    url                                                                 
##    <chr>                                                                                    <chr>                                                               
##  1 Anyone find it strange the number of missing or killed micro biologists. Then corona vi~ https://groups.google.com/g/misc.activism.progressive/c/sNeuzxqv93s~
##  2 What will the newly elected President of the United States do first after taking office~ https://voiceofwords1.blogspot.com/2020/11/what-will-newly-elected-~
##  3 kamala harris finally admitted they lied to the public about surgical masks protecting ~ https://publish.twitter.com/?query=https%3A%2F%2Ftwitter.com%2FKama~
##  4 kamala harris finally admitted they lied to the public about surgical masks protecting ~ https://publish.twitter.com/?query=https%3A%2F%2Ftwitter.com%2FKama~
##  5 German police sympathizes with Corona protesters at demonstration.                       https://twitter.com/IrenaKOKOT/status/1326093460726747136           
##  6 Mark Meadows Test Positive for Corona                                                    https://www.cbsnews.com/news/white-house-chief-of-staff-mark-meadow~
##  7 Mark Meadows, Trump Chief of Staff test positive for Corona Virus                        https://www.cnn.com/2020/10/25/politics/mark-meadows-controlling-co~
##  8 Corona Headquarters: The implementation of the traffic ban has started in 25 centers of~ https://jalebeh.blogsky.com/1399/08/12/post-379/                    
##  9 This is how the corona response works basically world wide                               https://www.youtube.com/watch?v=nSXIetP5iak                         
## 10 Trump cut funding to programs that would have warned us about the corona virus.          https://www.businessinsider.com/trump-cuts-programs-responsible-for~

Dies zum allgemeinen Vorgehen. In der Realität würden wir hier natürlich lange noch nicht aufhören. Was wenn wir uns nicht auf zehn Beiträge (oder 500, das Maximum, das die Pushshift Reddit API pro Anfrage vorsieht) beschränken möchten? Wir könnten in diesem Fall beispielsweise das Erstellungsdatum der Beiträge extrahieren, den Minimalwert speichern (also das Erstellungsdatum des ältesten Beitrags) und eine erneute Anfrage starten, dabei jedoch nur Beiträge abrufen, die vor diesem Datum erstellt wurden (also die 500 nächstälteren Beiträge) – und dann, unter Berücksichtigung des Rate Limits, einen Loop schreiben, der diese Schritte so lange wiederholt, bis keine weiteren Beiträge zurückgegeben werden.

Erneut gilt: jede API ist anders aufgebaut und erfordert daher spezifische Einarbeitung. Die Grundschritte sind aber immer nahezugleich: Dokumentation lesen, Anfrage mit httr stellen und dann schrittweise vorarbeiten, bis die gewünschten Daten vorhanden sind.

16.3 API-Wrapper nutzen

Die gute Nachricht: in vielen Fällen müssen wir uns nicht die Mühe machen, unsere eigenen Anfragen von Grund auf selbst zu schreiben. Für viele größere APIs gibt es sogenannte Wrapper-Packages, die die gängigen API-Anfragen in simplere Funktionen “verpacken”. Um die Twitter-APIs zu nutzen, können wir beispielsweise auf das Package rtweet zurückgreifen.

library(rtweet)

Um das Package nutzen können, benötigen wir einen Twitter-Account und müssen bei der ersten Verwendung einer Funktion einmalig eine Twitter-App authorisieren (hierzu öffnet sich automatisch ein Browser-Pop-Up), die die Kommunikation zwischen R und Twitter befähigt.

Danach können wir ohne große Zwischenschritte z. B. die fünf aktuellsten Tweets von Donald Trump abrufen:

trump_tweets <- get_timeline("realDonaldTrump", n = 5)

Das Resultat ist bereits ein Tibble, das mit 90 Variablen sehr viele Informationen über die einzelnen Tweets enthält. Wir sehen z. B., dass Trump offenbar gerne von seinem iPhone aus twittert:

trump_tweets %>% 
  select(text, source)
## # A tibble: 1 x 2
##   text                                                    source            
##   <chr>                                                   <chr>             
## 1 96% Approval Rating in the Republican Party. Thank you! Twitter for iPhone

Dank der großartigen R-Community ist die Arbeit mit gängigen APIs also erstaunlich einfach, zumindest sobald man die Zugangskriterien erfüllt. Es gilt also:

  • zunächst schauen, ob bereits ein Wrapper-Package für die gewünschte API vorhanden ist
  • erst dann mühsam eigene Anfragen schreiben

16.4 Übungsaufgaben

Aufgrund der Vielfalt an APIs gibt es diese Woche keine Übungsaufgaben im gewöhnlichen Sinne. Recherchieren Sie stattdessen in Ihrer Projektgruppe, welche APIs für Ihr Forschungsprojekt relevant sind, wie die Zugangsvoraussetzungen dafür sind und ob es etwaige R-Packages gibt, die Sie verwenden können.


  1. für Representation State Transfer. Mehr dazu hier↩︎

  2. In aller Regel nutzen diese Programme wiederum das Programm cURL, das somit das am häufigsten installierte und verwendete Programm der Welt sein dürfte.↩︎

  3. Etwas Lesestoff: After the ‘APIcalypse’: social media platforms and their fight against critical scholarly research↩︎

  4. Bei zugangsbeschränkten APIs muss hier häufig ein Anmeldeschlüssel mit übergeben werden.↩︎