library(tidyverse)
theme_set(theme_minimal())
1 Datenerhebung
Für die automatische Erhebung von Online-Inhalten mit R gibt es verschiedene Möglichkeiten, die mit unterschiedlichem Aufwand verbunden sind. Manche Daten sind direkt für die Auswertung in R geeignet, z.B. tabellarische Daten wie CSV-Dateien, andere müssen mühsam aus Inhalten extrahiert werden, die für menschliche RezipientInnen aufbereitet sind, z.B. Webseiten. Für manche Plattformen und Datenformate gibt es fertige R-Pakete, für andere müssen wir selbst die Aufbereitung programmieren.
Aufsteigend von der leichtesten zur schwierigsten Umsetzung, natürlich abhängig von der Plattform, die untersucht werden soll, ergibt sich diese Reihe:
- maschinenlesbaren Dateien direkt aus dem WWW einlesen (z.B. CSV-Files)
- fertige R-Pakete für einzelne Plattformen/Anbieter verwenden (z.B. für TikTok)
- über Feeds oder API (Application Programming Interface) standardisierte Daten erhalten
- über Web-Scraping HTML-Inhalte von Websites herunterladen und verarbeiten
- über ferngesteuerte Browser Inhalte erheben oder Screenshots erstellen
- NutzerInnen um Datenspenden bitten
Die Punkte 5 und 6 werden wir nicht behandeln, weil sie einerseits sehr plattformabhängig sind, und andererseits eine sehr tiefgehende Auseinandersetzung mit den technischen und forschungspraktischen Herausforderungen erfordern, die den Rahmen des Seminars sprengen würden. Leider fallen einige sehr populäre Platformen wie Facebook, Instagram und Twitter darunter, die den Forschenden die Erhebung selbst von “öffentlichen” Daten sehr erschweren.
Zu Beginn laden wir zunächst das tidyverse
Paket und setzen ein schöneres Theme für alle Grafiken, die wir erstellen.
1.1 Tabellarische Daten
1.1.1 CSV-Dateien
Am einfachsten ist die Situation, wenn Textdaten bereits in strukturierter maschinenlesbarer Form vorliegen. Im Internet gibt es zahlreiche mehr oder minder seriöse Quellen für solche Daten, etwa auf Kurs-Websites, bei Kaggle, oder auf dieser Workshop-Seite.
Wir haben hier einen Datensatz mit Songtexten von Taylor Swift ausgewählt, der im Rahmen der Tidy Tuesday Reihe für originelle Datenanalysen veröffentlicht wurde. Hier finden sich auch viele andere interessante Datensätze. Der Datensatz liegt direkt als CSV-File vor, das wir in R einlesen und ansehen können.
# Source:
<- readr::read_csv("https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2020/2020-09-29/taylor_swift_lyrics.csv")
taylor_swift_lyrics taylor_swift_lyrics
# A tibble: 132 × 4
Artist Album Title Lyrics
<chr> <chr> <chr> <chr>
1 Taylor Swift Taylor Swift Tim McGraw "He said the way my blue eye…
2 Taylor Swift Taylor Swift Picture to Burn "State the obvious, I didn't…
3 Taylor Swift Taylor Swift Teardrops on my Guitar "Drew looks at me,\nI fake a…
4 Taylor Swift Taylor Swift A Place in This World "I don't know what I want, s…
5 Taylor Swift Taylor Swift Cold As You "You have a way of coming ea… # ℹ 127 more rows
Die Songtexte sind in der Spalte Lyrics
, zusätzlich gibt es noch Angaben zu Song und Album.
1.1.2 Andere Formate
Manchmal liegen Daten in weniger verbreiteten Formaten wie SPSS oder Stata-Datensätze oder, wie im folgenden Beispiel, als sog. parquet-File vor. Auf der Plattform Hugging Face hat ein Nutzer die Inhalte der Tagesschau-Website per Webscraping (s.u.) heruntergeladen und aufbereitet. Eine kurze Web-Recherche ergab, dass für das Einlesen dieses Datenformats das R-Paket arrow
zur Verfügung steht. Dieses laden wir (nachdem wir es installiert haben, was RStudio uns vorschlagen sollte) und öffnen dann die Datei direkt über die URL.
library(arrow)
# Source: https://huggingface.co/datasets/bjoernp/tagesschau-2018-2023
<- arrow::read_parquet("https://huggingface.co/api/datasets/bjoernp/tagesschau-2018-2023/parquet/default/train/0.parquet")
tagesschau_archiv tagesschau_archiv
# A tibble: 21,847 × 6
date headline short_headline short_text article link
<chr> <chr> <chr> <chr> <chr> <chr>
1 2023-04-27 Türkei-Wahl in Deutschland… 1,5 Millionen… "Etwa 1,5… "Etwa … /aus…
2 2023-04-27 Bolsonaro bestreitet Verwi… Brasilien "Brasilie… "Brasi… /aus…
3 2023-04-27 Streiten, ob Hilfe wirklic… Afghanistan-P… "Deutschl… "Deuts… /inl…
4 2023-04-27 Republikaner machen Druck … US-Haushaltss… "Die Repu… "Die R… /aus…
5 2023-04-27 Russland plant Schein-Orga… Strategiepapi… "Russland… "Russl… /inv… # ℹ 21,842 more rows
Der Datensatz enthält über 20.000 Beiträge, und neben dem Veröffentlichungsdatum die Überschriften und Texte der Artikel, je in einer kurzen und langen Fassung.
1.2 Spezifische R-Pakete
Neben unzähligen Paketen für spezielle statistische Verfahren, Datenmanagement oder Visualisierung gibt es auch für viele Online-Dienste R-Pakete, mit denen Daten dieser Dienste komfortable heruntergeladen, modifiziert und z.T. auch wieder hochgeladen werden können. Eine unvollständige und z.T. auch veraltete Übersicht gibt es im CRAN Task Web Technologies, wo auch ein Unterpunkt für Social Media Clients zu finden ist. Leider sind einige Pakete, wie etwa Rfacebook
oder instaR
durch zunehmden Restriktionen und Veränderungen der Plattformen obsolet. Wir wählen daher zwei, die aktuell noch funktionieren, Mastodon und TikTok.
1.2.1 Mastodon-Posts
Die Mastodon-Plattform wird als Alternative zu Twitter/X vor allem für das sog. Microblogging benutzt. Die ARD betreibt eine eigene Instanz, um dort ihre Inhalte zu verbreiten. Wir können die öffentlichen Posts (oder Toots) ganz leicht mit dem rtoot
Paket herunterladen.
library(rtoot)
<- rtoot::get_timeline_public(instance = "ard.social")
ard_toots ard_toots
# A tibble: 20 × 29
id uri created_at content visibility sensitive spoiler_text
<chr> <chr> <dttm> <chr> <chr> <lgl> <chr>
1 111368582… http… 2023-11-07 09:35:02 "<p>Sä… public FALSE ""
2 111368576… http… 2023-11-07 09:33:39 "<p>Er… public FALSE ""
3 111368550… http… 2023-11-07 09:27:01 "<p>Di… public FALSE ""
4 111368523… http… 2023-11-07 09:20:05 "<p>It… public TRUE ""
5 111368483… http… 2023-11-07 09:10:00 "<p>De… public FALSE ""
# ℹ 15 more rows
# ℹ 22 more variables: reblogs_count <int>, favourites_count <int>,
# replies_count <int>, url <chr>, in_reply_to_id <chr>,
# in_reply_to_account_id <chr>, language <chr>, text <lgl>,
# application <I<list>>, poll <I<list>>, card <I<list>>, account <list>,
# reblog <I<list>>, media_attachments <I<list>>, mentions <I<list>>, # tags <I<list>>, emojis <I<list>>, favourited <lgl>, reblogged <lgl>, …
Neben dem eigentlichen Inhalt erhalten wir eine große Anzahl an Metadaten, u.a. auch Popularitätsindikatoren wie die Anzahl replies oder favourites.
1.2.2 TikTok-Videos
Das Paket traktok
ist z.Z. nicht auf CRAN, daher müssen wir es manuell mit der install_github()
Funktion installieren. Außerdem benötigen wir einen Browser-Cookie, da die TikTok-Website selbst ohne Login einen entsprechenden Cookie erfordert. Dieser ist als Datei unter data/tt_cookie.txt
hinterlegt und wird zunächst eingelesen.
::install_github("JBGruber/traktok", dependencies = F)
remoteslibrary(traktok)
::tt_get_cookies(x = "data/tt_cookie.txt") traktok
Als erstes laden wir mit tt_user_videos()
eine Liste der letzten Videos eines Accounts herunter. Aufgrund sog. Rate-Limits seitens TikTok kann dies mal länger, mal kürzer dauern.
<- traktok::tt_user_videos("https://www.tiktok.com/@edsheeran")
ed ed
# A tibble: 35 × 2
user_id `video_urls <- ...`
<chr> <chr>
1 edsheeran https://www.tiktok.com/@edsheeran/video/7284119424620137760
2 edsheeran https://www.tiktok.com/@edsheeran/video/7270920001131957536
3 edsheeran https://www.tiktok.com/@edsheeran/video/7256495610889653530
4 edsheeran https://www.tiktok.com/@edsheeran/video/7296153034474736928
5 edsheeran https://www.tiktok.com/@edsheeran/video/7293926597944462625 # ℹ 30 more rows
Wie wir sehen, ist das Ergebnis ein recht sparsamer Datensatz, in dem eigentlich nur die URLs der einzelnen Videos zu finden sind. Ein oder mehrere Videos samt Metadaten können wir über tt_videos()
herunterladen, wobei standardmäßig die Videos selbst nicht gespeichert werden.
<- traktok::tt_videos("https://www.tiktok.com/@edsheeran/video/7289455403551804704")
ed_video1 |>
ed_video1 glimpse()
Rows: 1
Columns: 16
$ video_id <chr> "7289455403551804704"
$ video_url <chr> "https://www.tiktok.com/@edsheeran/video/7289455…
$ video_timestamp <dbl> 1697208599
$ video_length <int> 22
$ video_title <chr> "I love doing this so much. Thanks @Save The Mus…
$ video_locationcreated <chr> "GB"
$ video_diggcount <int> 30200
$ video_sharecount <int> 104
$ video_commentcount <int> 302
$ video_playcount <int> 334200
$ video_description <chr> "I love doing this so much. Thanks @Save The Mus…
$ video_fn <chr> "./edsheeran_video_7289455403551804704.mp4"
$ author_username <chr> "edsheeran"
$ author_name <chr> "Ed Sheeran"
$ download_url <chr> "https://v16-webapp-prime.tiktok.com/video/tos/u… $ html_status <int> 200
Auch hier werden neben die Informationen zum Video wie Titel und Länge auch Metadaten und Indikatoren wie die Anzahl an Views mitgeliefert. Dies ist allerdings nur eine kleine Auswahl an Informationen, die der Autor des Pakets für besonders relevant erachtet. TikTok selbst liefert viele hundert weitere Spalten mit, die allerdings nur etwas aufwändiger aus dem JSON-Format (s.u.) extrahiert werden müssen.
1.3 Feeds und Web-APIs
Mit Feeds und JSON-APIs stehen seit langem standardisierte Zugriffsmechanismen für Web-Inhalte zur Verfügung. Diese sind mal mehr (RSS-Feeds), mal weniger (JSON) standardisiert: RSS-Feeds haben stets dieselben Spalten wir title
oder description
, während das JSON-Format syntaktisch zwar immer gleich ist, die enthaltenen Felder aber unterschiedlich sind. Daher müssen wir JSON-Daten immer manuell inspizieren, um relevante Felder zu finden, während Feeds leichter zu verarbeiten sind.
1.3.1 RSS Feeds
Für das Einlesen von Feeds gibt es das tidyRSS
Paket mit dem entsprechenden Befehl tidyfeed()
, dem wir nur die URL des Feeds übergeben müssen. Hier nehmen wir die Schlagzeilen von SPIEGEL Online.
library(tidyRSS)
<- tidyRSS::tidyfeed("https://www.spiegel.de/schlagzeilen/tops/index.rss")
spiegel |>
spiegel glimpse()
Rows: 20
Columns: 14
$ feed_title <chr> "DER SPIEGEL - Schlagzeilen – Tops", "DER SPIEGEL…
$ feed_link <chr> "https://www.spiegel.de/", "https://www.spiegel.d…
$ feed_description <chr> "Deutschlands führende Nachrichtenseite. Alles Wi…
$ feed_language <chr> "de", "de", "de", "de", "de", "de", "de", "de", "…
$ feed_pub_date <dttm> 2023-11-07 10:38:30, 2023-11-07 10:38:30, 2023-1…
$ feed_last_build_date <dttm> 2023-11-07 10:38:30, 2023-11-07 10:38:30, 2023-1…
$ feed_category <chr> "Wirtschaft", "Wirtschaft", "Wirtschaft", "Wirtsc…
$ item_title <chr> "Gasversorgung in Deutschland: Darum ist Deutschl…
$ item_link <chr> "https://www.spiegel.de/wirtschaft/service/gasver…
$ item_description <chr> "Die Angst vor Gasknappheit bestimmte den letzten…
$ item_pub_date <dttm> 2023-11-07 00:45:53, 2023-11-07 06:09:33, 2023-1…
$ item_guid <chr> "https://www.spiegel.de/wirtschaft/service/gasver…
$ item_enclosure <list> [], [], [], [], [], [], [], [], [], [], [], [], … $ item_category <chr> "Wirtschaft", "Politik", "Ausland", "Ausland", "A…
Interessant sind für uns vor allem die Item-Variablen, etwa item_title
oder item_description
, die sich auf die einzelnen Beiträg beziehen. Ebenso gibt es wieder Metadaten wie etwas das Publikationsdatum oder die Kategorie.
1.3.2 JSON-APIs
Für das Einlesen von JSON-Daten gibt es das jsonlite
Paket mit der fromJSON()
Funktion. Als Beispiel rufen wir die offizielle JSON-API der Tagesschau auf, die hier dokumentiert ist. Die eigentlichen Nachrichten sind im Feld news
, das wir in einen Datenframe konvertieren können.
library(jsonlite)
# Source: https://github.com/AndreasFischer1985/tagesschau-api
<- jsonlite::fromJSON("https://www.tagesschau.de/api2/news")$news |>
tagesschau_news as_tibble()
tagesschau_news
# A tibble: 122 × 24
sophoraId externalId title date teaserImage$title tags updateCheckUrl
<chr> <chr> <chr> <chr> <chr> <lis> <chr>
1 ofarim-verleumd… tagesscha… Säng… 2023… "Gil Ofarim (M),… <df> https://www.t…
2 liveblog-israel… 222eea00-… ++ W… 2023… <NA> <df> https://www.t…
3 ndr-news-blog-z… tagesscha… News… 2023… "Boris Herrmann … <df> https://www.t…
4 video-1269700 f10f9aa1-… Euro… 2023… "Sendungsbild" <df> https://www.t…
5 video-1269698 f22eb9e1-… Aktu… 2023… "Sendungsbild" <df> https://www.t…
# ℹ 117 more rows
# ℹ 21 more variables: teaserImage$copyright <chr>, $alttext <chr>,
# $imageVariants <df[,12]>, $type <chr>, tracking <list>, topline <chr>,
# firstSentence <chr>, details <chr>, detailsweb <chr>, shareURL <chr>,
# geotags <list>, regionId <int>, regionIds <list>, type <chr>,
# breakingNews <lgl>, brandingImage <df[,5]>, streams <df[,4]>, # alttext <chr>, copyright <chr>, ressort <chr>, comments <chr>
Auch hier gibt es zahlreiche Spalten, u.a. title
oder date
.
1.4 Web-Scraping
Wenn die für uns relevanten Informationen nicht in einem einfachen maschinenlesbaren Format vorliegen, müssen wir sie aus dem Quellcode der Webseite extrahieren. Diese besteht aus sog. HTML-Markup, das viele einzelne Elemente definiert. Die Herausforderung beim Web-Scraping besteht darin, die richtigen Elemente zu finden und zu extrahieren. Der Aufwand hierfür schwankt von Website zu Website. Wir verwenden dafür das Paket rvest
, mit dem zumindest technisch der Zugriff auf verschiedene HTML-Element recht leicht ist.
1.4.1 HTML-Tabellen
Am einfachsten haben wir es, wenn die Informationen bereits als HTML-Tabelle, d.h. im Element <table>
vorliegen. Dies ist z.B. bei vielen Wikipedia-Artikeln der Fall. Hier wollen wir die nach American Film Institute wichtigstens 100 Filme als Tabelle in R einlesen. Dazu lesen wir zunächst mit read_html()
die entsprechende Wikipedia-Seite ein und geben das Ergebnis weiter an die Funktion html_table()
, die alle HTML-Tabellen extrahiert.
library(rvest)
<- rvest::read_html("https://en.wikipedia.org/wiki/AFI%27s_100_Years...100_Movies_(10th_Anniversary_Edition)") |>
afi_list ::html_table()
rvest
afi_list
[[1]]
# A tibble: 14 × 2
X1 X2
<chr> <chr>
1 1998 100 Movies
2 1999 100 Stars
3 2000 100 Laughs
4 2001 100 Thrills
5 2002 100 Passions
# ℹ 9 more rows
[[2]]
# A tibble: 100 × 6
Rank `10th anniversary list (2007)` Director Year `Production companies`
<dbl> <chr> <chr> <int> <chr>
1 1 Citizen Kane Orson Welles 1941 Mercury Productions, …
2 2 The Godfather Francis For… 1972 Paramount Pictures, A…
3 3 Casablanca Michael Cur… 1942 Warner Bros.
4 4 Raging Bull Martin Scor… 1980 Chartoff-Winkler Prod…
5 5 Singin' in the Rain Gene Kelly,… 1952 Metro-Goldwyn-Mayer
# ℹ 95 more rows
# ℹ 1 more variable: `Change from 1998` <chr>
[[3]]
# A tibble: 1 × 2
vteAFI's 100 Years...100 Movies (10th Anniversary Edi…¹ vteAFI's 100 Years..…²
<chr> <chr>
1 "Citizen Kane (1941)\nThe Godfather (1972)\nCasablanca… "Citizen Kane (1941)\…
# ℹ abbreviated names:
# ¹`vteAFI's 100 Years...100 Movies (10th Anniversary Edition)`, # ²`vteAFI's 100 Years...100 Movies (10th Anniversary Edition)`
Wir sehen, dass es 3 Tabellen auf der Wikipedia Seite gibt, und wir eigentlich nur die 2. Tabelle wollen, daher wählen wir nur diese aus.
2] afi_list[
[[1]]
# A tibble: 100 × 6
Rank `10th anniversary list (2007)` Director Year `Production companies`
<dbl> <chr> <chr> <int> <chr>
1 1 Citizen Kane Orson Welles 1941 Mercury Productions, …
2 2 The Godfather Francis For… 1972 Paramount Pictures, A…
3 3 Casablanca Michael Cur… 1942 Warner Bros.
4 4 Raging Bull Martin Scor… 1980 Chartoff-Winkler Prod…
5 5 Singin' in the Rain Gene Kelly,… 1952 Metro-Goldwyn-Mayer
# ℹ 95 more rows # ℹ 1 more variable: `Change from 1998` <chr>
Der Datenframe entspricht genau der HTML-Tabelle auf der Seite.
1.4.2 HTML Scraping
Eine typische Aufgabe für Web-Scraping ist das Extrahieren von Überschriften und Texten aus Nachrichtenseiten, wie dies der Nutzer im o.g. Beispiel der Tagesschau gemacht hat. Wir wollen hier die Schlagzeilen der Heute-Website extrahieren. Eine ausführliche Inspektion der Seite ergibt, dass die Schlagzeilen im Element h3
mit der Klasse teaser-title
liegen. Wir extrahieren alle HTML-Elemente dieser Klasse mit der Funktion html_elements()
.
<- rvest::read_html("https://www.heute.de") |>
heute_headlines ::html_elements("h3.teaser-title")
rvestlength(heute_headlines)
[1] 78
Aktuell gibt es 78 dieser Elemente auf der Heute-Website. Die eigentlichen Texte können wir mit html_text()
erhalten, mit str_squish()
entfernen wir die von der Redaktion offenbar sehr großzügig vergebenen Leerzeichen innerhalb der Schlagzeilen.
|>
heute_headlines ::html_text() |>
rveststr_squish() |>
head()
[1] "Bund und Länder einig Scholz: Bund zahlt 7.500 Euro pro Flüchtling"
[2] "Bund-Länder-Pakt Turbo für Bauvorhaben: Umweltverbände besorgt"
[3] "Umfrage Jeder vierte Deutsche fühlt sich einsam"
[4] "Krieg in Nahost"
[5] "Luftangriffe und Kämpfe" [6] "Israel und Palästina Das Problem mit der Zwei-Staaten-Lösung"
Wollen wir nun statt der Texte die URLs der einzelnen Beiträge bekommen, müssen wir in das Unterelement <a>
und dessen Attribut href
schauen. Dort sind in HTML die URLs einer Verlinkung hinterlegt. Diese URLs könnten wir in einem nächsten Schritt nun auch wieder per Web-Scraping weiterverarbeiten.
|>
heute_headlines ::html_elements("a") |>
rvest::html_attr("href") |>
rvesthead()
[1] "/nachrichten/politik/migration-gipfel-bund-laender-scholz-100.html"
[2] "/nachrichten/politik/bund-laender-pakt-bau-beschleunigung-umweltverbaende-100.html"
[3] "/nachrichten/panorama/einsamkeit-deutschland-umfrage-100.html"
[4] "/nachrichten/politik/zwei-staaten-loesung-palaestina-israel-100.html"
[5] "/nachrichten/politik/baerbock-g7-japan-feuerpause-gaza-israel-100.html" [6] "/nachrichten/politik/usa-u-boot-atomwaffen-naher-osten-100.html"