Треки
/
R
R
/
Салабус
/
Регулярні вирази
Ре

Регулярні вирази у R

1 вправа

Про концепцію Регулярні вирази

Регулярні вирази дають змогу надзвичайно гнучко зіставляти рядки тексту (англ. string) зі шаблоном за допомогою предметно-орієнтованої мови (DSL), створеної саме для цього.

Advanced

Як і багато інших мов програмування, R не намагається реалізувати власну бібліотеку регулярних виразів. Натомість він використовує рушій регулярних виразів ICU. ICU подібний до популярної бібліотеки PCRE2, яку застосовують більшість новіших мов, хоча деякі крайні випадки обробляються інакше.

Багато можливостей регулярних виразів вбудовано в базовий R, і приклади цього можна побачити в старішому коді.

Для нового коду наполегливо радимо використовувати бібліотеку stringr.

Як зазначено в концепції рядків, stringr має добру документацію, яку зазвичай пишуть автори пакета.

Note

Цей силабус з 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  
Note

R for Data science зауважує:

str_match() повертає матрицю, тож працювати з нею не надто зручно.

Із приміткою:

Головно тому, що в цій книзі ми взагалі не розглядаємо матриці!

Один з авторів книги також написав бібліотеку stringr, тож він неявно критикує власну роботу.

Цікаво буде подивитися, як реалізація цієї функції змінюватиметься в майбутніх випусках.

Редагувати через GitHub Посилання відкривається в новому вікні або вкладці

Вивчити концепцію Регулярні вирази