Тема 8 Регулярные выражения
Есть старая шутка, ее приписывают программисту Джейми Завински: если у вас есть проблема, и вы собираетесь ее решать при помощи регулярных выражений, то у вас две проблемы. Регулярные выражения – это формальный язык, который используется для того, чтобы находить, извлекать и заменять части текста.
Регулярные выражения (regex, regexp) объединяют буквальные символы (литералы) и метасимволы (символы-джокеры, англ. wildcard characters).
Для поиска используется строка-образец (англ. pattern, по-русски её часто называют “шаблоном”, “маской”), которая задает правило поиска. Строка замены также может содержать в себе специальные символы.
Отличный путеводитель по миру регулярных выражений можно найти здесь.
8.1 Regex в базовом R
В базовом R за работу со строками отвечают, среди прочего, такие функции, как grep()
и grepl()
. При этом grepl()
возвращает TRUE, если шаблон найден в соответствующей символьной строке, а grep()
возвращает вектор индексов символьных строк, содержащих паттерн.
Обеим функциям необходим аргумент pattern
и аргумент x
, где pattern
- регулярное выражение, по которому производится поиск, а аргумент x
- вектор символов, по которым следует искать совпадения.
Функция gsub()
позволяет производить замену и требует также аргумента replacement
.
8.2 Литералы и классы
Буквальные символы – это то, что вы ожидаете увидеть (или не увидеть – для управляющих и пробельных символов); можно сказать, что это символы, которые ничего не “имеют в виду”. Их можно объединять в классы при помощи квадратных скобок, например, так: [abc]
.
## [1] TRUE FALSE TRUE
## [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:] | Печатные символы с пробелом |
Эти классы тоже можно задавать в качестве паттерна.
## [1] "жираф" "верблюд" "зебра"
В пакете stringr
есть небольшой датасет words
. Найдите все слова с последовательностью символов wh. Сколько слов содержат два гласных после w?
В качестве классов можно рассматривать и следующие обозначения:
Представление | Эквивалент | Значение |
---|---|---|
\d | [0-9] | Цифра |
\D | [^\\d] | Любой символ, кроме цифры |
\w | [A-Za-zА-Яа-я0-9_] | Символы, образующие «слово» (буквы, цифры и символ подчёркивания) |
\W | [^\\w] | Символы, не образующие «слово» |
\s | [ \t\v\r\n\f] | Пробельный символ |
\S | [^\\s] | Непробельный символ |
## [1] "жираф" "верблюд" "зебра"
Внутри квадратных скобор знак ^
означает отрицание:
## [1] "" "1" "0"
Найдите все слова в words
, в которых за w следует согласный. Замените всю пунктуацию в строке “tomorrow?and-tomorrow_and!tomorrow” на пробелы.
8.3 Якоря
Якоря позволяют искать последовательности символов в начале или в конце строки. Знак ^
(вне квадратных скобок!) означает начало строки, а знак $
– конец. Мнемоническое правило: First you get the power (^) and then you get the money ($).
## [1] TRUE FALSE
Найдите все слова в words
, которые заканчиваются на x. Найдите все слова, которые начинаются на b или на g.
8.4 Метасимволы
Все метасимволы представлены в таблице ниже.
Описание | Символ |
---|---|
открывающая квадратная скобка | [ |
закрывающая квадратная скобка | ] |
обратная косая черта | \ |
карет | ^ |
знак доллара | $ |
точка | . |
вертикальная черта | | |
знак вопроса | ? |
астериск | * |
плюс | + |
открывающая фигурная скобка | { |
закрывающая фигурная скобка | } |
открывающая круглая скобка | ( |
закрывающая круглая скобка | ) |
Квадратные скобки используются для создания классов, карет и знак доллара – это якоря, но карет внутри квадратных скобок может также быть отрицанием. Точка – это любой знак.
## [1] 2 3
Найдите все слова в words
, в которых есть любые два символа между b и k.
8.5 Экранирование
Если необходимо найти буквальную точку, буквальный знак вопроса и т.п., то используется экранирование: перед знаком ставится косая черта. Но так как сама косая черта – это метасимвол, но нужно две косые черты, первая из которых экранирует вторую.
## [1] 1
## [1] FALSE TRUE FALSE
Узнайте, все ли предложения в sentences (входит в stringr) кончаются на точку.
8.6 Квантификация
Квантификатор после символа, символьного класса или группы определяет, сколько раз предшествующее выражение может встречаться. Квантификатор может относиться более чем к одному символу в регулярном выражении, только если это символьный класс или группа.
Представление | Число повторений | Эквивалент |
---|---|---|
? | Ноль или одно | {0,1} |
* | Ноль или более | {0,} |
+ | Одно или более | {1,} |
Пример:
## [1] TRUE TRUE FALSE
## [1] FALSE TRUE TRUE
## [1] TRUE TRUE TRUE
Точное число повторений (интервал) можно задать в фигурных скобках:
Представление | Число повторений |
---|---|
{n} | Ровно n раз |
{m,n} | От m до n включительно |
{m,} | Не менее m |
{,n} | Не более n |
## [1] FALSE TRUE FALSE FALSE
## [1] FALSE TRUE TRUE FALSE
## [1] TRUE TRUE TRUE FALSE
Часто используется последовательность .*
для обозначения любого количества любых символов между двумя частями регулярного выражения.
Узнайте, в каких предложениях в sentences за пробелом следует ровно три согласных.
8.7 Жадная и ленивая квантификация
В регулярных выражениях квантификаторам соответствует максимально длинная строка из возможных (квантификаторы являются жадными, англ. greedy). Это может оказаться значительной проблемой. Например, часто ожидают, что выражение <.*>
найдёт в тексте теги HTML. Однако если в тексте есть более одного HTML-тега, то этому выражению соответствует целиком строка, содержащая множество тегов.
vec <- c("<p><b>Википедия</b> — свободная энциклопедия, в которой <i>каждый</i> может изменить или дополнить любую статью.</p>")
gsub("<.*>", "", vec) # все исчезло!
## [1] ""
Чтобы этого избежать, надо поставить после квантификатора знак вопроса. Это сделает его ленивым.
regex | значение |
---|---|
?? | 0 или 1, лучше 0 |
*? | 0 или больше, как можно меньше |
+? | 1 или больше, как можно меньше |
{n,m}? | от n до m, как можно меньше |
Пример:
## [1] "Википедия — свободная энциклопедия, в которой каждый может изменить или дополнить любую статью."
Дана строка “tomorrow (and) tomorrow (and) tomorrow”. Необходимо удалить первые скобки с их содержанием.
8.8 Regex в stringr: основы
Пакет stringr
не является частью tidyverse
, хотя и разделяет его принципы36. Его надо загружать отдельно:
Это очень удобный инструмент для работы со строками. Вот так можно узнать длину строки или объединить ее с другими строками:
## [1] 5 7
## [1] "красивый_жираф" "красивый_верблюд"
Элементы вектора можно объединить в одну строку:
## [1] "жираф, верблюд"
С помощью str_sub()
и str_sub_all()
можно выбрать часть строки37.
## [1] "жир" "вер"
## [1] "жира" "верблю"
Функции ниже меняют начертание с прописного на строчное или наоборот:
## [1] "ЖИРАФ" "ВЕРБЛЮД"
## [1] "жираф" "верблюд"
## [1] "Жираф" "Верблюд"
Одна из полезнейших функций в этом пакете – str_view()
; она помогает увидеть, что поймало регулярное выражение – до того, как вы внесете какие-то изменения в строку.
## [2] │ <a.c>
Например, с помощью этой функции можно убедиться, что вертикальная черта выступает как логический оператор “или”:
## [1] │ <grey>
## [2] │ <gray>
Создайте тиббл с двумя столбцами: letters и numbers (1:26). Преобразуйте, чтобы в третьем столбце появился результат соединения первых двух через подчеркивание, например a_1. Отфильтруйте, чтобы остались только ряды, где есть цифра 3 или буква x.
8.9 str_detect() и str_count()
Аналогом grepl()
в stringr
является функция str_detect()
## [1] "apple" "apricot" "avocado" "banana"
## [5] "bell pepper" "bilberry"
## [1] TRUE FALSE TRUE TRUE FALSE FALSE
## [1] 0.35
## [1] 28
Отрицание можно задать двумя способами:
data("words")
no_vowels1 <- !str_detect(words, "[aeiou]") # слова без гласных
no_vowels2 <- str_detect(words, "^[^aeiou]+$") # слова без гласных
sum(no_vowels1 != no_vowels2)
## [1] 0
Логический вектор можно использовать для индексирования:
## [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$"))
## # A tibble: 18 × 2
## god i
## <chr> <int>
## 1 Ares 3
## 2 Artemis 4
## 3 Dionysus 7
## 4 Hades 8
## 5 Hephaestus 9
## 6 Hermes 11
## 7 Zeus 14
## 8 Chaos 17
## 9 Chronos 18
## 10 Erebus 19
## 11 Eros 20
## 12 Hypnos 21
## 13 Uranus 22
## 14 Phanes 24
## 15 Pontus 25
## 16 Tartarus 26
## 17 Thanatos 28
## 18 Nemesis 31
Вариацией этой функции является str_count()
:
## [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
## [29] 1 0 0
Эту функцию удобно использовать вместе с mutate()
из dplyr
:
df %>%
mutate(
vowels = str_count(god, "[AEIOYaeiou]"),
consonants = str_count(god, "[^AEIOYaeiou]")
)
## # A tibble: 31 × 4
## god i vowels consonants
## <chr> <int> <int> <int>
## 1 Aphrodite 1 4 5
## 2 Apollo 2 3 3
## 3 Ares 3 2 2
## 4 Artemis 4 3 4
## 5 Athena 5 3 3
## 6 Demeter 6 3 4
## 7 Dionysus 7 3 5
## 8 Hades 8 2 3
## 9 Hephaestus 9 4 6
## 10 Hera 10 2 2
## # ℹ 21 more rows
Преобразуйте sentences из пакета stringr
в тиббл; в новом столбце сохраните количество пробелов в каждом предложении.
8.10 str_extract(), str_subset() и str_match()
Функция str_extract()
извлекает совпадения38.
Сначала зададим паттерн для поиска.
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
. Результат будет немного отличаться: вторая функция вернет матрицу, в которой хранится не только сочетание слов, но и каждый компонент отдельно.
## [1] "the smooth" "the sheet" "the depth" "a chicken"
## [5] "the parked" "the sun" "the huge" "the ball"
## [9] "the woman" "a helps"
## [,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
)
## # A tibble: 720 × 3
## sentence article noun
## <chr> <chr> <chr>
## 1 The birch canoe slid on the smooth planks. the smoo…
## 2 Glue the sheet to the dark blue background. the sheet
## 3 It's easy to tell the depth of a well. the depth
## 4 These days a chicken leg is a rare dish. a chic…
## 5 Rice is often served in round bowls. <NA> <NA>
## 6 The juice of lemons makes fine punch. <NA> <NA>
## 7 The box was thrown beside the parked truck. the park…
## 8 The hogs were fed chopped corn and garbage. <NA> <NA>
## 9 Four hours of steady work faced us. <NA> <NA>
## 10 A large size in stockings is hard to sell. <NA> <NA>
## # ℹ 710 more rows
Найдите в sentences все предложения, где есть to, и выберите следующее за этим слово. Переведите в нижний регистр. Узнайте, сколько всего уникальных сочетаний.
8.11 str_replace
Функции str_replace()
и str_replace_all()
позволяют заменять совпадения на новые символы.
## [1] "-pple" "p-ar" "b-nana"
## [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. ὢ ἀί ἀῦ! "
ὢ ἀί ἀῦ! Не все у нас получилось гладко. Попробуем иначе:
## [1] "nihil hāc sōlitūdine iūcundius, nisi paulum interpellāsset Amyntae fīlius. ὢ ἀ ἀῦ! "
Удалилась (буквально была заменена на пустое место) та диакритика, которая есть в новогреческом (ί). Но остались еще буквы со сложной диакритикой, которой современные греки не пользуются.
## [1] "nihil hāc sōlitūdine iūcundius, nisi paulum interpellāsset Amyntae fīlius. ! "
! Мы молодцы. Избавились от этого непонятного греческого.
На самом деле, конечно, str_replace
хорош тем, что он позволяет производить осмысленные замены. Например, мы можем в оставшемся латинском текст заменить гласные с макроном (черточка, означающая долготу) на обычные гласные.
## [1] "nihil hac solitudine iucundius, nisi paulum interpellasset Amyntae filius. ! "
Красота. О более сложных заменах с перемещением групп можно посмотреть видео здесь и здесь. Это помогает даже в таком скорбном деле, как переоформление библиографии.
Дана библиографическая запись:
Ast, Friedrich. 1816. Platon’s Leben und Schriften. Leipzig, Weidmann.
Используя регулярные выражения, замените полное имя на инициал. Запятую перед инициалом удалите. Уберите название издательства. Год поставьте в круглые скобки.
8.12 str_split
Функция str_split()
помогает разбить текст на предложения, слова или просто на бессмысленные наборы символов. Это важный этап подготовки текста для анализа, и проводится он нередко именно с применением регулярных выражений.
## [[1]]
## [1] "The" "birch" "canoe" "slid" "on"
## [6] "the" "smooth" "planks."
##
## [[2]]
## [1] "Glue" "the" "sheet" "to"
## [5] "the" "dark" "blue" "background."
Но можно обойтись и без регулярных выражений.
## [1] │ <This> <is> <a> <sentence>. <This> <is> <another> <sentence>.
## [1] │ <This is a sentence. ><This is another sentence.>
Очень удобно, но убедитесь, что в вашем языке границы слов и предложения выглядят как у людей. С древнегреческим эта штука не справится (как делить на предложения греческие и латинские тексты, я рассказывала здесь):
apology <- c("νῦν δ' ἐπειδὴ ἀνθρώπω ἐστόν, τίνα αὐτοῖν ἐν νῷ ἔχεις ἐπιστάτην λαβεῖν; τίς τῆς τοιαύτης ἀρετῆς, τῆς ἀνθρωπίνης τε καὶ πολιτικῆς, ἐπιστήμων ἐστίν; οἶμαι γάρ σε ἐσκέφθαι διὰ τὴν τῶν ὑέων κτῆσιν. ἔστιν τις,” ἔφην ἐγώ, “ἢ οὔ;” “Πάνυ γε,” ἦ δ' ὅς. “Τίς,” ἦν δ' ἐγώ, “καὶ ποδαπός, καὶ πόσου διδάσκει;")
str_view_all(apology, boundary("sentence"))
## [1] │ <νῦν δ' ἐπειδὴ ἀνθρώπω ἐστόν, τίνα αὐτοῖν ἐν νῷ ἔχεις ἐπιστάτην λαβεῖν; τίς τῆς τοιαύτης ἀρετῆς, τῆς ἀνθρωπίνης τε καὶ πολιτικῆς, ἐπιστήμων ἐστίν; οἶμαι γάρ σε ἐσκέφθαι διὰ τὴν τῶν ὑέων κτῆσιν. ἔστιν τις,” ἔφην ἐγώ, “ἢ οὔ;” “Πάνυ γε,” ἦ δ' ὅς. ><“Τίς,” ἦν δ' ἐγώ, “καὶ ποδαπός, καὶ πόσου διδάσκει;>
Полный крах 💩