Регулярні вирази дають змогу надзвичайно гнучко зіставляти рядки тексту (англ. string) зі шаблоном за допомогою предметно-орієнтованої мови (DSL), створеної саме для цього.
Багато можливостей регулярних виразів вбудовано в базовий R, і приклади цього можна побачити в старішому коді.
Для нового коду наполегливо радимо використовувати бібліотеку stringr.
Як зазначено в концепції рядків, stringr має добру документацію, яку зазвичай пишуть автори пакета.
Цей силабус з R розрахований на те, що читач уже знайомий з базовим синтаксисом регулярних виразів. Ми зосередимося виключно на можливостях, притаманних саме R.
Нижче наведено кілька ресурсів, щоб освіжити знання про регулярні вирази.
Багато з функцій, уже розглянутих у концепції Strings, типово призначені для пошуку за регулярними виразами.
Ми навмисно зробили концепцію рядків відносно простою, зіставляючи лише рядкові літерали, але натомість можна використати будь-який шаблон регулярного виразу.
Формально регулярний вираз у R створюють викликом regex().
Для зручності функції stringr сприймають будь-який рядок-шаблон як регулярний вираз.
У прикладі нижче перший str_replace_all() є синтаксичним цукром для другого варіанта:
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"
Шпаргалка - корисний довідник: усі функції з аргументом pattern (надруковані червоним) приймають регулярний вираз, а на сторінці 2 підсумовано синтаксис регулярних виразів.
Зауважмо, що функція regex() усе ще може бути важливою, якщо потрібно перевизначити типові значення, як-от чутливість до регістру чи локаль.
# default is case sensitive
"A Mixed Assortment" |>
str_replace_all(regex("[aeiou]", ignore_case = TRUE), "*")
#> [1] "* M*x*d *ss*rtm*nt"
Багато кодів регулярних виразів містять зворотний слеш, як-от \s вище.
У звичайних рядках R символ \ потрібно екранувати, подвоюючи його, звідси "\\s".
Зворотні слеші можуть розмножуватися й заплутувати код, тож у складних випадках може допомогти використання необроблених рядків.
Типово синтаксис для визначення необробленого рядка такий: r"( )", але можна використовувати й інші роздільники.
Оскільки круглі дужки ( ) і квадратні дужки [ ] обидва поширені в синтаксисі регулярних виразів (для груп захоплення й груп опцій), фігурні дужки { } можуть бути вдалим вибором.
input |> str_replace_all(r"{\s\w}", "__")
#> [1] "Some__nput__tring"
Ось кілька запитань і функції, які допоможуть на них відповісти.
У деяких прикладах використовується fruit, вектор із 80 назв фруктів, що входить до stringr для практики.
Чи містить рядок тексту збіг?
str_detect(), або str_starts() і str_ends(), щоб не використовувати символи привʼязки ^ чи $.
# 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
Повернути лише рядки тексту зі збігом?
Скористайтеся str_subset(), щоб отримати рядки, і str_which(), щоб отримати індекси.
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
Скільки збігів у кожному рядку?
str_count()
# count vowels
fruit |> str_count("[aeiou]") |> head(10)
#> [1] 2 3 4 3 3 2 2 3 5 3
Де міститься збіг?
str_view() - для інтерактивної роботи, str_locate() або 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
Часто нам хочеться знати, що саме збіглося. Для цього в регулярний вираз додають групи захоплення в дужках.
У межах одного й того самого регулярного виразу можна посилатися на захоплення за допомогою \1, \2 тощо.
Наприклад, пошук повторюваних пар літер:
fruit |> str_subset("(..)\\1")
[1] "banana" "coconut" "cucumber" "jujube" "papaya" "salal berry"
За схожим синтаксисом групи захоплення можна використовувати в рядках заміни:
# double to triple
fruit |>
str_subset("(..)\\1") |>
str_replace("(..)\\1", "\\1\\1\\1")
[1] "bananana" "cococonut" "cucucumber" "jujujube" "papapaya" "salalal berry"
Щоб зібрати збіги для подальшої обробки, скористайтеся функцією str_match(), якщо потрібен лише перший збіг, або str_match_all(), якщо потрібні всі.
Групи захоплення зазвичай дають складні вихідні дані в будь-якій мові.
Підхід R (який до того ж векторизовано, що додає складності) - згенерувати матрицю з str_match(): один рядок на кожен вхідний рядок, стовпець 1 містить повний збіг, а решта стовпців - різні групи захоплення.
Іменовані групи можуть бути перевагою, адже вони стають назвами стовпців у матриці результатів.
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"
Ми поки що мало говорили про матриці в цьому силабусі, хоча є документ про концепцію, до якого можна звернутися. Матриці погано вписуються в екосистему Tidyverse, хоч і є частиною базового R уже давно.
Коротко кажучи: розглядайте матриці як вектори з двома індексами, у порядку [rows, cols].
Залиште будь-який індекс порожнім, щоб отримати весь рядок чи весь стовпець.
Коли ми отримуємо окремий рядок r за допомогою m[r,] або окремий стовпець c за допомогою m[, c], повернене значення типово зводиться до вектора.
> 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"
Неминуче str_match_all() дає ще складніше структуровані вихідні дані:
[[1]], як описано в концепції списків.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"
Звісно, збіг може й не знайтися.
Тоді результатом буде спеціальне значення NA.
Тож варто перевіряти це, як описано в концепції ніщо.
# failed match
recipe |> str_match("(not here)")
[,1] [,2]
[1,] NA NA
R for Data science зауважує:
str_match()повертає матрицю, тож працювати з нею не надто зручно.
Із приміткою:
Головно тому, що в цій книзі ми взагалі не розглядаємо матриці!
Один з авторів книги також написав бібліотеку stringr, тож він неявно критикує власну роботу.
Цікаво буде подивитися, як реалізація цієї функції змінюватиметься в майбутніх випусках.