vec <- c("a", "d", "c")
grepl("[abc]", vec)[1] TRUE FALSE TRUE
grep("[abc]", vec)[1] 1 3
Есть старая шутка, ее приписывают программисту Джейми Завински: если у вас есть проблема, и вы собираетесь ее решать при помощи регулярных выражений, то у вас две проблемы. Регулярные выражения – это формальный язык, который используется для того, чтобы находить, извлекать и заменять части текста.
Регулярные выражения (regex, regexp) объединяют буквальные символы (литералы) и метасимволы (символы-джокеры, англ. wildcard characters).
Для поиска используется строка-образец (англ. pattern, по-русски её часто называют “шаблоном”, “маской”), которая задает правило поиска. Строка замены также может содержать в себе специальные символы.
Отличный путеводитель по миру регулярных выражений можно найти здесь.
В базовом R за работу со строками отвечают, среди прочего, такие функции, как grep() и grepl(). При этом grepl() возвращает TRUE, если шаблон найден в соответствующей символьной строке, а grep() возвращает вектор индексов символьных строк, содержащих паттерн.
Обеим функциям необходим аргумент pattern и аргумент x, где pattern - регулярное выражение, по которому производится поиск, а аргумент x - вектор символов, по которым следует искать совпадения.
Функция gsub() позволяет производить замену и требует также аргумента replacement.
Буквальные символы – это то, что вы ожидаете увидеть (или не увидеть – для управляющих и пробельных символов); можно сказать, что это символы, которые ничего не “имеют в виду”. Их можно объединять в классы при помощи квадратных скобок, например, так: [abc].
vec <- c("a", "d", "c")
grepl("[abc]", vec)[1] TRUE FALSE TRUE
grep("[abc]", vec)[1] 1 3
Для некоторых классов есть специальные обозначения.
| Класс | Эквивалент | Значение |
|---|---|---|
| [:upper:] | [A-Z] | Символы верхнего регистра |
| [:lower:] | [a-z] | Символы нижнего регистра |
| [:alpha:] | [[:upper:][:lower:]] | Буквы |
| [:digit:] | [0-9], т. е. \d | Цифры |
| [:alnum:] | [[:alpha:][:digit:]] | Буквы и цифры |
| [:word:] | [[:alnum:]_], т. е. \w | Символы, образующие «слово» |
| [:punct:] | [-!“#$%&’()*+,./:;<=>?@[\]_`{|}~] | Знаки пунктуации |
| [:blank:] | [\s\t] | Пробел и табуляция |
| [:space:] | [[:blank:]\v\r\n\f], т. е. \s | Пробельные символы |
| [:cntrl:] | Управляющие символы (перевод строки, табуляция и т.п.) | |
| [:graph:] | Печатные символы | |
| [:print:] | Печатные символы с пробелом |
Эти классы тоже можно задавать в качестве паттерна.
vec <- c("жираф", "верблюд1", "0зебра")
gsub( "[[:digit:]]", "", vec)[1] "жираф" "верблюд" "зебра"
В качестве классов можно рассматривать и следующие обозначения:
| Представление | Эквивалент | Значение |
|---|---|---|
| \d | [0-9] | Цифра |
| \D | [^\\d] | Любой символ, кроме цифры |
| \w | [A-Za-zА-Яа-я0-9_] | Символы, образующие «слово» (буквы, цифры и символ подчёркивания) |
| \W | [^\\w] | Символы, не образующие «слово» |
| \s | [ \t\v\r\n\f] | Пробельный символ |
| \S | [^\\s] | Непробельный символ |
gsub( "\\d", "", vec) # вторая косая черта "экранирует" первую[1] "жираф" "верблюд" "зебра"
Внутри квадратных скобор знак ^ означает отрицание:
gsub( "[^[:digit:]]", "", vec) [1] "" "1" "0"
Якоря позволяют искать последовательности символов в начале или в конце строки. Знак ^ (вне квадратных скобок!) означает начало строки, а знак $ – конец. Мнемоническое правило: First you get the power (^) and then you get the money ($).
vec <- c("The spring is a lovely time",
"Fall is a time of peace")
grepl("time$", vec)[1] TRUE FALSE
Все метасимволы представлены в таблице ниже.
| Описание | Символ |
|---|---|
| открывающая квадратная скобка | [ |
| закрывающая квадратная скобка | ] |
| обратная косая черта | \ |
| карет | ^ |
| знак доллара | $ |
| точка | . |
| вертикальная черта | | |
| знак вопроса | ? |
| астериск | * |
| плюс | + |
| открывающая фигурная скобка | { |
| закрывающая фигурная скобка | } |
| открывающая круглая скобка | ( |
| закрывающая круглая скобка | ) |
Квадратные скобки используются для создания классов, карет и знак доллара – это якоря, но карет внутри квадратных скобок может также быть отрицанием. Точка – это любой знак.
vec <- c("жираф", "верблюд1", "0зебра")
grep(".б", vec) [1] 2 3
Если необходимо найти буквальную точку, буквальный знак вопроса и т.п., то используется экранирование: перед знаком ставится косая черта. Но так как сама косая черта – это метасимвол, но нужно две косые черты, первая из которых экранирует вторую.
vec <- c("жираф?", "верблюд.", "зебра")
grep("\\?", vec) [1] 1
grepl("\\.", vec)[1] FALSE TRUE FALSE
Квантификатор после символа, символьного класса или группы определяет, сколько раз предшествующее выражение может встречаться. Квантификатор может относиться более чем к одному символу в регулярном выражении, только если это символьный класс или группа.
| Представление | Число повторений | Эквивалент |
|---|---|---|
| ? | Ноль или одно | {0,1} |
| * | Ноль или более | {0,} |
| + | Одно или более | {1,} |
Пример:
vec <- c("color", "colour", "colouur")
grepl("ou?r", vec) # ноль или одно [1] TRUE TRUE FALSE
grepl("ou+r", vec) # одно или больше[1] FALSE TRUE TRUE
grepl("ou*r", vec) # ноль или больше[1] TRUE TRUE TRUE
Точное число повторений (интервал) можно задать в фигурных скобках:
| Представление | Число повторений |
|---|---|
| {n} | Ровно n раз |
| {m,n} | От m до n включительно |
| {m,} | Не менее m |
| {,n} | Не более n |
vec <- c("color", "colour", "colouur", "colouuuur")
grepl("ou{1}r", vec)[1] FALSE TRUE FALSE FALSE
grepl("ou{1,2}r", vec)[1] FALSE TRUE TRUE FALSE
grepl("ou{,2}r", vec) # это включает и ноль![1] TRUE TRUE TRUE FALSE
Часто используется последовательность .* для обозначения любого количества любых символов между двумя частями регулярного выражения.
В регулярных выражениях квантификаторам соответствует максимально длинная строка из возможных (квантификаторы являются жадными, англ. greedy). Это может оказаться значительной проблемой. Например, часто ожидают, что выражение <.*> найдёт в тексте теги HTML. Однако если в тексте есть более одного HTML-тега, то этому выражению соответствует целиком строка, содержащая множество тегов.
vec <- c("<p><b>Википедия</b> — свободная энциклопедия, в которой <i>каждый</i> может изменить или дополнить любую статью.</p>")
gsub("<.*>", "", vec) # все исчезло![1] ""
Чтобы этого избежать, надо поставить после квантификатора знак вопроса. Это сделает его ленивым.
| regex | значение |
|---|---|
| ?? | 0 или 1, лучше 0 |
| *? | 0 или больше, как можно меньше |
| +? | 1 или больше, как можно меньше |
| {n,m}? | от n до m, как можно меньше |
Пример:
gsub("<.*?>", "", vec) # все получилось![1] "Википедия — свободная энциклопедия, в которой каждый может изменить или дополнить любую статью."
Пакет stringr является частью tidyverse1:
library(tidyverse)── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr 1.1.4 ✔ readr 2.1.5
✔ forcats 1.0.0 ✔ stringr 1.5.1
✔ ggplot2 3.5.2 ✔ tibble 3.2.1
✔ lubridate 1.9.4 ✔ tidyr 1.3.1
✔ purrr 1.0.4
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag() masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
Это очень удобный инструмент для работы со строками. Вот так можно узнать длину строки или объединить ее с другими строками:
vec <- c("жираф", "верблюд")
str_length(vec)[1] 5 7
str_c("красивый_", vec)[1] "красивый_жираф" "красивый_верблюд"
Элементы вектора можно объединить в одну строку:
str_c(vec, collapse = ", ") # теперь у них общие кавычки[1] "жираф, верблюд"
С помощью str_sub() и str_sub_all() можно выбрать часть строки2.
vec <- c("жираф", "верблюд")
str_sub(vec, 1, 3)[1] "жир" "вер"
str_sub(vec, 1, -2)[1] "жира" "верблю"
Функции ниже меняют начертание с прописного на строчное или наоборот:
VEC <- str_to_upper(vec)
VEC[1] "ЖИРАФ" "ВЕРБЛЮД"
str_to_lower(VEC)[1] "жираф" "верблюд"
str_to_title(vec)[1] "Жираф" "Верблюд"
Одна из полезнейших функций в этом пакете – str_view(); она помогает увидеть, что поймало регулярное выражение – до того, как вы внесете какие-то изменения в строку.
str_view(c("abc", "a.c", "bef"), "a\\.c")[2] │ <a.c>
Например, с помощью этой функции можно убедиться, что вертикальная черта выступает как логический оператор “или”:
str_view(c("grey", "gray"), "gr(e|a)y")[1] │ <grey>
[2] │ <gray>
Аналогом grepl() в stringr является функция str_detect()
library(rcorpora)
data("fruit")
head(fruit)[1] "apple" "apricot" "avocado" "banana" "bell pepper"
[6] "bilberry"
str_detect(head(fruit), "[aeiou]$")[1] TRUE FALSE TRUE TRUE FALSE FALSE
# какая доля слов заканчивается на гласный?
mean(str_detect(fruit, "[aeiou]$"))[1] 0.35
# сколько всего слов заканчивается на гласный?
sum(str_detect(fruit, "[aeiou]$"))[1] 28
Отрицание можно задать двумя способами:
data("words")
no_vowels1 <- !str_detect(words, "[aeiou]") # слова без гласных
no_vowels2 <- str_detect(words, "^[^aeiou]+$") # слова без гласных
sum(no_vowels1 != no_vowels2)[1] 0
Логический вектор можно использовать для индексирования:
words[!str_detect(words, "[aeiou]")][1] "by" "dry" "fly" "mrs" "try" "why"
Эту функцию можно применять вместе с функцией filter() из пакета dplyr:
library(dplyr)
gods <- corpora(which = "mythology/greek_gods")
df <- tibble(god = as.character(gods$greek_gods),
i = seq_along(god)
)
df |>
filter(str_detect(god, "s$"))Вариацией этой функции является str_count():
str_count(as.character(gods$greek_gods), "[Aa]") [1] 1 1 1 1 2 0 0 1 1 1 0 1 0 0 1 2 1 0 0 0 0 1 2 1 0 2 3 2 1 0 0
Эту функцию удобно использовать вместе с mutate() из dplyr:
df |>
mutate(
vowels = str_count(god, "[AEIOYaeiou]"),
consonants = str_count(god, "[^AEIOYaeiou]")
)Функция str_extract() извлекает совпадения3.
Сначала зададим паттерн для поиска.
colours <- c(" red", "orange", "yellow", "green", "blue", "purple")
colour_match <- str_c(colours, collapse = "|")
colour_match[1] " red|orange|yellow|green|blue|purple"
И применим к предложениями. Используем str_extract_all(), т.к. str_extract() возвращает только первое вхождение.
has_colour <- str_subset(sentences, colour_match)
matches <- str_extract_all(has_colour, colour_match)
head(unlist(matches))[1] "blue" "blue" "blue" "yellow" "green" " red"
Круглые скобки используются для группировки. Например, мы можем задать шаблон для поиска существительного или прилагательного с артиклем.
noun <- "(a|the) ([^ ]+)" # как минимум один непробельный символ после пробела
has_noun <- sentences |>
str_subset(noun) |>
head(10)
has_noun [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] "The box was thrown beside the parked truck."
[6] "The boy was there when the sun rose."
[7] "The source of the huge river is the clear spring."
[8] "Kick the ball straight and follow through."
[9] "Help the woman get back to her feet."
[10] "A pot of tea helps to pass the evening."
Дальше можно воспользоваться уже известной функцией str_extract() или применить str_match. Результат будет немного отличаться: вторая функция вернет матрицу, в которой хранится не только сочетание слов, но и каждый компонент отдельно.
has_noun |>
str_extract(noun) [1] "the smooth" "the sheet" "the depth" "a chicken" "the parked"
[6] "the sun" "the huge" "the ball" "the woman" "a helps"
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"
[7,] "the huge" "the" "huge"
[8,] "the ball" "the" "ball"
[9,] "the woman" "the" "woman"
[10,] "a helps" "a" "helps"
Функция tidyr::extract() работает похожим образом, но требует дать имена для каждого элемента группы. Этим удобно пользоваться, если ваши данные хранятся в виде тиббла.
tibble(sentence = sentences) |>
tidyr::extract(
sentence, c("article", "noun"), "(a|the) ([^ ]+)",
remove = FALSE
)Функции str_replace() и str_replace_all() позволяют заменять совпадения на новые символы.
x <- c("apple", "pear", "banana")
str_replace(x, "[aeiou]", "-")[1] "-pple" "p-ar" "b-nana"
str_replace_all(x, "[aeiou]", "-")[1] "-ppl-" "p--r" "b-n-n-"
Этим можно воспользоваться, если вы хотите, например, удалить из текста все греческие символы. Для стандартного греческого алфавита хватит [Α-Ωα-ω], но для древнегреческого этого, например, не хватит. Попробуем на отрывке из письма Цицерона Аттику, которое содержит греческий текст.
cicero <- "nihil hāc sōlitūdine iūcundius, nisi paulum interpellāsset Amyntae fīlius. ὢ ἀπεραντολογίας ἀηδοῦς! "
str_replace_all(cicero, "[Α-Ωα-ω]", "")[1] "nihil hāc sōlitūdine iūcundius, nisi paulum interpellāsset Amyntae fīlius. ὢ ἀί ἀῦ! "
ὢ ἀί ἀῦ! Не все у нас получилось гладко. Попробуем иначе:
str_replace_all(cicero, "[\u0370-\u03FF]", "")[1] "nihil hāc sōlitūdine iūcundius, nisi paulum interpellāsset Amyntae fīlius. ὢ ἀ ἀῦ! "
Удалилась (буквально была заменена на пустое место) та диакритика, которая есть в новогреческом (ί). Но остались еще буквы со сложной диакритикой, которой современные греки не пользуются.
no_greek <- str_replace_all(cicero, "[[\u0370-\u03FF][\U1F00-\U1FFF]]", "")
no_greek[1] "nihil hāc sōlitūdine iūcundius, nisi paulum interpellāsset Amyntae fīlius. ! "
! Мы молодцы. Избавились от этого непонятного греческого.
На самом деле, конечно, str_replace хорош тем, что он позволяет производить осмысленные замены. Например, мы можем в оставшемся латинском текст заменить гласные с макроном (черточка, означающая долготу) на обычные гласные.
str_replace_all(no_greek, c("ā" = "a", "ū" = "u", "ī" = "i", "ō" = "o"))[1] "nihil hac solitudine iucundius, nisi paulum interpellasset Amyntae filius. ! "
Красота. О более сложных заменах с перемещением групп можно посмотреть видео здесь и здесь. Это помогает даже в таком скорбном деле, как переоформление библиографии.
Функция str_split() помогает разбить текст на предложения, слова или просто на бессмысленные наборы символов. Это важный этап подготовки текста для анализа, и проводится он нередко именно с применением регулярных выражений.
sentences |>
head(2) |>
str_split(" ")[[1]]
[1] "The" "birch" "canoe" "slid" "on" "the" "smooth"
[8] "planks."
[[2]]
[1] "Glue" "the" "sheet" "to" "the"
[6] "dark" "blue" "background."
Но можно обойтись и без регулярных выражений.
x <- "This is a sentence. This is another sentence."
str_view_all(x, boundary("word"))[1] │ <This> <is> <a> <sentence>. <This> <is> <another> <sentence>.
str_view_all(x, boundary("sentence"))[1] │ <This is a sentence. ><This is another sentence.>
Очень удобно, но убедитесь, что в вашем языке границы слов и предложения выглядят как у людей. С древнегреческим эта штука не справится (как делить на предложения греческие и латинские тексты, я рассказывала здесь):
apology <- c("νῦν δ' ἐπειδὴ ἀνθρώπω ἐστόν, τίνα αὐτοῖν ἐν νῷ ἔχεις ἐπιστάτην λαβεῖν; τίς τῆς τοιαύτης ἀρετῆς, τῆς ἀνθρωπίνης τε καὶ πολιτικῆς, ἐπιστήμων ἐστίν; οἶμαι γάρ σε ἐσκέφθαι διὰ τὴν τῶν ὑέων κτῆσιν. ἔστιν τις,” ἔφην ἐγώ, “ἢ οὔ;” “Πάνυ γε,” ἦ δ' ὅς. “Τίς,” ἦν δ' ἐγώ, “καὶ ποδαπός, καὶ πόσου διδάσκει;")
str_view_all(apology, boundary("sentence"))[1] │ <νῦν δ' ἐπειδὴ ἀνθρώπω ἐστόν, τίνα αὐτοῖν ἐν νῷ ἔχεις ἐπιστάτην λαβεῖν; τίς τῆς τοιαύτης ἀρετῆς, τῆς ἀνθρωπίνης τε καὶ πολιτικῆς, ἐπιστήμων ἐστίν; οἶμαι γάρ σε ἐσκέφθαι διὰ τὴν τῶν ὑέων κτῆσιν. ἔστιν τις,” ἔφην ἐγώ, “ἢ οὔ;” “Πάνυ γε,” ἦ δ' ὅς. ><“Τίς,” ἦν δ' ἐγώ, “καὶ ποδαπός, καὶ πόσου διδάσκει;>
Полный крах 💩