Работа со строками и текстами

Операции со строками

Смена регистра, tolower() и toupper()

Одна из самых простых операций со строковыми значениями — это смена регистра, перевод заглавных букв в строчные и наоборот. Наиболее частые способы задачи, в которых используется смена регистра, — уникализировать значения (чтобы Слово и слово были идентичными объектами), привести к требуемому стандарту (например, идентификатор мобильного устройства использует только заглавные буквы, а в логах почему-то оказались строчные) и так далее.

tolower('Слово')
## [1] "слово"
toupper('6d92078a-8246-4Bb4-AE5B-76104861e7DC')
## [1] "6D92078A-8246-4BB4-AE5B-76104861E7DC"

Слияние строк

paste() и paste0()

Для слияния строк обычно используются две базовые функции paste() и paste0(). Основное различие между функциями в используемом разделителе: paste0() используется тогда, когда требуется слить строки без какого-либо разделителя, paste() — когда надо задать какой-то разделитель, по умолчанию используется пробел. Если в функции paste() использовать аргумент sep = '', то результат будет идентичен результату функции paste0(). В обе функции можно передавать объекты разных базовых типов, все они будут приведены к строковому типу (значение NA будет прочитано как строковое 'NA', TRUE аналогично):

paste0('пример №', 1, ': демонстрируем работу функции paste0()')
## [1] "пример №1: демонстрируем работу функции paste0()"
paste('пример №', 2, ': демонстрируем работу функции paste()', sep = '_')
## [1] "пример №_2_: демонстрируем работу функции paste()"

Функция paste() векторизирована и воспринимает все аргументы (кроме sep и collapse) как отдельные векторы, которые и обрабатывает поэлементно. Это значит, что если векторы будут разной длины, то короткий вектор будет обрабатываться циклично, и это приведет к удвоениям значений, по длине самого длинного вектора. В примере ниже одинарные векторы 'пример', ':' и 'строка' повторяются трижды, по длине вектора c(3, 4, 5). Вектор c(1:2) состоит из двух элементов, в то время как в результате должно быть использовано три элемента из него, поэтому первый элемент берётся повторно.

paste('пример', c(3, 4, 5), ':', 'строка', c(1:2), sep = '_')
## [1] "пример_3_:_строка_1" "пример_4_:_строка_2" "пример_5_:_строка_1"

Если в функцию paste() передать только один вектор, то результат будет идентичен исходному вектору, так как нет другого вектора, с элементами которого можно было бы проводить слияние. В тех случаях, когда есть необходимость слить все элементы одного вектора в одно строковое значение, используется аргумент collapse:

paste(letters[1:5], sep = ', ')
## [1] "a" "b" "c" "d" "e"
paste(letters[1:5], collapse = ', ')
## [1] "a, b, c, d, e"

Чаще всего разница между аргументами sep и collapse проявляется при работе с таблицами. Так, sep используется в тех случаях, когда необходимо создать колонку или вектор значений из значений других колонок, то есть путем поэлементного слияния векторов (колонок). collapse используется тогда, когда надо собрать из значений колонки одинарный вектор, нередко одновременно с группировкой (одинарный вектор для каждого уникального значения группирующей переменной):

# создаём датасет и колонку
df <- data.frame(var1 = rep(c('id1', 'id2'), each = 3),
                   var2 = sample(letters, 6),
                   var3 = sample(6))
df$var4 <- paste(df$var2, df$var3, sep = '_')

# получаем одно значение из всех значений колонки var2
paste(df$var2, collapse = '_')
## [1] "c_r_j_g_q_u"
# получаем одно значение из всех значений колонки var2 с группировкой по колонке var1
aggregate(var2 ~ var1, data = df, FUN = paste, collapse = '_')
##   var1  var2
## 1  id1 c_r_j
## 2  id2 g_q_u

glue()

glue — функция одноименного пакета, аналогична по назначению функции paste(). Функция glue() — это “синтаксический сахар” и имеет только одно отличие, которое существенно облегчает работу в ряде задач: требуемое значение можно передать сразу в строку с помощью фигурных скобок, а не дополнительным вектором. В этом glue() схожа с sprintf() (в целом подобные конструкции можно найти во многих языках программирования):

my_value <- 999

# выражение с использованием paste()
paste0('сейчас используем значение: ', my_value, ', в функциях paste()/paste0() это сложно для восприятия')
## [1] "сейчас используем значение: 999, в функциях paste()/paste0() это сложно для восприятия"
# выражение с использованием glue()
glue::glue('сейчас используем значение: {my_value}, в функции glue() это выглядит проще')
## сейчас используем значение: 999, в функции glue() это выглядит проще

Функция glue() чаще всего используется в тех ситуациях, когда в какой-то блок кода необходимо передавать большой ряд изменяемых значений (как правило, это глобальные переменные, объявленные ранее). Например, с помощью glue() можно писать запросы к базам данных по одному и тому же периоду или использовать один запрос с разными токенами API-доступа. Например, так бы выглядели запросы к PostgreSQL за последние 5 дней от текущей даты:

# создаём объекты с требуемыми датами 
t_start <- Sys.Date() - 5
t_end <- Sys.Date()

# формируем SQL-запрос 
query <- glue::glue("SELECT * FROM my_table WHERE ts BETWEEN '{t_start}' AND '{t_end}';")
query
## SELECT * FROM my_table WHERE ts BETWEEN '2022-01-30' AND '2022-02-04';

Разделение и выделение строк

Разделение строк, strsplit()

Наряду со слиянием строк, нередка обратная ситуация, когда надо разделить строку на одну или несколько частей. Чаще всего для таких задач используется функция strsplit() или её аналоги в дополнительных пакетах, например, data.table::tstrsplit(). strsplit() в качестве аргументов принимает объект-строку, символ, по которому надо разделить строку, и указание, воспринимать символ-разделитель как есть или как regexp-паттерн (в том случае, если разделитель имеет дополнительное значение как регулярное выражение).

my_string <- 'ab_cd.efg.hi_jk'

# разбиваем строку по символу '_'
strsplit(my_string, '_')
## [[1]]
## [1] "ab"        "cd.efg.hi" "jk"
# используем как разделитель точку, как regexp-обозначение любого символа
strsplit(my_string, '.')
## [[1]]
##  [1] "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""
# используем как разделитель точку as is
strsplit(my_string, '.', fixed = TRUE)
## [[1]]
## [1] "ab_cd" "efg"   "hi_jk"

В результате strsplit() возвращает список, в котором первым и единственным элементом списка служит вектор требуемых значений. Следовательно, для того чтобы получить какой-то определенный элемент списка, необходимо прямо его указать с помощью операторов [ и [[:

strsplit(my_string, '_')[[1]][1:2]
## [1] "ab"        "cd.efg.hi"

Выделение строк, substr() и strtrim()

Функции substr() и strtrim() используются в ситуациях, когда необходимо каким-то образом сократить длину строки или же извлечь определенную её часть. Например, подобные ситуации возникают в тех случаях, когда критично количество символов в названии файла или таблицы. Так, по базовым настройкам баз данных Oracle, название таблиц не может быть длиннее 30 символов. Соответственно, при экспорте данных в базу необходимо контролировать длину строк:

# создаём длинное название таблицы
new_db_table_name <- '1_very_long_name_for_dummy_table_into_my_database'

# определяем, какой длины полученное название
nchar(new_db_table_name)
## [1] 49
# создаём новое название, оставляем только 30 символов
strtrim(new_db_table_name, 30)
## [1] "1_very_long_name_for_dummy_tab"
# создаём новое название, оставляем только 3-32-й символы
substr(new_db_table_name, start = 3, stop = 32)
## [1] "very_long_name_for_dummy_table"

Изменение части строки, gsub() и sub()

Выделение строк с помощью substr() и strtrim() обладает одним большим недостатком: необходимо знать количество символов в строке или же задавать какой-то интервал желаемых символов. В большинстве случаев оказывается удобнее ориентироваться на содержание строки, а не просто на количество и порядковые номера символов в ней. В других случаях бывает необходимо заменить какие-нибудь символы в строке: например, убрать знаки препинания из предложения или каким-то другим образом почистить строковые значения. Все эти задачи можно при большом желании и упорстве решить с помощью substr(), однако будет намного проще и эффективнее воспользоваться функцией gsub(). Функция gsub() ищет в заданной строке требуемый паттерн (фиксированный набор символов или же регулярное выражение) и заменяет его на другой произвольный набор символов, нередко на пустую строку ('', отсутствие символов). Функция gsub() заменяет все найденные по соответствию паттерна части строки, функция sub() — только первое вхождение.

# предложение, в котором есть опечатки
my_string <- 'Senten8ce with err0ors'

# удаляем числа с помощью regexp-паттерна и заменяем слово with
my_string <- gsub(pattern = '[0-9]', replacement = '', x = my_string)
my_string
## [1] "Sentence with errors"
# заменяем слово with, указываем доппараметры
my_string <- gsub(pattern = 'with', replacement = 'without', x = my_string, fixed = TRUE)
my_string
## [1] "Sentence without errors"

Извлечение части строки, regmatches()

Функция gsub() — очень мощный инструмент при работе со строковыми данными, особенно при препроцессинге данных. Тем не менее, некоторые задачи с её помощью выполнить достаточно сложно, в первую очередь из-за того, что функция предполагает поиск и замену какого-то набора символов, удовлетворяющих паттерну. То есть в ситуациях, когда нужно, наоборот, удалить всё, кроме определенного набора символов, требуется достаточно много усилий, чтобы решить задачу с помощью gsub(). Средствами регулярных выражений сделать операцию логического отрицания можно лишь в относительно простых случаях, а вот задачи типа вычленения e-mail-адреса из строки сделать уже достаточно сложно. В подобных ситуациях лучше использовать пару функций — regexpr() и regmatches(). Первая функция определяет номер символа, с которого начинается требуемая комбинация символов, и длину определённого набора. Функция regmatches() используется для извлечения строки на основе результата regexpr() (при необходимости можно воспользоваться и substr()). Попробуем разными методами извлечь e-mail из строки:

# задаём e-mail
my_email <- 'my email address is fenrir999@asgard.com'

# извлечение с помощью gsub() и regexp-групп
gsub('(.*\\s)([A-Za-z0-9]+\\@[A-Za-z0-9]+\\.[A-Za-z]+)', '\\2', my_email)
## [1] "fenrir999@asgard.com"
# извлечение с помощью regexpr() и regmatches()
regmatches(my_email, regexpr("([A-Za-z0-9]+@[A-Za-z0-9]+\\.[A-Za-z]+)", my_email))
## [1] "fenrir999@asgard.com"
# извлечение с помощью regexpr() и substr()
tmp <- regexpr("([A-Za-z0-9]+@[A-Za-z0-9]+\\.[A-Za-z]+)", my_email)
substr(my_email, start = tmp[1], stop = tmp[1] + attr(tmp, "match.length"))
## [1] "fenrir999@asgard.com"

Регулярные выражения

Паттерны

В примерах к функциям gsub() и regexpr() мы упоминали и применяли особые конструкции, которые используются при обработке строковых данных — регулярные выражения. Регулярные выражения (англ. Regular Expressions, regexps, в русском языке нередко встречается транслитерация “регэкспы”) — это специальный язык для описания шаблонов строк. Регулярные выражения используются для поиска определённых строк, проверки их на соответствие какому-либо шаблону и другой подобной работы. В функциях gsub() и regexpr() мы как раз искали необходимые нам части строк с помощью паттернов (шаблонов), написанных в виде регулярных выражений.

Реализация регэкспов в разных языках программирования может различаться, в R используется расширенная версия регулярных выражений (ERE, стандарт POSIX 1003.2) с некоторыми собственными дополнениями, а также Perl-совместимые регулярные выражения (PCRE 8.36). Согласно стандарту POSIX 1003.2, длина регулярных выражений не может превышать 256 байтов. Впрочем, как показывает практика, с таким ограничением мало кто сталкивается в своей работе.

У регулярных выражений есть, к сожалению, одна достаточно неприятная особенность: если выражение написано неправильно (в частности, при использовании метасимволов), то в результате в зависимости от контекста использования будет либо пропуск нужных элементов, либо просто отсутствие какого-либо эффекта. Поэтому есть смысл упрощать работу с регулярными выражениями, особенно если нет достаточного опыта их использования. Например, изменять строки в несколько этапов или же просто тестировать регулярные выражения небольшими блоками.

Символы

В большинстве своём символы в регулярных выражениях обозначают сами себя, за некоторыми исключениями. Каждый символ в паттерне задаёт соответствие только одному символу в строке. Как следствие, в тех случаях, когда необходимо указать какой-то символ из определенного набора или обозначить, что этот символ повторяется некоторое количество раз подряд, необходимо использовать символьные классы или квантификаторы соответственно.

В R ряд функций для поиска и замены строк по паттерну имеют две версии: первая обрабатывает только первое найденное значение, соответствующее паттерну, вторая версия — все найденные по паттерну значения. Например, функция sub() находит и заменяет первое вхождение искомого символа, gsub — все вхождения:

# задаём строку
my_string <- 'mystring998989'

# удаляем символ 9, первое вхождение
sub('9', "", my_string)
## [1] "mystring98989"
# удаляем все символы 9 с помощью функции gsub() 
gsub('9', "", my_string)
## [1] "mystring88"

Если в паттерне указать несколько символов, то поиск по строке будет произведен именно по такому сочетанию символов. Например, удалим из строки (заменим на пустую строку) сочетание 89:

# удаляем сочетание символов 89, первое вхождение
sub('89', "", my_string)
## [1] "mystring9989"
# удаляем сочетание символов 89, все вхождения
gsub('89', "", my_string)
## [1] "mystring99"

Следует учитывать, что некоторые символы или сочетания символов могут быть проинтерпретированы как управляющие конструкции регулярных выражений (см. Метасимволы), и тогда потребуются дополнительные указания, как должны быть обработаны эти символы. Некоторые группы символов могут просто иметь дополнительные фиксированные значения. Так, для кодирования перехода на новую строку, табуляции и некоторых других непечатаемых символов используются определённые обозначения: \n, \r, \t, \v, \f. Соответственно, если в строке встретится какое-то из этих сочетаний, то строка будет прочитана и обработана не как строка, содержащая символы обратного слеша и буквы, а как строка со спецсимволом (в случае с табуляцией — \t):

string_with_tab <- 'metachara\tcters'
cat(string_with_tab)
## metachara    cters
# пробуем удалить из строки букву t
gsub('t', '_DELETED_', string_with_tab)
## [1] "me_DELETED_achara\tc_DELETED_ers"

Подобные символы, тем не менее, можно обрабатывать, как и все прочие:

gsub('\t', '_DELETED_', string_with_tab)
## [1] "metachara_DELETED_cters"

Классы символов

Задачи на поиск конкретного символа с помощью регулярных выражений встречаются, но обычно требуется большая гибкость поиска. Например, не конкретный символ или сочетание символов, а какой-либо символ из набора — знаки пунктуации или же цифры.

Cамостоятельно указать какой-то набор символов можно с помощью квадратных скобок (стоит помнить, что квадратные скобки как метасимволы регулярных выражений и квадратные скобки в R — это разные конструкции). При этом можно либо просто перечислить все символы набора, либо, если они принадлежат какому-то известному множеству (например, цифры), указать первый и последний элементы множества через знак дефиса (-). Также можно сочетать сокращённые формы записи наборов символов. Символы кириллического и латинского алфавитов — разные множества, точно так же, как строчные и прописные буквы.

Внутри классов поведение метасимволов может различаться в зависимости от места их указания в наборе символов класса. Например, ^ первым символом в наборе задаёт логическое отрицание не из этих символов, и чтобы избежать такого поведения, знак ^ надо поставить на любое место, кроме первого в цепочке. Знак ], если есть необходимость его включения в набор символов, наоборот, надо ставить первым в наборе, в противном случае он будет проинтерпретирован как завершение класса. Знак -, если указан не первым и не последним, интерпретируется как знак интервала в известном диапазоне символов (например, [0-9] означает от 0 до 9, а [09-] — символы 0, 9 и -).

# задаём строку
my_string <- 'D9586bNd879мрЯпп'

# укажем, что удалить надо любую из цифр
gsub('[0123456789]', '', my_string)
## [1] "DbNdмрЯпп"
gsub('[0-9]', '', my_string)
## [1] "DbNdмрЯпп"
# удаляем цифры, а также строчные буквы кириллицы и латиницы
gsub('[0-9a-zа-я]', '', my_string)
## [1] "DNЯ"

Символьные классы в POSIX

Стандарт POSIX 1003.2 поддерживает несколько определённых обозначений для часто используемых символьных классов:

  • [:alnum:] — все буквы и цифры, сочетание символьных классов [:alpha:] и [:digit:];

  • [:alpha:] — буквы алфавита в обоих регистрах, для прописных и строчных букв есть отдельные символьные классы — [:lower:] и [:upper:];

  • [:punct:] — знаки пунктуации, !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~;

  • [:digit:] — арабские цифры 0123456789;

  • [:xdigit:] — цифры в шестнадцатеричном формате — 0123456789ABCDEFabcdef;

  • [:graph:] — графические знаки, объединённый класс, состоящий из классов [:alnum:] и [:punct:];

  • [:print:] — печатаемые знаки, класс [:graph:], дополненный пробелом;

  • [:blank:] — непечатаемые символы (пробел, знак табуляции, в зависимости от локали — неразрывный пробел и возможные другие непечатаемые символы);

  • [:cntrl:] — управляющие символы (в таблицах символов ANSCII коды 000-031 и 121 — в десятичной, 000-037 и 177 — в восьмеричной, 000-07F в шестнадцатеричной системах счислений);

  • [:space:] — некоторые управляющие символы, которые используются для создания разрывов между символами — пробел, переход на новую строку, возврат каретки, перевод страницы и т. д., в зависимости от локали могут содержать дополнительные знаки.

При указании символьного класса с помощью его имени надо помнить, что обозначение класса также заключается в []. То есть вместо [:digit:] (обозначение класса) надо использовать [[:digit:]] (указание символьного класса в регулярном выражении):

gsub('[[:digit:]]', '', my_string)
## [1] "DbNdмрЯпп"

Дополнительные символьные классы

Помимо обозначений символьных классов, которые поддерживаются стандартом POSIX, также в R реализован ряд собственных символьных классов, которые, как правило, обозначают наиболее часто используемые комбинации:

  • \\d — цифры, аналогично [0-9];

  • \\D — нецифры, обратно \\d, аналогично [^0-9];

  • \\w — символы, которые используются в письме, аналогично [A-z0-9_];

  • \\W — обратный \\w набор, символы, которые не используются в письме;

  • \\S — все знаки, кроме пробела, аналогично [^[:space:]] или [^\\s].

# задаём строку
my_string <- 'D9586bNd879мрЯпп'

# удаляем все цифры с помощью класса [0-9]
gsub('[0-9]', '', my_string)
## [1] "DbNdмрЯпп"
# удаляем все цифры с помощью класса [:digit:]
gsub('[[:digit:]]', '', my_string)
## [1] "DbNdмрЯпп"
# удаляем все цифры с помощью класса \\d
gsub('\\d', '', my_string)
## [1] "DbNdмрЯпп"

Метасимволы

Ряд символов имеет дополнительное значение: эти символы используются не только сами по себе, но и как определенные конструкции языка регулярных выражений. В частности, это символ [, который используется для создания произвольного символьного класса. Полный список выглядит следующим образом:

  • . — подстановочный знак (wildcard), используется в тех случаях, когда необходимо указать, что на этом месте может быть любой знак;

  • \ — используется для экранирования метасимволов;

  • | — логический оператор ИЛИ;

  • ( и ) — используются для указания групп символов;

  • [ вместе с ] используются для указания символьных классов;

  • ^ — якорь, указывающий на начало строки, а также логический оператор отрицания, используемый в символьных классах;

  • $ — якорь, указывающий на конец строки;

  • *, + и ? — квантификаторы, указывающие, что предыдущий символ или группа символов могут или должны повториться некоторое количество раз;

  • { и } — используются как квантификатор, указывающий конкретное количество повторений предыдущего символа или группы символов;

  • < и > — используется в Perl-совместимых регулярных выражениях.

Для того, чтобы эти метасимволы воспринимались не как элементы языка регулярных выражений, а как есть, их необходимо экранировать двумя символами \\. Необходимость в двух символах \\ для экранирования возникает из-за того, что строковая запись регулярного выражения в R и собственно выражение на языке регулярных выражений несколько различаются. В частности, интерпретатор R также воспринимает обратный слеш как символ экранирования:

string_with_escapes <- c('\\a', '\\z', '\\')
writeLines(string_with_escapes)
## \a
## \z
## \

Таким образом, если мы хотим удалить из строки символы, которые могут быть проинтерпретированы как управляющие конструкции на языке регулярных выражений, мы должны либо экранировать эти символы двумя обратными слешами \\, либо в функции, использующей регулярное выражение, задавать аргумент fixed = TRUE:

# пробуем удалить символы .* без экранирования
gsub('.*', '_DELETED_', 'metachara.*cters')
## [1] "_DELETED_"
# пробуем удалить с экранированием каждого символа
gsub('\\.\\*', '_DELETED_', 'metachara.*cters')
## [1] "metachara_DELETED_cters"
# используем аргумент fixed = TRUE, чтобы воспринимать паттерн не как регулярное выражение
gsub('.*', '_DELETED_', 'metachara.*cters', fixed = TRUE)
## [1] "metachara_DELETED_cters"

Логические операции

В регулярных выражениях логические операции представлены в достаточно ограниченном виде, можно даже сказать, крайне бедно. Наиболее очевидная из существующих операций — это ИЛИ, когда надо задать несколько вариантов комбинаций символов. В частности, это полезный инструмент для фильтрации вектора строковых значений по определённому критерию. Например, регулярными выражениями с логическим оператором можно просто выделить из списка логов те файлы, которые были созданы в 2015-2016 годах:

models <- c('log_2014.csv', 'log_2015.csv', 'log_2016.csv', 'log_2017.csv', 'log_2018.csv')
models[grep('2015|2016', models)]
## [1] "log_2015.csv" "log_2016.csv"

Второй логический оператор — ^, используется в тех случаях, когда надо исключить символы определённого класса. Следует помнить, что ^ используется в этом значении сугубо внутри [], в противном случае будет интерпретироваться в другом значении.

# заменяем на '_' любой из символов d, e, f
gsub('[def]', '_', 'fadbcdefghe')
## [1] "_a_bc___gh_"
# заменяем на '_' всё, кроме символов d, e, f
gsub('[^def]', '_', 'fadbcdefghe')
## [1] "f_d__def__e"

Квантификаторы

Для того чтобы в паттерне указать, что какой-то символ или группа символов может повторяться, используют специальные управляющие знаки — квантификаторы:

  • ? — предыдущий символ или группа символов в паттерне может встречаться 0 или 1 раз, также вместе с другими квантификаторами используется для нежадного поиска;

  • * — предыдущий символ или группа символов в паттерне может встречаться 0 или больше раз;

  • + — предыдущий символ или группа символов в паттерне может встречаться 1 или больше раз;

  • {n} — предыдущий символ или группа символов может встречаться в паттерне строго n раз;

  • {n,} — предыдущий символ или группа символов может встречаться в паттерне n и более раз, онструкции {0,} и {1,} тождественны * и + соответственно;

  • {n,m} — предыдущий символ или группа символов может встречаться в паттерне n раз, но не более, чем m раз, конструкции {0,1} и ? тождественны.

Простейший пример использования квантификаторов — когда надо обработать какой-то символ, который встречается несколько раз. Например, скрыть последние четыре цифры в номере телефона:

phone_number <- 'my phone number: +7-929-138-5896'
gsub('[0-9]{4}$', 'XXXX', phone_number)
## [1] "my phone number: +7-929-138-XXXX"

Подстановочные знаки

Подстановочные знаки используются в тех ситуациях, когда надо просто указать, что в этом месте может быть любой символ. Несмотря на множественное число (wildcards), в регулярных выражениях в качестве подстановочного знака используется только метасимвол ., который обозначает любой знак.

Обычно подстановочный знак используется в сочетании с квантификаторами, указывающими, что в этом месте может быть любое количество символов. Например, использование подстановочных знаков может помочь найти строку, из которой известно только несколько символов. Попробуем из списка строк выбрать только адреса электронной почты в домене ru, исходя из предположения, что требуемые адреса содержат знак @ и сочетание .ru:

emails <- c('myemail@yahoo.com', 'id638@yandex.ru', 'simple string', 'lizzy@mail.ru', 'pumpkinpie@gmail.com')
emails[grep('@.+\\.ru', emails)]
## [1] "id638@yandex.ru" "lizzy@mail.ru"

Здесь первая . используется как подстановочный знак, + — квантификатор, указывающий, что после @ может встретиться один или более любых символов. \\. — это уже как раз . с экранированием, так как мы ищем именно комбинацию .ru.

Якоря

Якоря используются для обозначения, что сочетание символов в паттерне обязательно должно начинать или завершать строку. Также есть якоря, которые маркируют начало или конец не строки, а слова, однако само определение слова зависит от используемой локали. В R используются следующие якори:

  • ^ и $ — начало и конец строки соответственно;

  • \\< и \\> — начало и конец слова;

  • \\b — пустая строка или край слова;

  • \\B — не край слова.

Простым примером может быть выбор и чтение только xls-файлов с игнорированием xlsx-файлов или прочих файлов в папке. Так же при импорте файлов MS Office нередко приходится игнорировать ещё и временные файлы, которые создаются при работе с файлом MS Office и обычно скрыты от пользователя (их можно отличить по символам ~$ в начале названия файла). Для этого надо указать, что в начале названия строки нет маркеров временных файлов или что название файла начинается с требуемого буквосочетания. Также надо указать, что файл заканчивается строго на xls. Символы в названии, которые находятся между символами, используемыми в начале и конце строки, можно задать с помощью подстановочного знака и квантификатора (.*):

files <- c('report_2018.xlsx', 'report2017.xlsx', '~$report2016.xls', 'report2016.xls', 'report2015.xls', 'report2015.doc')

# пробуем извлечь xls-файлы c указанием якорей:
files[grep('^report.*xls$', files)]
## [1] "report2016.xls" "report2015.xls"
files[grep('^[^~$].*xls$', files)]
## [1] "report2016.xls" "report2015.xls"

Группы

Квантификаторы обычно работают относительно предыдущего указанного символа. Однако нередко возникают ситуации, когда требуется провести какую-то операцию над определённой группой символов. В таких случаях используют знаки группировки — ( и ). Набор символов и/или выражение, заключенные в скобки, будут восприниматься как неразрывная последовательность символов и обрабатываться соответствующим образом. Например, если мы хотим удалить из строки строго двукратное повторение группы символов:

my_string <- c('abRababL')

# пробуем удалить пару символов без указания группы
gsub('ab', '_DELETED_', my_string)
## [1] "_DELETED_R_DELETED__DELETED_L"
# группируем символы с помощью ()
gsub('(ab){2}', '_DELETED_', my_string)
## [1] "abR_DELETED_L"

При необходимости всю обрабатываемую строку можно с помощью символьных классов, групп и прочих инструментов регулярных выражений представить в паттерне. При подобном представлении группы нумеруются и может быть обработана конкретная группа символов, вызванная по номеру. Номера групп обозначаются как \\1-\\9.

Например, в строке есть как текст, так и номер телефона, и мы хотим извлечь всё, кроме текста и кода страны. Для этого мы пишем паттерн, который описывает всю строку, и необходимые нам части строки представляем в виде групп. Далее с помощью gsub() мы изменяем исходную строку так, что в ней остаются только требуемые нам группы из кодирующих телефонный номер:

phone_number <- 'my phone number: +7-929-138-58-96'
phone_number <- 'my phone number: +7-929-1385896'

phone_pattern <- '.+(\\+[0-9]*)-?([0-9]{3})-?([0-9]{3})-?([0-9]{2})-?([0-9]{2})'
gsub(phone_pattern, '\\2-\\3-\\4\\5', phone_number)
## [1] "929-138-5896"

В примере выше конструкция .+ обозначает все возможные символы до группы (\\+[0-9]*), которая, в свою очередь, обозначает числовой код страны. Код страны может стоять как из одной цифры, так и из нескольких. Конструкция -? нужна для того, чтобы учесть ситуации, когда номер телефона записывается с разделителями, расставленными в произвольном порядке (+7-929-138-5896 vs +79291385896 vs +7-929-138-58-96 и т. д.). Группы ([0-9]{3}), ([0-9]{2}) обозначают, собственно, сколько раз должны повторяться цифры в этой группе символов.

В результате в паттерне оказывается представлена вся строка, а элементы номера телефона представлены в виде пяти групп. Функция sub() находит в строке совпадение с паттерном и замещает строку на набор групп \\2-\\3-\\4\\5, притом четвертая и пятая группы сознательно не разделены знаком -, чтобы в результате последние четыре цифры номера были целостным блоком. Так как паттерн кодирует всю строку, то в результате мы получаем только те части строки, которые описываются группами. В нашем случае это номер телефона без указания кода страны.

Работа с кодировками

Наборы символов

С проблемой кодировок (точнее, наборов символов) встречаются все неанглоязычные пользователи, особенно сложно приходится пользователям Windows. На мой взгляд, работа с кодировками кириллицы занимает первое место в рейтинге самых неприятных задач в R, ей уступают даже тонкости работы с таймстампами и регулярными выражениями. Коротко это можно выразить как “надо отличать проблему с кодировкой на стороне источника данных, проблему кодировки клиента, получающего данные, и проблему отображения кодировки в IDE”.

Тема кодировок и сложностей работы с ними исходит из того, что необходимо кодировать всё многообразие символов естественных языков в машинно-читаемом виде (бинарной записи). Один из самых старых вариантов наборов символов, в котором символы кодируются одним байтом (т. е. всего 256 символов, так как 2 ^ 8 = 256), был создан в 1963 году, это ASCII. В нём нижнюю часть таблицы занимают технические и управляющие символы, цифры и латиница (коды 0-127). А верхнюю часть таблицы (128-255) занимают дополнительные и национальные символы. В такой конструкции нет особых проблем с латиницей, однако все прочие языки сталкиваются с необходимостью как-то разместить национальные символы в уже существующей таблице. Из-за жёстких ограничений подходы по составлению таблиц символов для нелатиницы бывали временами весьма экстремальными. Например, долгое время популярная в рунете и вообще в русскоязычной среде кодировка KOI8-R предполагала такое размещение кириллических символов в верхней части кодовой таблицы, чтобы позиции символов кириллицы соответствовали их фонетическим аналогам в английском алфавите из нижней части таблицы. Это означает, что если в тексте, написанном в KOI8-R, для каждого символа убрать по одному биту слева, то получится относительно читаемый текст, подобный транслиту.

Позднее (в 1991 году) появился стандарт Unicode, предполагающий кодирование символов более чем одним байтом. Стандарт состоит из двух основных частей: универсального набора символов (Universal character set, UCS) и семейства кодировок (Unicode transformation format, UTF). Коды в стандарте Unicode разделены на несколько областей. Так, область с кодами от U+0000 до U+007F содержит символы набора ASCII, и коды этих символов совпадают с их кодами в ASCII (символы с кодами 0-127, нижняя таблица). Далее расположены области символов других систем письменности, знаки пунктуации и прочие технические символы. UTF-8 и UTF-16 как раз два семейства кодировок символов в Unicode, которые различаются в первую очередь минимальным количеством байтов для кодирования символа (один байт в UTF-8 и два байта, даже для латиницы и технических символов, в UTF-16).

В настоящее время наборы символов стандартизированы, всё большую популярность набирает UTF-8. При этом набор символов формально зависит в первую очередь не от операционной системы, а от установленной локали. Тем не менее, в Windows используется кодировка CP1252 (ранее, до Windows 10, — CP1251).

Определение кодировки

Для определения кодировки данных есть несколько пакетов, в частности, некоторые функции stringi и пакет uchardet. По личному опыту, функции второго пакета чуть лучше определяют нестандартные кодировки. uchardet основан на утилите uchardet, которую разрабатывает компания Mozilla (любопытно, что при разработке кодировки русского языка использовались как тестовые). В основе методов автоматического определения кодировок лежат статистические модели частот символов и биграмм в языке, а также на схемах построения кодировок.

В пакете uchardet всего несколько функций: определение кодировок строк и побайтовой записи (в R), а также определение кодировки файла без импорта в рабочее окружение.

library(uchardet)
# определение UTF-8 кодировки
utf8 <- '\u4e0b\u5348\u597d'
print(utf8)
## [1] "下午好"
## [1] "UTF-8"

Перекодировка, iconv()

Функции импорта данных имеют, как правило, аргумент для указания кодировки. Тем не менее, временами этого бывает недостаточно (например, в файле смешанная кодировка или данные импортируются из базы данных). В таких случаях для конвертации данных в нужную кодировку эффективнее всего использовать функцию iconv() (есть и другие, но эта наиболее гибкая и мощная). Функция iconv() вызывает консольную утилиту iconv, правда, стоит учитывать, что для каждой платформы своя реализация, и результаты перекодировки могут различаться в Windows и, например, Linux.

# перекодируем строку из UTF-8 в CP1251
iconv_example <- iconv('перекодируем из UTF-8 в CP1251', from = 'UTF-8', to = 'CP1251')
iconv_example
## [1] "\xef\xe5\xf0\xe5\xea\xee\xe4\xe8\xf0\xf3\xe5\xec \xe8\xe7 UTF-8 \xe2 CP1251"
# делаем обратную перекодировку, сочетаем с детекцией кодировки
iconv(iconv_example, detect_str_enc(iconv_example), 'UTF-8')
## [1] "перекодируем из UTF-8 в CP1251"

iconv поддерживает достаточно большой список кодировок. Посмотреть список можно с помощью iconvlist():

# пять случайных кодировок из списка iconvlist
sample(iconvlist(), 5)
## [1] "OSF0005000A" "TIS-620"     "EUCJP-WIN"   "CSIBM038"    "IBM9448"