<- c("a", "d", "c")
vec 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]
.
<- c("a", "d", "c")
vec 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:] | Печатные символы с пробелом |
Эти классы тоже можно задавать в качестве паттерна.
<- c("жираф", "верблюд1", "0зебра")
vec 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 ($).
<- c("The spring is a lovely time",
vec "Fall is a time of peace")
grepl("time$", vec)
[1] TRUE FALSE
Все метасимволы представлены в таблице ниже.
Описание | Символ |
---|---|
открывающая квадратная скобка | [ |
закрывающая квадратная скобка | ] |
обратная косая черта | \ |
карет | ^ |
знак доллара | $ |
точка | . |
вертикальная черта | | |
знак вопроса | ? |
астериск | * |
плюс | + |
открывающая фигурная скобка | { |
закрывающая фигурная скобка | } |
открывающая круглая скобка | ( |
закрывающая круглая скобка | ) |
Квадратные скобки используются для создания классов, карет и знак доллара – это якоря, но карет внутри квадратных скобок может также быть отрицанием. Точка – это любой знак.
<- c("жираф", "верблюд1", "0зебра")
vec grep(".б", vec)
[1] 2 3
Если необходимо найти буквальную точку, буквальный знак вопроса и т.п., то используется экранирование: перед знаком ставится косая черта. Но так как сама косая черта – это метасимвол, но нужно две косые черты, первая из которых экранирует вторую.
<- c("жираф?", "верблюд.", "зебра")
vec grep("\\?", vec)
[1] 1
grepl("\\.", vec)
[1] FALSE TRUE FALSE
Квантификатор после символа, символьного класса или группы определяет, сколько раз предшествующее выражение может встречаться. Квантификатор может относиться более чем к одному символу в регулярном выражении, только если это символьный класс или группа.
Представление | Число повторений | Эквивалент |
---|---|---|
? | Ноль или одно | {0,1} |
* | Ноль или более | {0,} |
+ | Одно или более | {1,} |
Пример:
<- c("color", "colour", "colouur")
vec 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 |
<- c("color", "colour", "colouur", "colouuuur")
vec 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-тега, то этому выражению соответствует целиком строка, содержащая множество тегов.
<- c("<p><b>Википедия</b> — свободная энциклопедия, в которой <i>каждый</i> может изменить или дополнить любую статью.</p>")
vec gsub("<.*>", "", vec) # все исчезло!
[1] ""
Чтобы этого избежать, надо поставить после квантификатора знак вопроса. Это сделает его ленивым.
regex | значение |
---|---|
?? | 0 или 1, лучше 0 |
*? | 0 или больше, как можно меньше |
+? | 1 или больше, как можно меньше |
{n,m}? | от n до m, как можно меньше |
Пример:
gsub("<.*?>", "", vec) # все получилось!
[1] "Википедия — свободная энциклопедия, в которой каждый может изменить или дополнить любую статью."
Пакет stringr
не является частью tidyverse
, хотя и разделяет его принципы1. Его надо загружать отдельно:
library(stringr)
Это очень удобный инструмент для работы со строками. Вот так можно узнать длину строки или объединить ее с другими строками:
<- c("жираф", "верблюд")
vec str_length(vec)
[1] 5 7
str_c("красивый_", vec)
[1] "красивый_жираф" "красивый_верблюд"
Элементы вектора можно объединить в одну строку:
str_c(vec, collapse = ", ") # теперь у них общие кавычки
[1] "жираф, верблюд"
С помощью str_sub()
и str_sub_all()
можно выбрать часть строки2.
<- c("жираф", "верблюд")
vec str_sub(vec, 1, 3)
[1] "жир" "вер"
str_sub(vec, 1, -2)
[1] "жира" "верблю"
Функции ниже меняют начертание с прописного на строчное или наоборот:
<- str_to_upper(vec)
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")
<- !str_detect(words, "[aeiou]") # слова без гласных
no_vowels1
<- str_detect(words, "^[^aeiou]+$") # слова без гласных
no_vowels2
sum(no_vowels1 != no_vowels2)
[1] 0
Логический вектор можно использовать для индексирования:
!str_detect(words, "[aeiou]")] words[
[1] "by" "dry" "fly" "mrs" "try" "why"
Эту функцию можно применять вместе с функцией filter() из пакета dplyr:
library(dplyr)
<- corpora(which = "mythology/greek_gods")
gods
<- tibble(god = as.character(gods$greek_gods),
df 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.
Сначала зададим паттерн для поиска.
<- c(" red", "orange", "yellow", "green", "blue", "purple")
colours <- str_c(colours, collapse = "|")
colour_match colour_match
[1] " red|orange|yellow|green|blue|purple"
И применим к предложениями. Используем str_extract_all()
, т.к. str_extract()
возвращает только первое вхождение.
<- str_subset(sentences, colour_match)
has_colour <- str_extract_all(has_colour, colour_match)
matches head(unlist(matches))
[1] "blue" "blue" "blue" "yellow" "green" " red"
Круглые скобки используются для группировки. Например, мы можем задать шаблон для поиска существительного или прилагательного с артиклем.
<- "(a|the) ([^ ]+)" # как минимум один непробельный символ после пробела
noun
<- sentences |>
has_noun 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) |>
::extract(
tidyrc("article", "noun"), "(a|the) ([^ ]+)",
sentence, remove = FALSE
)
Функции str_replace()
и str_replace_all()
позволяют заменять совпадения на новые символы.
<- c("apple", "pear", "banana")
x str_replace(x, "[aeiou]", "-")
[1] "-pple" "p-ar" "b-nana"
str_replace_all(x, "[aeiou]", "-")
[1] "-ppl-" "p--r" "b-n-n-"
Этим можно воспользоваться, если вы хотите, например, удалить из текста все греческие символы. Для стандартного греческого алфавита хватит [Α-Ωα-ω]
, но для древнегреческого этого, например, не хватит. Попробуем на отрывке из письма Цицерона Аттику, которое содержит греческий текст.
<- "nihil hāc sōlitūdine iūcundius, nisi paulum interpellāsset Amyntae fīlius. ὢ ἀπεραντολογίας ἀηδοῦς! "
cicero
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. ὢ ἀ ἀῦ! "
Удалилась (буквально была заменена на пустое место) та диакритика, которая есть в новогреческом (ί). Но остались еще буквы со сложной диакритикой, которой современные греки не пользуются.
<- str_replace_all(cicero, "[[\u0370-\u03FF][\U1F00-\U1FFF]]", "")
no_greek 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."
Но можно обойтись и без регулярных выражений.
<- "This is a sentence. This is another sentence."
x 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.>
Очень удобно, но убедитесь, что в вашем языке границы слов и предложения выглядят как у людей. С древнегреческим эта штука не справится (как делить на предложения греческие и латинские тексты, я рассказывала здесь):
<- c("νῦν δ' ἐπειδὴ ἀνθρώπω ἐστόν, τίνα αὐτοῖν ἐν νῷ ἔχεις ἐπιστάτην λαβεῖν; τίς τῆς τοιαύτης ἀρετῆς, τῆς ἀνθρωπίνης τε καὶ πολιτικῆς, ἐπιστήμων ἐστίν; οἶμαι γάρ σε ἐσκέφθαι διὰ τὴν τῶν ὑέων κτῆσιν. ἔστιν τις,” ἔφην ἐγώ, “ἢ οὔ;” “Πάνυ γε,” ἦ δ' ὅς. “Τίς,” ἦν δ' ἐγώ, “καὶ ποδαπός, καὶ πόσου διδάσκει;")
apology
str_view_all(apology, boundary("sentence"))
[1] │ <νῦν δ' ἐπειδὴ ἀνθρώπω ἐστόν, τίνα αὐτοῖν ἐν νῷ ἔχεις ἐπιστάτην λαβεῖν; τίς τῆς τοιαύτης ἀρετῆς, τῆς ἀνθρωπίνης τε καὶ πολιτικῆς, ἐπιστήμων ἐστίν; οἶμαι γάρ σε ἐσκέφθαι διὰ τὴν τῶν ὑέων κτῆσιν. ἔστιν τις,” ἔφην ἐγώ, “ἢ οὔ;” “Πάνυ γε,” ἦ δ' ὅς. ><“Τίς,” ἦν δ' ἐγώ, “καὶ ποδαπός, καὶ πόσου διδάσκει;>
Полный крах 💩