12 Arbeiten mit Textdaten
Insbesondere wenn wir zu Medieninhalten forschen, sind wir häufig mit Textdaten konfrontiert. In diesem Kapitel wird daher ein Überblick über die wichtigsten Funktionen zur Arbeit mit character
-Variablen gegeben sowie das Konzept der regulären Ausdrücke eingeführt.
Bevor wir damit beginnnen, nochmals eine kurze Wiederholung zu character
-Objekten in R sowie ein neues Konzept.
Zeichenketten (auch Strings genannt) werden in R (ebenso wie in nahezu allen Programmiersprachen) durch Anführungszeichen definiert:
Dabei ist es unerheblich, ob einfache oder doppelte Anführungszeichen verwendet werden:
Somit können auch Zeichenketten gespeichert werden, die (das jeweils andere) Anführungszeichen enthalten:
Was machen wir, wenn beide Arten von Anführungszeichen in einem Textobjekt vorkommen sollen? In diesem Fall helfen uns Maskierungszeichen (Escape Characters) weiter, Zeichen, die einer Programmiersprache signalisieren, das nachfolgende Funktionszeichen als einfaches Zeichen ohne spezielle Funktion zu behandeln. In R (wie auch in den meisten anderen Sprachen) wird der Backslash \
als Maskierungszeichen verwendet:
In der Konsolenausgabe zeigt R auch Maskierungszeichen an:
## [1] "In diesem \"Text\" befinden sich weitere Anführungszeichen"
Möchten wir den tatsächlichen Inhalt eines Textobjekts sehen, können wir die Funktion writeLines()
verwenden:
## In diesem "Text" befinden sich weitere Anführungszeichen
Im Übrigen bedeutet dies auch, dass wir, wenn ein Backslash in einem String vorkommen soll, diesen durch einen vorangestellten Backslash maskieren müssen – wir signalisieren R also durch das Maskierungszeichen \
, dass der nachfolgende \
nicht als Maskierungszeichen behandelt werden soll:
## \
12.1 Einfache String-Operationen mit stringr
Das Tidyverse enthält das Package stringr
, das auf den Umgang mit Strings spezalisiert ist. Alle relevanten Funktionen beginnen mit dem Suffix str_
.27
Wir laden daher zunächst wieder das Tidyverse-Package.
Alle str_
-Funktionen sind vektorisiert, werden also auf jedes Element eines (Text-)Vektors angwendet. Dadurch kann man sie auch gut auf Textvariablen in Datensätzen bzw. Tibbles anwenden, um etwa mit der mutate()
-Funktion bestehende Variablen zu verändern oder neue Variablen zu erzeugen. Zur Demonstration der Funktionen wird aber der Einfachkeit halber mit einfachen Textvektoren gearbeitet. Das erste Argument der str_
-Funktionen ist immer ein Textvektor.
12.1.1 Zeichenlänge bestimmen mit str_length()
Mit str_length()
zählen wir die Anzahl an Zeichen in einem String:28
## [1] 5 5 7
12.1.2 Strings zusammenfügen mit str_c()
und str_glue()
Mit str_c()
lassen sich mehrere einzelne Strings zusammenfügen; dabei kann über das Argument sep
eine Zeichenkette zum Trennen der Begriffe genutzt werden:29
## [1] "Guten Tag"
Wird ein Vektor mit mehr als einem Element übergeben, werden weitere Strings an jedes Vektorelement angehängt:
## [1] "Apfel-Mus" "Mango-Mus" "Kumquat-Mus"
Soll stattdessen ein Vektor mit mehreren Strings in einen einzelnen String umgewandelt werden, muss über das Argument collapse
eine Trennzeichenkette angegeben werden (wobei auch ein leerer String ""
übergeben werden kann):
## [1] "Apfel, Mango, Kumquat"
Für komplexere String-Verknüpfungen bietet sich die Funktion str_glue()
an, mit der mittels geschweifter Klammern {}
Objektnamen oder ganze R-Ausdrücke als Platzhalter definiert werden können:
## Apfel hat 5 Buchstaben.
## Mango hat 5 Buchstaben.
## Kumquat hat 7 Buchstaben.
12.1.3 Teile von Strings auswählen mit str_sub()
str_sub()
(von Subset) kann genutzt werden, um einen Teil eines Strings auszuwählen, wobei die Start- und Endposition als zweites und drittes Argument übergeben werden:30
## [1] "Ap" "Ma" "Ku"
Mit negativen Werten wird von hinten gezählt:
## [1] "el" "go" "at"
Das kann natürlich auch kombiniert werden:
# Entferne den Anfangsbuchstaben (wähle alle Zeichen vom zweiten bis zum letzten aus):
str_sub(obst, 2, -1)
## [1] "pfel" "ango" "umquat"
12.1.4 Groß- und Kleinschreibung transformieren mit str_to
-Funktionen
str_to_lower()
und str_to_upper()
wandeln Strings komplett in Klein- bzw. Großbuchstaben um:31
## [1] "apfel" "mango" "kumquat"
## [1] "APFEL" "MANGO" "KUMQUAT"
Daneben gibt es noch str_to_title()
(alle Anfangsbuchstaben groß) und str_to_sentence()
(Anfangsbuchstaben des ersten Wortes groß, alles andere klein):
str_to_title("in the beginning the Universe was created. this has made a lot of people very angry and been widely regarded as a bad move.")
## [1] "In The Beginning The Universe Was Created. This Has Made A Lot Of People Very Angry And Been Widely Regarded As A Bad Move."
12.1.5 Überschüssigen Whitespace entfernen mit str_trim()
und str_squish
str_trim()
entfernt alle Leerzeichen am Anfang und am Ende eines Strings. str_squish()
entfernt auch mehrfache Leerzeichen innerhalb eines Strings:
## [1] "ein unsauberer String"
## [1] "ein unsauberer String"
12.2 Reguläre Ausdrücke
Gerade bei unbereinigten Daten ist es oft das Ziel, bestimmte Muster in Strings zu erkennen (wird beispielsweise eine bestimmte Person in einem Text genannt?) und/oder zu extrahieren. Hier kommen reguläre Ausdrücke (auf Englisch: regular expressions oder auch kurz RegEx) ins Spiel – Zeichenketten, die Muster in Zeichenketten formal beschreiben.
Reguläre Ausdrücke sehen für Laien oft aus wie Kauderwelsch und benötigen etwas Einübungszeit. Mit den Hilfsfunktionen str_view()
(erste Übereinstimmung in einem String) und str_view_all()
(alle Übereinstimmungen in einem String) können wir uns schnell anzeigen lassen, ob ein RegEx-Muster in einem String vorkommt oder nicht.
12.2.1 Exakte Übereinstimmungen
Im einfachsten Fall suchen wir nach einer exakten Zeichenkette – etwa um festzustellen, ob der Name Trump
in aktuellen Schlagzeilen zur USA auftaucht:
12.2.2 Anker
Mit den Sonderzeichen ^
und $
definieren wir, dass die zu suchende Zeichenkette sich am Anfang (^
) bzw. am Ende ($
) des Strings befindet. Um etwa nur Schlagzeilen zu finden, die mit "Trump"
beginnen, suchen wir nach dem Muster "^Trump"
:
Analog findet n$
nur in der letzten Schlagzeile eine Übereinstimmung, da nur diese auf "n"
endet:
Wie können wir dann nach dem Vorkommen eines Dollarsymbols suchen? Hier müssen wir – wie auch bei allen folgenden RegEx-Sonderzeichen – wieder auf das Maskierungszeichen \
zurückgreifen. Allerdings ist der Backslash \
in R ja bereits als Maskierungszeichen für Strings im Allgemeinen – und nicht als Maskierungszeichen für RegEx – definiert. Wir müssen den \
daher mit einem weiteren \
maskieren, damit R die RegEx-Zeichenfolge \$
erkennt (ja, das ist Anfangs sehr verwirrend):
12.2.3 Mehrere Suchbegriffe
Mit dem uns schon bekannten ODER
-Symbol |
können wir nach dem Vorkommen mehrerer Zeichenketten suchen:
12.2.4 Quantifier
Sogenannte Quantifier können dazu genutzt werden, um festzulegen, wie oft das zuvor angebene Muster in dem String vorkommen muss:
*
: 0-mal oder öfter (sinnvoll, um optionale Bestandteile zu definieren)+
: 1-mal oder öfter _{n}
: Exaktn
-mal{n,}
: Mindestensn
-mal{n,m}
: Mindestensn
-mal, maximalm
-mal
Quantifier beziehen sich standardmäßig auf ein einzelnes, vorangestelltes Zeichen. Soll eine längere Zeichenkette mit einem Quantifier versehen werden, kann diese in runde Klammern ()
gestellt werden:
12.2.5 Spezielle Zeichentypen
Um bestimmte Zeichentypen zu matchen, stehen u. a. folgende Zeichen(folgen) zur Verfügung:
.
: Alle Zeichen\d
: Alle Ziffern (und\D
das Gegenteil, also alles außer Ziffern)\w
: Alle alphanumerischen Zeichen (Klein- und Großbuchstaben, Ziffern, Unterstrich;\W
das Gegenteil)\s
: Whitespace (Leerzeichen, Umbrüche;\S
das Gegenteil)
(Bei den drei letztgenannten muss in R der Backslash maskiert werden, also z. B. "\\d"
).
Zudem können zu matchende Zeichentypen durch eckige Klammern selbst definiert werden – [abc]
beispielsweise matcht ein a
, b
oder c
.
12.2.6 RegEx-Zeichen kombinieren
Natürlich können wir all diese Zeichen kombinieren, um komplexere Muster zu matchen. Mit dem Muster "[\\w-]+\\s+\\d+[a-z]*"
erfassen wir beispielsweise typische deutsche Straßennamen mitsamt Hausnummern, die in Regel nach dem Muster “Straßenname Hausnummer” aufgebaut sind – das sieht auf den ersten Blick sehr undurchsichtig aus, lässt sich aber wie folgt aufschlüsseln:
\w
sucht nach allen alphanumerischen Zeichen; für R müssen wir den Backslash maskieren, schreiben also\\w
.- Der Bindestrich
-
ist in diesen Zeichen nicht enthalten, wir fügen diesen also noch manuell hinzu und umschließen beides in eckigen Klammern[]
.[\\w-]
sucht also nach allen alphanumerischen Zeichen und dem Bindestrich-
. - Wir geben nun an, dass wir diese Zeichen mindestens einmal vorfinden möchten, daher schließt an dieses Suchmuster das
+
an. Damit dürften wir so ziemlich alle deutschen Straßennamen abdecken. - Typischerweise folgt auf den Straßennamen ein Leerzeichen und dann die Hausnummer. Leerzeichen und andere Whitespace-Zeichen matchen wir mit
\s
, wobei auch hier ein weiterer\
zum Maskieren benötigt wird. Damit wir auch Fälle erfassen, in denen (aus Versehen) zwei oder mehr Leerzeichen zwischen Straßenname und Hausnummer stehen, schließen wir erneut den Quantifier+
an – wir suchen also nach mindestens einem Leerzeichen. - Die Hausnummer besteht in der Regel aus einer oder mehrerer Ziffen; dies matchen wir mittels
\\d+
. - Manche Hausnummern haben zusätzlich noch einen Kleinbuchstaben, um unterschiedliche Gebäudeeinheiten zu unterscheiden. Mittels
[a-z]
legen wir fest, dass alle Kleinbuchstaben vona
bisz
gesucht werden sollen. Dieses Muster ist jedoch optional, da nicht alle Hausnummern darauf enden. Wir fügen also hier ein*
an, das die vorangestellte Zeichenfolge 0-mal oder öfter matcht.
adressen <- c(
"Oettingenstraße 67",
"Geschwister-Scholl-Platz 1",
"Schellingstraße 3a"
)
str_view(adressen, "[\\w-]+\\s+\\d+[a-z]*")
Ja, das sieht auf den ersten Blick aus wie Kauderwelsch und ist zu Beginn nicht sonderlich intuitiv; man gewöhnt sich aber daran. Und: für viele Anwendungsfälle (z. B. URLs oder Twitter-IDs aus einem Text extrahieren) findet man online schnell passende RegEx-Phrasen, die dann nur noch auf R angepasst (Maskierungszeichen!) und validiert werden müssen.
12.3 RegEx und stringr
Schauen wir uns nach diesem eher abstrakten Überblick einige praktische Anwendungsbeispiele an, die Funktionen auf dem stringr
-Package verwenden.
12.3.1 Muster finden mit str_detect()
str_detect()
prüft für einen Textvektor, ob das angegebene Muster darin vorkommt, und gibt dies als logischen Vektor zurück:
schlagzeilen <- c(
"Nach Feier: 140 Personen in Corona-Quarantäne",
"Wo die Auflagen gelockert werden",
"Corona-Lockerungen: Das ist seit Montag anders"
)
str_detect(schlagzeilen, "Corona")
## [1] TRUE FALSE TRUE
Das kann z. B. auch dazu genutzt werden, schnell einen Datensatz zu filtern. Nehmen wir beispielsweise den Beispiel-Datensatz starwars
, der zum Tidyverse-Package gehört und entsprechend mit starwars
aufgerufen werden kann.
## # A tibble: 87 x 14
## name height mass hair_color skin_color eye_color birth_year sex gender homeworld species films vehicles starships
## <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr> <chr> <chr> <list> <list> <list>
## 1 Luke Skywalker 172 77 blond fair blue 19 male masculine Tatooine Human <chr [5]> <chr [2]> <chr [2]>
## 2 C-3PO 167 75 <NA> gold yellow 112 none masculine Tatooine Droid <chr [6]> <chr [0]> <chr [0]>
## 3 R2-D2 96 32 <NA> white, blue red 33 none masculine Naboo Droid <chr [7]> <chr [0]> <chr [0]>
## 4 Darth Vader 202 136 none white yellow 41.9 male masculine Tatooine Human <chr [4]> <chr [0]> <chr [1]>
## 5 Leia Organa 150 49 brown light brown 19 female feminine Alderaan Human <chr [5]> <chr [1]> <chr [0]>
## 6 Owen Lars 178 120 brown, grey light blue 52 male masculine Tatooine Human <chr [3]> <chr [0]> <chr [0]>
## 7 Beru Whitesun lars 165 75 brown light blue 47 female feminine Tatooine Human <chr [3]> <chr [0]> <chr [0]>
## 8 R5-D4 97 32 <NA> white, red red NA none masculine Tatooine Droid <chr [1]> <chr [0]> <chr [0]>
## 9 Biggs Darklighter 183 84 black light brown 24 male masculine Tatooine Human <chr [1]> <chr [0]> <chr [1]>
## 10 Obi-Wan Kenobi 182 77 auburn, white fair blue-gray 57 male masculine Stewjon Human <chr [6]> <chr [1]> <chr [5]>
## # ... with 77 more rows
Um schnell die Star-Wars-Figuren auszuwählen, deren Name eine Ziffer beinhaltet (C-3PO, R2D2 usw.), können wir filter()
und str_detect()
kombinieren:
## # A tibble: 6 x 14
## name height mass hair_color skin_color eye_color birth_year sex gender homeworld species films vehicles starships
## <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr> <chr> <chr> <list> <list> <list>
## 1 C-3PO 167 75 <NA> gold yellow 112 none masculine Tatooine Droid <chr [6]> <chr [0]> <chr [0]>
## 2 R2-D2 96 32 <NA> white, blue red 33 none masculine Naboo Droid <chr [7]> <chr [0]> <chr [0]>
## 3 R5-D4 97 32 <NA> white, red red NA none masculine Tatooine Droid <chr [1]> <chr [0]> <chr [0]>
## 4 IG-88 200 140 none metal red 15 none masculine <NA> Droid <chr [1]> <chr [0]> <chr [0]>
## 5 R4-P17 96 NA none silver, red red, blue NA none feminine <NA> Droid <chr [2]> <chr [0]> <chr [0]>
## 6 BB8 NA NA none none black NA none masculine <NA> Droid <chr [1]> <chr [0]> <chr [0]>
12.3.2 Muster zählen mit str_count()
str_count()
funktioniert analog zu str_detect()
, nur dass kein logischer Vektor, sondern ein numerischer Vektor zurückgegeben wird, in dem gezählt wird, wie häufig das gesuchte Muster in den jeweiligen Strings vorkommt:
## [1] 0 2 1
12.3.3 Muster extrahieren mit str_extract()
Neben dem Prüfen, ob ein bestimmtes Muster vorhanden ist, zählt das Extrahieren dieser Muster zu den häufigsten Anwendungsfällen. Das ist die Aufgabe der Funktion str_extract()
:32
## [1] "Herr" "Frau"
Für ein etwas komplexeres Beispiel nehmen wir einmal an, wir finden folgenden Datensatz vor:
test_ergebnis <- tibble(kandidat = c("A", "B", "C", "D", "E", "F"),
punkte = c("1", "2", "0.32", ".555", "-22", "33 Punkte"))
test_ergebnis
## # A tibble: 6 x 2
## kandidat punkte
## <chr> <chr>
## 1 A 1
## 2 B 2
## 3 C 0.32
## 4 D .555
## 5 E -22
## 6 F 33 Punkte
Für weitere Analysen wäre es natürlich deutlich angenehmer, wenn wir mit den Punktewerten auch rechnen könnten. Wir können diese mit einer RegEx extrahieren. Dafür müssen wir formalisieren, wie Punktezahlen in diesem Datensatz aufgebaut sein können:
- zunächst steht ein optionales
-
für negative Werte; zur Erinnerung: mit einem Asterisk*
legen wir fest, dass das vorangegange Zeichen mindestens 0-mal, d.h. optional vorkommen soll. Wir beginnen unsere RegEx-Zeichenfolge daher mit"-*"
. - Dann folgt, ebenfalls optional, eine oder mehrere Ziffern. Wir benötigen also das (maskierte) Sonderzeichen
\\d
für Ziffern sowie erneut ein Asterisk*
; unsere RegEx-Folge lautet nun"-*\\d*
. - Nun folgt, erneut optional, ein Punkt
.
als Dezimaltrennzeichen. Da es sich bei dem Punkt um ein RegEx-Sonderzeichen handelt, müssen wir dieses doppelt maskieren33:\\.
. Auch dieser Punkt ist optional, wir hängen also erneut ein*
an; unsere RegEx-Folge lautet nun"-*\\d*\\.*
- Schließlich und zwingend kommt mindestens eine Ziffer in Zahlen vor. Wir benötigen also erneut das Sonderzeichen für Ziffern
\\d
und legen mit dem Sonderzeichen+
fest, dass dieses mindestens einmal oder öfter vorkommen muss. Unsere finale RegEx-Folge lautet"-*\\d*\\.*\\d+"
.
Da str_extract()
immer Text extrahiert, wandeln wir das Ergebnis noch in den Typ numeric
um:
## # A tibble: 6 x 3
## kandidat punkte punkte_numerisch
## <chr> <chr> <dbl>
## 1 A 1 1
## 2 B 2 2
## 3 C 0.32 0.32
## 4 D .555 0.555
## 5 E -22 -22
## 6 F 33 Punkte 33
12.3.4 Muster ersetzen mit str_replace
(bzw. str_replace_all()
):
Der dritte häufige Anwendungsfall ist, dass Muster ersetzt werden sollen. Dafür können str_replace()
(ersetzt erste Übereinstimmung) und str_replace_all()
(ersetzt alle Übereinstimmungen) verwendet werden, wobei zunächst das zu ersetzende Muster, dann das Replacement angegeben wird. Wurde beispielsweise das Dezimaltrennzeichen fälschlicherweise als Komma, nicht als Punkt eingelesen:
## [1] "1.2" "2.3" "3.66"
Bei mehreren Mustern und zugehörigen Replacements können wir einen benannten Vektor übergeben:
## [1] "Herr Müller" "Frau Meier"
Eine praktische Übersicht über alle relevanten stringr
-Funktionen sowie RegEx in R bietet dieses Cheatsheat.
12.4 Übungsaufgaben
Erstellen Sie für die folgenden Übungsaufgaben eine eigene Skriptdatei oder eine R-Markdown-Datei und speichern diese als ue12_nachname.R
bzw. ue12_nachname.Rmd
ab.
Fügen Sie diesem Datensatz zu einem Experiment mittels mutate()
eine neue Spalte hinzu, die lediglich die Gruppenkennung (A
, B
oder C
) enthält:
experiment <- tibble(experimentalgruppe = c("Gruppe A", "Gruppe B", "Gruppe A", "Gruppe C"))
experiment
## # A tibble: 4 x 1
## experimentalgruppe
## <chr>
## 1 Gruppe A
## 2 Gruppe B
## 3 Gruppe A
## 4 Gruppe C
In der Internet Movie Database verfügt jeder Film über eine eindeutige ID, die nach dem Schema "tt[7 Ziffern]"
aufgebaut ist. Extrahieren Sie diese ID aus den folgenden URLS:
imdb_urls <- c(
"https://www.imdb.com/title/tt6751668/?ref_=hm_fanfav_tt_4_pd_fp1",
"https://www.imdb.com/title/tt0260991/",
"www.imdb.com/title/tt7282468/reviews",
"https://m.imdb.com/title/tt4768776/"
)
Käpseles-Aufgabe (optional)
str_match
funktioniert ähnlich zu str_extract()
, nur dass wir durch runde Klammern ()
Gruppen in einem RegEx-Muster definieren können, die dann getrennt extrahiert werden.
Lesen Sie sich die Dokumentation zu str_match()
durch und. Extrahieren Sie dann aus folgendem Vektor getrennt folgende Adressbestandteile:
- Straßenname
- Hausnummer
- Postleitzahl
- Stadt
- Land
Viele der Funktionen aus
stringr
sind unter anderem Namen auch bereits in der Basisversion von R enthalten. Wir nutzen dennoch vorrangig die Funktionen ausstringr
, da diese neben der einheitlichen Benennung auch eine einheitlichere Syntax aufweisen sowie viele kleine Detailkorrekturen und Hilfsfeatures beinhalten, die man in den Basis-Äquivalenten vermisst.↩︎Die R-Basis-Version dieser Funktion lautet
nchar()
.↩︎In der Basis-Version:
paste()
↩︎In der Basis-Version:
strsub()
.↩︎In der Basis-Version:
tolower()
undtoupper()
.↩︎str_extract()
extrahiert dabei immer die erste Übereinstimmung. Sollen alle Übereinstimmungen mit dem Muster aus einem String extrahier werden, kann die Funktionstr_extract_all()
verwendet werden, deren Ausgabe aber entsprechend etwas unhandlicher ist.↩︎Einmal, damit RegEx merkt, dass wir den Punkt nicht als RegEx-Sonderzeichen behandeln möchten, und einmal, damit R den Maskierungs-Backslash nicht als R-Maskierungszeichen erkennt.↩︎