Reguläre Ausdrücke sind eine äußerst vielseitige Möglichkeit, bei Strings einen Musterabgleich durchzuführen. Dazu verwendet man eine domänenspezifische Sprache (DSL), die genau für diesen Zweck entworfen wurde.
Wie viele andere Programmiersprachen versucht R nicht, eine eigene Regex-Bibliothek zu implementieren. Stattdessen nutzt es die Engine für reguläre Ausdrücke von ICU. ICU ähnelt der beliebten Bibliothek PCRE2, die von den meisten neueren Sprachen verwendet wird, allerdings werden einige Randfälle anders behandelt.
Viele Regex-Funktionen sind in Base R enthalten, und in älterem Code findest du vielleicht Beispiele dafür.
Für neuen Code wird dringend empfohlen, die Bibliothek stringr zu verwenden.
Wie im Strings-Konzept angemerkt, hat stringr eine gute Dokumentation, die in der Regel von den Autorinnen und Autoren des Pakets geschrieben wird.
Dieser R-Lehrplan geht davon aus, dass du bereits mit der grundlegenden Regex-Syntax vertraut bist. Wir konzentrieren uns ausschließlich auf R-spezifische Funktionen.
Unten findest du einige Ressourcen, mit denen du deine Kenntnisse über reguläre Ausdrücke auffrischen kannst.
Viele der Funktionen, die bereits im Strings-Konzept besprochen wurden, sind standardmäßig für Regex-Suchen ausgelegt.
Wir haben das Strings-Konzept bewusst einfach gehalten und nur mit String-Literalen abgeglichen, aber stattdessen kann jedes beliebige Regex-Muster verwendet werden.
Formell wird ein Regex in R durch den Aufruf von regex() erzeugt.
Der Einfachheit halber behandeln stringr-Funktionen jeden Muster-String als Regex.
Im folgenden Beispiel ist der erste str_replace_all()-Aufruf nur syntaktischer Zucker für die zweite Variante:
input <- "Some input string"
# "\\s" matches any whitespace character
input |> str_replace_all("\\s", "_")
[1] "Some_input_string"
input |> str_replace_all(regex("\\s"), "_")
[1] "Some_input_string"
Das Cheatsheet ist ein nützlicher Leitfaden: Alle Funktionen mit einem pattern-Argument (rot gedruckt) akzeptieren ein Regex, und Seite 2 fasst die Regex-Syntax zusammen.
Beachte, dass die Funktion regex() weiterhin wichtig sein kann, wenn du die Standardeinstellungen überschreiben musst, etwa die Groß- und Kleinschreibung oder das Locale.
# default is case sensitive
"A Mixed Assortment" |>
str_replace_all(regex("[aeiou]", ignore_case = TRUE), "*")
#> [1] "* M*x*d *ss*rtm*nt"
Viele Regex-Muster enthalten einen Backslash, wie das oben verwendete \s.
In normalen R-Strings muss \ durch Verdopplung escaped werden, daher "\\s".
Backslashes können sich häufen und unübersichtlich werden, deshalb kann es helfen, in komplexen Fällen Raw-Strings zu verwenden.
Standardmäßig lautet die Syntax zum Definieren eines Raw-Strings r"( )", aber auch andere Begrenzungszeichen sind erlaubt.
Da runde Klammern ( ) und eckige Klammern [ ] in der Regex-Syntax beide häufig vorkommen (für Erfassungs- und Optionsgruppen), sind geschweifte Klammern { } vielleicht eine gute Wahl.
input |> str_replace_all(r"{\s\w}", "__")
#> [1] "Some__nput__tring"
Hier sind einige Fragen und Funktionen, die bei der Beantwortung helfen können.
Einige Beispiele verwenden fruit, einen Vektor mit 80 Fruchtnamen, der zum Üben in stringr enthalten ist.
Enthält ein String eine Übereinstimmung?
str_detect() oder str_starts(), str_ends(), um die Anker ^ oder $ zu vermeiden.
# berries begining with b
fruit |> str_detect("^b.*berry") |> head(15)
#> [1] FALSE FALSE FALSE FALSE FALSE TRUE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE FALSE
# alternative syntax
fruit |> str_starts("b.*berry") |> head(15)
#> [1] FALSE FALSE FALSE FALSE FALSE TRUE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE FALSE
Nur Strings zurückgeben, die eine Übereinstimmung enthalten?
Verwende str_subset(), um die Strings zu erhalten, und str_which(), um die Indizes zu erhalten.
fruit |> str_subset("a...e")
[1] "apple" "blackberry" "mandarine" "nectarine" "pineapple" "pomegranate" "raspberry" "salal berry"
> fruit |> str_which("a...e")
[1] 1 7 48 51 62 64 70 73
Wie viele Übereinstimmungen gibt es in jedem String?
str_count()
# count vowels
fruit |> str_count("[aeiou]") |> head(10)
#> [1] 2 3 4 3 3 2 2 3 5 3
Wo befindet sich die Übereinstimmung?
str_view() für die interaktive Nutzung, str_locate() oder allgemeiner str_locate_all().
# str_view only shows matching elements, with match in < > delimiters
fruit |> str_view("a...e")
[1] │ <apple>
[7] │ bl<ackbe>rry
[48] │ mand<arine>
[51] │ nect<arine>
[62] │ pine<apple>
[64] │ pomegr<anate>
[70] │ r<aspbe>rry
[73] │ sal<al be>rry
# str_locate returns all elements
fruit |> str_locate("a...e") |> head(10)
start end
[1,] 1 5
[2,] NA NA
[3,] NA NA
[4,] NA NA
[5,] NA NA
[6,] NA NA
[7,] 3 7
[8,] NA NA
[9,] NA NA
[10,] NA NA
Häufig möchten wir wissen, was übereinstimmt. Das erreicht man, indem man Erfassungsgruppen in runden Klammern in das Regex aufnimmt.
Es ist möglich, innerhalb desselben Regex auf Erfassungsgruppen zurückzuverweisen, mit \1, \2 usw.
Zum Beispiel das Finden wiederholter Buchstabenpaare:
fruit |> str_subset("(..)\\1")
[1] "banana" "coconut" "cucumber" "jujube" "papaya" "salal berry"
Mit ähnlicher Syntax können Erfassungsgruppen in Ersetzungs-Strings verwendet werden:
# double to triple
fruit |>
str_subset("(..)\\1") |>
str_replace("(..)\\1", "\\1\\1\\1")
[1] "bananana" "cococonut" "cucucumber" "jujujube" "papapaya" "salalal berry"
Sammle Übereinstimmungen für die spätere Verarbeitung mit der Funktion str_match() für nur die erste Übereinstimmung oder str_match_all() für alle Übereinstimmungen.
Erfassungsgruppen erzeugen in jeder Sprache tendenziell komplizierte Ausgaben.
Der R-Ansatz (der als zusätzliche Herausforderung auch vektorisiert ist) besteht darin, aus str_match() eine Matrix zu erzeugen, mit einer Zeile pro Eingabe-String, wobei Spalte 1 den vollständigen übereinstimmenden String enthält und die übrigen Spalten die verschiedenen Erfassungsgruppen.
Die Verwendung benannter Gruppen kann von Vorteil sein, da sie zu Spaltennamen in der Ergebnis-Matrix werden.
recipe <- "dissolve 25g sugar in 200ml water"
recipe |> str_match("(\\d+g) .* (\\d+ml)")
[,1] [,2] [,3]
[1,] "25g sugar in 200ml" "25g" "200ml"
# use named capture groups
recipe |> str_match("(?<wt>\\d+g) .* (?<vol>\\d+ml)")
wt vol
[1,] "25g sugar in 200ml" "25g" "200ml"
Wir haben in diesem Lehrplan nicht viel über Matrizen gesagt, aber es gibt ein Konzeptdokument, auf das du zugreifen kannst. Matrizen passen nicht gut zum Tidyverse-Ökosystem, obwohl sie seit langem Teil von Base R sind.
Kurz zusammengefasst: Behandle Matrizen wie Vektoren mit zwei Indizes, in der Reihenfolge [rows, cols].
Lass einen der beiden Indizes leer, um die gesamte Zeile bzw. Spalte zu erhalten.
Wenn du mit m[r,] eine einzelne Zeile r oder mit m[, c] eine einzelne Spalte c abrufst, wird der zurückgegebene Wert standardmäßig zu einem Vektor abgeflacht.
> amounts <- recipe |> str_match("(?<wt>\\d+g) .* (?<vol>\\d+ml)")
# get all of row 1, as a named vector
> amt1 <- amounts[1,]
> amt1
wt vol
"25g sugar in 200ml" "25g" "200ml"
# get a vector element
amt1["wt"] |> unname()
[1] "25g"
Unweigerlich erzeugt str_match_all() eine Ausgabe mit noch mehr Struktur:
[[1]], wie im Listen-Konzept beschrieben.match().mix <- "Mix 200g of sugar, 180g of flour and 150g of butter"
# raw output
> str_match_all(mix, "(\\d+)g")
[[1]]
[,1] [,2]
[1,] "200g" "200"
[2,] "180g" "180"
[3,] "150g" "150"
# we just want the first list element
> str_match_all(mix, "(\\d+)g")[[1]]
[,1] [,2]
[1,] "200g" "200"
[2,] "180g" "180"
[3,] "150g" "150"
# captures are in column 2, so get that as a vector
> str_match_all(mix, "(\\d+)g")[[1]][,2]
[1] "200" "180" "150"
Natürlich können Übereinstimmungen fehlschlagen.
Das Ergebnis ist dann der spezielle Wert NA.
Sei darauf vorbereitet, dies zu testen, wie im Nichts-Konzept beschrieben.
# failed match
recipe |> str_match("(not here)")
[,1] [,2]
[1,] NA NA
R for Data Science merkt an:
str_match()gibt eine Matrix zurück, daher ist sie nicht besonders einfach zu handhaben.
Mit der Fußnote:
Hauptsächlich, weil wir in diesem Buch nie Matrizen besprechen!
Einer der Autoren des Buchs hat auch die Bibliothek stringr geschrieben und kritisiert damit implizit sein eigenes Werk.
Es wird interessant sein zu sehen, wie sich die Implementierung dieser Funktion in zukünftigen Versionen weiterentwickelt.