11  Strings

11.1 Voraussetzungen und erste Beispiele

  • In diesem Kapitel werden die Grundlagen des Pakets stringr behandelt.
  • Man kann Zeichenketten, die auch Strings genannt werden, mit Hilfe von einfachen oder doppelten Anführungszeichen erstellen.
text1 <- "Mit doppelten Anführungszeichen"
text2 <- 'Wenn "Anführungszeichen" im Text vorkommen klappen die einfachen'
  • Das einfache Beispiel zeigt schon, dass einige Sonderzeichen anders behandelt werden müssen. Man nennt diese Sonderzeichen auch Escape-Sequenzen.
x <- c("\'", "\"", "\\", "\u00b5") # einfach, doppelt, Backslash, µ
x 
[1] "'"  "\"" "\\" "µ" 
'
"
\
µ
  • Man sieht, dass die gedruckte Darstellung des Strings und der String selbst nicht automatisch das Gleiche ist!

  • Die komplette Liste der Escape-Sequenzen bekommt man z.B. mit der Hilfe zu Anführungszeichen:

help('"')

11.2 Zusammenfügen von Strings

  • Das Zusammenfügen von Zeichenketten geschieht mit der Funktion str_c(). Dabei kann ein Separator mit dem Argument sep= angegeben werden. Außerdem werden Vektoren recycled.
str_c("x", "y")
[1] "xy"
str_c("x", "y", "z", sep = ", ")         # Separator
[1] "x, y, z"
str_c("x", c("y","z"))                   # vektorisiert -> recycling
[1] "xy" "xz"
str_c(c("a", "b", "c"), collapse = ", ") # kollabiert einen Vektor
[1] "a, b, c"
  • Im Allgemeinen bleiben NAs auch nach dem Zusammenfügen NAs, es sei denn sie sollen explizit als Zeichenkette behandelt werden. Dies ist der wesentliche Unterschied zur Funktion paste() aus Base-R.
x <- c("abc", NA)
str_c("+|", x, "|+")                 # NA bleibt NA
[1] "+|abc|+" NA       
str_c("+|", str_replace_na(x), "|+") # NA wird wie Zeichenkette behandelt
[1] "+|abc|+" "+|NA|+" 

11.3 Teile von Zeichenketten

  • Mit der Funktion str_sub() kann auf Teile der Zeichenkette zugegriffen werden. Dabei geben die Argumente start= und end= die betroffenen Positionen an.
x <- c("Kanne", "Rand", "Laden")
str_sub(x, 1, 3)
[1] "Kan" "Ran" "Lad"
str_sub(x, -3, -1) # Negative Zahlen zählen von hinten
[1] "nne" "and" "den"
  • Es spielt keine Rolle, wenn die Zeichenkette zu kurz ist
y <- c("A")
str_sub(y, 1, 5)
[1] "A"
  • Es ist auch möglich Zuweisungen zu machen:
str_sub(x,1,1) <- "w"
x
[1] "wanne" "wand"  "waden"
str_sub(x,1,1) <- str_to_upper(str_sub(x,1,1))
x
[1] "Wanne" "Wand"  "Waden"

11.3.1 Locales

  • Wir hatten schon die Funktion str_to_upper() gesehen. Analog gibt es auch
options(width = 67)
x <- c("Das ist ein String", "in", "3 Teilen")
str_to_upper(x)
[1] "DAS IST EIN STRING" "IN"                 "3 TEILEN"          
str_to_lower(x)
[1] "das ist ein string" "in"                 "3 teilen"          
str_to_title(x)
[1] "Das Ist Ein String" "In"                 "3 Teilen"          
str_to_sentence(x)
[1] "Das ist ein string" "In"                 "3 teilen"          
  • Gegebenenfalls muss das Argument locale= gesetzt werden.
# Türkisch hat zwei i's: mit und ohne Punkt. Bei Kapitalisieren muss das 
# ggf.  berücksichtigt werden durch setzen eines locale-Arguments:
str_to_upper(c("i", "ı"))
[1] "I" "I"
str_to_upper(c("i", "ı"), locale = "tr")
[1] "İ" "I"
  • Auch die Funktionen str_sort() und str_order() haben das Argument locale=, da verschiedene Sprachen verschiedene Ordnungen in den Alphabeten haben.

11.4 Reguläre Ausdrücke

Einfaches Identifizieren von Mustern

  • Ein wichtiges und effizientes Werkzeug um Muster in Zeichenketten zu identifizieren sind reguläre Ausdrücke.
  • Um das identifizierte Muster zu visualisieren, bieten sich die Funktion str_view() an.
punkt_und_backslash <- c("\\.", "\\\\")
writeLines(punkt_und_backslash)
\.
\\
x <- c("Apfel", "Banane", "Mango", "Traube")
str_view(x, "an")
[2] │ B<an><an>e
[3] │ M<an>go
x <- c("Apfel", "Banane", "Mango", "Traube")
str_view(x, ".a.")
[2] │ <Ban>ane
[3] │ <Man>go
[4] │ T<rau>be

Während die Syntax im oberen Beispiel klar sein sollte, ist im unteren Beispiel der Punkte, nicht sofort klar: es wird nicht nach Punkten in einem Ausdruck gesucht, sondern die Punkte sind Platzhalter. Ein Frage, die sich nun stellt ist Wie indentifiziert man “.”? Dazu muss benötigt man eine Escape-Sequenz.

11.4.1 Anker und Zeichenklassen

Reguläre Ausdrücke betreffen prinzipiell jeden Teil eines Strings. Manchmal möchte man den Anfang oder das Ende betreffend oder nach Klassen von Zeichen suchen.

  • Anfang oder Ende:

  • \^ der Anfang eines Strings

  • \$ das Ende eines Strings

  • Klassen von Zeichen:

    • \d eine beliebige Ziffer,
    • \s ein beliebiges Leerzeichen (z.B. Leertaste, Tab, neue Zeile),
    • [abc] die Buchstaben a, b oder c,
    • [\^{}abc] alles außer die Buchstaben a, b oder c.

Wichtig: Um einen regulären Ausdruck mit \d oder \s zu generieren, muss \ ‘escaped’ werden, und man schreibt:

writeLines(c("\\d", "\\s"))
\d
\s
x <- c("abcd", "dcba", "a")

# Anfang der Zeichenkette
str_view(x, "^a") 
[1] │ <a>bcd
[3] │ <a>
# Ende der Zeichenkette
str_view(x, "a$") 
[2] │ dcb<a>
[3] │ <a>

Beispiele: Zeichenklassen

x <- c("1 2 3 abc", "ABC 123", "abc ABC")

str_view(x, "[bcd]")    
[1] │ 1 2 3 a<b><c>
[3] │ a<b><c> ABC
str_view(x, "\\d")      
[1] │ <1> <2> <3> abc
[2] │ ABC <1><2><3>

Mit Hilfe von | kann zwischen alternativen Möglichkeiten gesucht werden (Vorsicht, schwache Bindungsstärke):

str_view(c("Stefan", "Stephan"), "Ste(f|ph)an") 
[1] │ <Stefan>
[2] │ <Stephan>
str_view(c("Stefan", "Stephan"), "Stef|phan")   
[1] │ <Stef>an
[2] │ Ste<phan>
  • Eine Zeichenklasse, die genau ein Zeichen enthält, ist eine Alternative zu Backslash-Escape Sequenzen.
  • Dies funktioniert mit fast allen Sonderzeichen: \$, . |, ? *, +, (, ), ], {, }
  • Allerdings nicht mit [, \, ^, - da diesen eine besondere Bedeutung innerhalb der regulären Ausdrücke zukommt. Diese müssen mittels Backslash escaped werden.
y <- c("abc", "a.c", "a*c", "a c")

str_view(y, "a[.]c") 
[2] │ <a.c>
str_view(y, ".[*]c") 
[3] │ <a*c>
str_view(y, "a[ ]")  
[4] │ <a >c

11.5 Wiederholungen

  • Der nächste Schritt ist zu kontrollieren wie oft ein Muster auftaucht:

    • ? : 0 oder 1
    • + : 1 oder mehr
    • * : 0 oder mehr
  • Die Bindungsstärke der Operatoren ist sehr hoch, so dass meist runde Klammern gesetzt werden müssen. Der Ausdruck wie?der ist sowohl wider als auch wieder.

  • Es ist auch möglich die Zahl der Treffer genau zu spezifizieren:

    • {n} : genau n
    • {n,} : n oder mehr
    • {,m} : höchstens m
    • {n,m} : zwischen n und m

Dabei wird der längste mögliche String herausgesucht. Setzt man ein ? hinter den Ausdruck, so wird nur die Mindestlänge herausgesucht.

x <- "1888 ist MDCCCLXXXVIII"
str_view(x, "CC?")  
[1] │ 1888 ist MD<CC><C>LXXXVIII
str_view(x, "C[LX]+")
[1] │ 1888 ist MDCC<CLXXX>VIII
str_view(x, "C{2,3}")
[1] │ 1888 ist MD<CCC>LXXXVIII
str_view(x, "C{2,3}?")
[1] │ 1888 ist MD<CC>CLXXXVIII

11.5.1 Gruppierungen und Rückverweisenen

  • Mit den runden Klammern () kann man nicht nur komplexe Ausdrücke eindeutig formulieren, sie erlauben es auch nummerierte (1, 2, usw.) Gruppen (numbered capturing group) zu bilden.
  • Dabei wird der reguläre Ausdruck innerhalb der Klammern gespeichert, und es ist möglich mittel \1, \2 etc. darauf zu verweisen.
str_view(fruit, "(..)\\1", match = TRUE)
 [4] │ b<anan>a
[20] │ <coco>nut
[22] │ <cucu>mber
[41] │ <juju>be
[56] │ <papa>ya
[73] │ s<alal> berry
str_view(fruit, "(o)\\1", match = TRUE)
 [9] │ bl<oo>d orange
[33] │ g<oo>seberry
str_view(fruit, "(.)(.)\\2\\1", match = TRUE)
 [5] │ bell p<eppe>r
[17] │ chili p<eppe>r
str_view(fruit, "(.)(.)(.)\\2\\1", match = TRUE)
[4] │ b<anana>

Anwendungen

Nachdem reguläre Ausdrücke nun bekannt sind, wird es Zeit diese sinnvoll einzusetzen. Folgende Themen sollen dazu behandelt werden:

  • Zeichenketten finden, die ein bestimmtes Muster haben,
  • Positionen eines Musters finden,
  • Muster extrahieren,
  • Muster ersetzen,
  • Strings aufgrund eines Musters teilen.

Reguläre Ausdrücke sind sehr mächtig, und ist verlockend Probleme mit einem einzigen regulären Ausdruck zu lösen. Aber

,,Some people, when confronted with a problem, think ‘I know, I’ll use regular expressions.’ Now they have two problems.``

(Jamie Zawinski)

Ein Beispiel ist dieser reguläre Ausdruck, der überprüft, ob eine E-Mailadresse zulässig ist:

Ist E-Mailadresse zulässig?

11.6 Übereinstimmungen erkennen

  • Ob ein Muster ist einer Zeichenkette erhalten ist oder nicht kann mit der Funktion str_detect() herausgefunden werden.
x <- c("Apfel", "Birne", "Dattel")
str_detect(x, "l")
[1]  TRUE FALSE  TRUE
  • Da die logischen Werte TRUE und FALSE in einem numerischen Kontext zu 1 und 0 werden, ist es möglich auf längeren Vektoren statistische Kenngrößen zu berechnen.
# Einlesen 370105 engl. Wörter 
words <- read_lines("https://tinyurl.com/3t569b4v")

# Wörter der Länge 5:
wordle <- words[str_length(words) == 5] 

# eine Übersicht: 
summary(wordle) 
   Length     Class      Mode 
    15921 character character 
# Wie viele starten mit einem t:
sum(str_detect(wordle, "^t"))
[1] 981
# Wie viele enthalten Doppelkonsonanten:
sum(str_detect(wordle, "([^aeiou])\\1"))
[1] 1358
# Wie viele haben keine Vokale:
sum(!str_detect(wordle, "[aeiou]")) 
[1] 54
sum(str_detect(wordle, "^[^aeiou]+$")) 
[1] 54
# Prozent Wörter enden auf Vokal:
mean(str_detect(wordle, "[aeiou]$"))
[1] 0.2743546
  • Um Wörter mit einer bestimmten Sequenz oder eines bestimmten Musters zu filtern, kann die Funktion str_subset() verwendet werden:
# kompliziert:
wordle[str_detect(wordle, "axy$")]
[1] "ataxy" "braxy" "coaxy" "flaxy"
# oder die Kurzform:
str_subset(wordle, "axy$")
[1] "ataxy" "braxy" "coaxy" "flaxy"
  • Sind die Daten in einem Dataframe, so können die Funktionen analog in der Funktion filter() verwendet werden.
df <- tibble(woerter = wordle, 
             i = seq_along(wordle))
df |> filter(str_detect(woerter, "buy$"))
# A tibble: 2 × 2
  woerter     i
  <chr>   <int>
1 rebuy   11404
2 upbuy   14914
  • Eine Variation von str_detect() ist str_count(), dabei wird die Anzahl der Übereinstimmungen in einer Zeichenkette gezählt.
x <- c("Augsburg", "Atlanta")
str_count(x, "a")
[1] 0 2
# Durchschnittliche Vokalzahl
mean(str_count(wordle, "[aeiou]"))
[1] 1.874443
  • Es ist zu bemerken, dass Treffer niemals überlappen, wie das folgende Beispiel zeigt:
# liefert 2 und nicht 3 Treffer:
str_count("abababa", "aba")
[1] 2

11.7 Übereinstimmungen extrahieren

  • Um den Text einer Übereinstimmung herauszuziehen benutzt man die Funktion str_extract().
  • Um die Funktionsweise zu illustrieren nehmen wir die folgenden Daten:
length(sentences)
[1] 720
head(sentences)  
[1] "The birch canoe slid on the smooth planks." 
[2] "Glue the sheet to the dark blue background."
[3] "It's easy to tell the depth of a well."     
[4] "These days a chicken leg is a rare dish."   
[5] "Rice is often served in round bowls."       
[6] "The juice of lemons makes fine punch."      
  • Als Beispiel sollen nun die in den Sätzen erhaltenen Farben extrahiert werden. Dazu bildet man einen regulären Ausdruck, der die Farben enthält:
colours <- c("red", "orange", "yellow", "green", "blue", "purple")
colour_match <- str_c(colours, collapse = "|")
colour_match
[1] "red|orange|yellow|green|blue|purple"
  • Nun filtert man die Sätze heraus, die mindestens eine der Farben enthalten
has_colours <- str_subset(sentences, colour_match)
matches <- str_extract(has_colours, colour_match)
head(matches)
[1] "blue" "blue" "red"  "red"  "red"  "blue"
  • Die Funktion str_extract() extrahiert immer nur den ersten Treffer.

Durch Auswählen der Sätze, die mehr als eine Übereinstimmung haben.

more <- sentences[str_count(sentences, colour_match) > 1]
str_view(more, colour_match)
  • Mit der Funktion str_extract_all() erhält man alle Übereinstimmungen. Das Ergebnis ist eine Liste. Listen sind eine praktische Datenstruktur, auf die wir später nochmal zurückkommen. Wählt man zusätzlich das Argument simplify = TRUE, so erhält man eine Matrix, eine weitere Datenstruktur, die nicht nur für die lineare Algebra praktisch ist ;-).
x <- c("a", "b-c", "a.b.c")
str_extract_all(x, "[a-z]", simplify = TRUE)
     [,1] [,2] [,3]
[1,] "a"  ""   ""  
[2,] "b"  "c"  ""  
[3,] "a"  "b"  "c" 

11.8 Gruppierte Übereinstimmungen

  • Für die runden Klammern ( ) gibt es neben den bisher besprochenen noch eine weitere Anwendung, nämlich um Teile eines komplexen Ausdrucks zu extrahieren.

  • Im folgenden Beispiel ist jedes Wort, dass nach den Artikeln a, an oder the kommt gesucht.

noun <- "(a|an|the) ([^ ]+)"
has_noun <- sentences |>
            str_subset(noun) |>
            head(6)

has_noun |> str_extract(noun)
[1] "the smooth" "the sheet"  "the depth"  "a chicken" 
[5] "the parked" "the sun"   

Bemerkung: Es ist ein wenig kompliziert in einem regulären Ausdruck ein Wort zu definieren. Der obige Ausdruck ist eine einfache Näherung und für viele Anwendungen geeignet (hier allerdings nur sehr bedingt). * Mit der Funktion str_match() erhält man jede einzene Komponente in Form einer Matrix:

has_noun |> str_match(noun)
     [,1]         [,2]  [,3]     
[1,] "the smooth" "the" "smooth" 
[2,] "the sheet"  "the" "sheet"  
[3,] "the depth"  "the" "depth"  
[4,] "a chicken"  "a"   "chicken"
[5,] "the parked" "the" "parked" 
[6,] "the sun"    "the" "sun"    
  • Hat man die Daten in einem Tibble, so ist die Funktion tidyr::extract() einfacher zu benutzen. Diese macht das gleiche wie str_match(), allerding muss man für die Übereinstimmungen Namen angeben, da das Ergebnis als eigene Merkmale gespeichert werden.
tibble(sentence = sentences) |> 
  tidyr::extract(
    sentence, c("article", "noun"), "(a|the) ([^ ]+)", 
    remove = FALSE
  )
# A tibble: 720 × 3
   sentence                                    article noun   
   <chr>                                       <chr>   <chr>  
 1 The birch canoe slid on the smooth planks.  the     smooth 
 2 Glue the sheet to the dark blue background. the     sheet  
 3 It's easy to tell the depth of a well.      the     depth  
 4 These days a chicken leg is a rare dish.    a       chicken
 5 Rice is often served in round bowls.        <NA>    <NA>   
 6 The juice of lemons makes fine punch.       <NA>    <NA>   
 7 The box was thrown beside the parked truck. the     parked 
 8 The hogs were fed chopped corn and garbage. <NA>    <NA>   
 9 Four hours of steady work faced us.         <NA>    <NA>   
10 A large size in stockings is hard to sell.  <NA>    <NA>   
# ℹ 710 more rows
  • Analog zu str_match() benötigt man str_match_all(), wenn alle Übereinstimmungen pro String erkannt werden sollen.

11.8.1 Übereinstimmungen ersetzen

  • Die Funktion str_replace() und str_replace_all() erlauben es Übereinstimmungen durch andere Zeichenketten zu ersetzen.
  • Mit der Funktion str_replace_all() können auch mehrere (verschiedene) Ersetzungen gleichzeitig durchgeführt, wenn die ausgetausche Zeichenkette ein Vektor ist.
  • Man kann auch rückverweisen um Übereinstimmungen zu ersetzen oder zu tauschen (im Beispiel wird das zweite und das dritte Wort getauscht).
x <- c("Apfel", "Birne", "Clementine")
str_replace(x, "[aeiou]", "-")
[1] "Apf-l"      "B-rne"      "Cl-mentine"
str_replace_all(x, "[aeiou]", "-")
[1] "Apf-l"      "B-rn-"      "Cl-m-nt-n-"
x <- c("1 Auto", "2 Kinder", "3 CDs")
str_replace_all(x, c("1" = "ein", "2" = "zwei", "3" = "drei"))
[1] "ein Auto"    "zwei Kinder" "drei CDs"   
sentences |> 
   str_replace("([^ ]+) ([^ ]+) ([^ ]+)", "\\1 \\3 \\2") |> 
   head(4)
[1] "The canoe birch slid on the smooth planks." 
[2] "Glue sheet the to the dark blue background."
[3] "It's to easy tell the depth of a well."     
[4] "These a days chicken leg is a rare dish."   

11.9 Splitting

  • Die Funktion str_split() zerlegt eine Zeichenkette. Die Argumente sind
  • string= : die zu zerlegende Zeichenkette,
  • pattern=: das Muster bei dem die Zeichenkette getrennt wird. Dieses wird komplett entnommen, ist also in der getrennten Ausgabeliste nicht mehr enthalten,
  • n=Inf: Anzahl der zurückgegebenen Stücke, und
  • simplify=FALSE : Rückgabe ist eine Liste, falls TRUE wird eine Zeichenketten-Matrix zurückgegeben.
sentences |> head(3) |> str_split(" ")
[[1]]
[1] "The"     "birch"   "canoe"   "slid"    "on"      "the"    
[7] "smooth"  "planks."

[[2]]
[1] "Glue"        "the"         "sheet"       "to"         
[5] "the"         "dark"        "blue"        "background."

[[3]]
[1] "It's"  "easy"  "to"    "tell"  "the"   "depth" "of"    "a"    
[9] "well."
str_split(c("a|b|c|d"), "\\|")
[[1]]
[1] "a" "b" "c" "d"

11.10 Splitting und Muster finden

# als Matrix
sentences |> head(3) |> str_split(" ", simplify = TRUE)
     [,1]   [,2]    [,3]    [,4]   [,5]  [,6]    [,7]    
[1,] "The"  "birch" "canoe" "slid" "on"  "the"   "smooth"
[2,] "Glue" "the"   "sheet" "to"   "the" "dark"  "blue"  
[3,] "It's" "easy"  "to"    "tell" "the" "depth" "of"    
     [,8]          [,9]   
[1,] "planks."     ""     
[2,] "background." ""     
[3,] "a"           "well."
# nur je 2 Vektoren
sentences |> head(3) |> str_split(" ", n = 2, simplify = TRUE)
     [,1]   [,2]                                    
[1,] "The"  "birch canoe slid on the smooth planks."
[2,] "Glue" "the sheet to the dark blue background."
[3,] "It's" "easy to tell the depth of a well."     
  • Mit den Funktionen str_locate() und str_locate_all() gibt den Start-und Enpunkt eines Musters. Dies ist genau dann sinnvoll, wenn keine der anderen Funktionen exakt das macht was man braucht. (\implies ?str_locate)
  • Mit str_locate() können passende Muster gefunden werden, mit str_sub() extrahiert oder geändert werden.