Краткое руководство по строкам и регулярным выражениям в R

Сейчас легко найти данные. Но вот найти высококачественные оказывается весьма проблематично. Одна из характерных черт низкопробных данных в том, что они запутаны и редко точны. Сколько бы мы, профессионалы в этой сфере, не говорили об алгоритмах и проверке моделей, большую часть времени занимает именно очистка данных.

В этом смысле работа со строками требует несколько иного набора навыков, чем работа с теми же списками или data.frame. В текущей статье, как вы уже поняли, мы будем учиться максимально эффективно управлять строками. Начнем!

Вставка и разделение

Вставка и разделение частей строк  —  это две из наиболее типичных задач, с которыми мы встречаемся.

Для них у нас есть две простые функции: paste() и strsplit().

paste('Ugurcan' , 'Demir' , sep = " ")
## [1] "Ugurcan Demir"
## [[1]]
## [1] "Ugurcan" "Demir"
strsplit("Ugurcan Demir" , split = " ")
paste("The","United" ,"States" ,"of" ,"America" , sep = " ")
## [1] "The United States of America"
unlist(strsplit("The United States of America" , split = " "))
## [1] "The"     "United"  "States"  "of"      "America"

Общее число символов и разделение

У R и Python есть немало общего. Оба этих языка легки в освоении и за счет своих гибких библиотек становятся типичным выбором среди статистов, практиков машинного обучения, да и любых интересующихся наукой о данных людей. Если же вы пришли в R из Python, то примеры, которые я планирую показать, могут показаться вам несколько странными.

Например, для нахождения общего количества символов интуитивным выбором будет lengh(). Но в R это происходит не так, как в Python.

length("The United States of America")
## [1] 1
nchar("The United States of America")
## [1] 28

Разделение здесь тоже отличается. Для этого у нас есть две функции: substr() и substring(). Они работают абсолютно одинаково, если указать параметры начала и завершения. Тем не менее у substring() есть предустановленное стоп-значение, а у substr() нет.

substr("The United States of America" , start = 10 , stop = 20)
## [1] "d States of"

substring("The United States of America" , first = 10 , last = 20)
## [1] "d States of"

Вот что происходит, если не передать аргумент в параметр stop:

substring("The United States of America" , first = 10 )
## [1] "d States of America"
substr("The United States of America" , start = 10 )
## Error in substr("The United States of America", start = 10): argument "stop" is missing, with no default

regexec() , gregexpr() и grep()

Я уже слышу ваш вопрос: “А откуда нам знать индексы для передачи аргументов?”. Что ж, когда у вас всего один фрагмент строки, то это несложно подсчитать на пальцах, но в случае миллионов, да даже десятков, строк данных такой подход не сгодится. К счастью, у нас есть для этого две прекрасные функции.

Первая из них, regexec(), используется для поиска первого вхождения подстроки внутри большой строки.

regexec(pattern = 'United' , text =  "The United States of America" )
## [[1]]
## [1] 5
## attr(,"match.length")
## [1] 6
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE

regexec(pattern = 'U' , text =  "The United States of America"  )
## [[1]]
## [1] 5
## attr(,"match.length")
## [1] 1
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE

При этом gregexpr() находит все вхождения подстроки.

gregexpr(pattern = 'e' , text =  "The United States of America"  )
## [[1]]
## [1]  3  9 16 24
## attr(,"match.length")
## [1] 1 1 1 1
## attr(,"index.type")
## [1] "chars"
## attr(,"useBytes")
## [1] TRUE

Функция grep() получает второй аргумент, который отражает уже не фрагмент строки, а вектор строк. В ответ же она возвращает индексы элементов, которые содержат искомую подстроку. Если дополнительно установить значение параметра на T (TRUE), функция вернет сами элементы.

grep(pattern = 'wigh' , x = c('Michael' , "Jim" , "Dwight" , "Pam") )
## [1] 3

grep(pattern = 'm' , x = c('Michael' , "Jim" , "Dwight" , "Pam") )
## [1] 2 4

grep(pattern = 'm' , x = c('Michael' , "Jim" , "Dwight" , "Pam") , value = T)
## [1] "Jim" "Pam"

sub() и gsub()

sub() и gsub() идут еще дальше и заменяют часть большой строки, соответствующую заданной подстроке, строкой, переданной в качестве аргумента.

sub(pattern = 'm' , replacement = "n" , x = c('Michael',"Jim","Dwight","Pam"))
## [1] "Michael" "Jin"     "Dwight"  "Pan"

sub(pattern = "i" , replacement = "a" , x = "The United States of America")
## [1] "The Unated States of America"

Если вы заметили, что слово “America” осталось прежним, то дело в том, что sub() заменяет только первое вхождение подстроки. Для замены всех мы используем gsub().

gsub(pattern = "i" , replacement = "a" , x = "The United States of America")
## [1] "The Unated States of Ameraca"

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

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

Эти выражения встречаются во многих языках программирования и несколько отличаются в реализации. Их основная задача  —  искать паттерн строки в заданной строке большего размера.

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

Метасимволы

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

  • “$”
  • “*”
  • “+”
  • “.”
  • “?”
  • “[ ]”
  • “^”
  • “{ }”
  • “|”
  • “( )”
  • “\ ”

Далее я объясню действие каждого из этих метасимволов.

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

Среди метасимволов знаки “?” , “*” , “+” и “{ }” называются квантификаторами, потому что указывают, сколько раз мы хотим увидеть заданный паттерн.

  • “*”: предыдущий элемент должен встречаться 0 или более раз.
  • “+”: предыдущий элемент должен встречаться 1 или более раз.
  • “?”: предыдущий элемент должен встречаться 0 или 1 раз.
  • “{ ,m}”: предыдущий элемент должен встречаться m или меньше раз.
  • “{n, }”: предыдущий элемент должен встречаться n или более раз.
  • “{n , m}”: предыдущий элемент должен встречаться от n до m раз.
  • “{m}”: предыдущий элемент должен встречаться ровно m раз.
letter_vector <- c(
  "AACACA","BBCCBC","CCABBB","ABABAA","ACBCAA","BCACBC",
  "BABABA","CACABA","BBABAB","BCCBAB","CAABCC","BCCBCA",
  "CAAABA","BAABCB","CCABBC","ABABBA","CABAAC","CAABCC",
  "CABCAC","AABCAA","CAAACB","BBACCA","BCAAAB","BBACBC",
  "CCCCBC","ACABCA","BCBBBC","AABBCC","CCBBBB","BBABBA","BBCAAC"
)

grep(pattern = "ABC" , x =  letter_vector , value = T)
## [1] "CAABCC" "BAABCB" "CAABCC" "CABCAC" "AABCAA" "ACABCA"

grep(pattern = "AB*C" , x =  letter_vector , value = T)
##  [1] "AACACA" "ACBCAA" "BCACBC" "CACABA" "CAABCC" "BAABCB" "CCABBC" "CABAAC"
##  [9] "CAABCC" "CABCAC" "AABCAA" "CAAACB" "BBACCA" "BBACBC" "ACABCA" "AABBCC"
## [17] "BBCAAC"

grep(pattern = "AB+C" , x =  letter_vector , value = T)
## [1] "CAABCC" "BAABCB" "CCABBC" "CAABCC" "CABCAC" "AABCAA" "ACABCA" "AABBCC"

grep(pattern = "AB?C" , x =  letter_vector , value = T)
##  [1] "AACACA" "ACBCAA" "BCACBC" "CACABA" "CAABCC" "BAABCB" "CABAAC" "CAABCC"
##  [9] "CABCAC" "AABCAA" "CAAACB" "BBACCA" "BBACBC" "ACABCA" "BBCAAC"

grep(pattern = "AB{,2}C" , x =  letter_vector , value = T)
##  [1] "AACACA" "ACBCAA" "BCACBC" "CACABA" "CAABCC" "BAABCB" "CCABBC" "CABAAC"
##  [9] "CAABCC" "CABCAC" "AABCAA" "CAAACB" "BBACCA" "BBACBC" "ACABCA" "AABBCC"
## [17] "BBCAAC"

grep(pattern = "AB{2,}C" , x =  letter_vector , value = T)
## [1] "CCABBC" "AABBCC"

grep(pattern = "AB{1,2}C" , x =  letter_vector , value = T)
## [1] "CAABCC" "BAABCB" "CCABBC" "CAABCC" "CABCAC" "AABCAA" "ACABCA" "AABBCC"

grep(pattern = "AB{2}C" , x =  letter_vector , value = T)
## [1] "CCABBC" "AABBCC"

Метасимволы начала и завершения

Символы “^” и “$” представляют начало и конец строки соответственно. Иногда их еще называют якорями. При этом они никаким символам не соответствуют.

grep(pattern = "^A" , x =  letter_vector , value = T)
## [1] "AACACA" "ABABAA" "ACBCAA" "ABABBA" "AABCAA" "ACABCA" "AABBCC"

grep(pattern = "C$" , x =  letter_vector , value = T)
##  [1] "BBCCBC" "BCACBC" "CAABCC" "CCABBC" "CABAAC" "CAABCC" "CABCAC" "BBACBC"
##  [9] "CCCCBC" "BCBBBC" "AABBCC" "BBCAAC"

Плейсхолдер

Следующий метасимвол  —  это “.”, который соответствует любому символу в том месте, где используется. В примере ниже ищется любой паттерн, начинающийся с “C”, заканчивающийся на “A” и имеющий между этими символами любые два символа.

grep(pattern = "C..A" , x =  letter_vector , value = T)
## [1] "AACACA" "ACBCAA" "CACABA" "BCCBAB" "BCCBCA" "CAAABA" "CABAAC" "CAAACB"
## [9] "BCAAAB"

Последовательности

Метасимвол “\” при использовании с набором ключевых букв служит для определения конкретной последовательности символов в строке и сопоставляется с этой последовательностью при использовании в функциях для строк. Ниже приводится список ключевых букв, которые часто используются с этим метасимволом:

  • “\d” = цифра;
  • “\D” = не цифра;
  • “\w” = словесный символ (a-z, A-Z, 0???9);
  • “\W” = не словесный символ;
  • “\s” = пробельный символ;
  • “\S” = не пробельный символ;
  • “\b” = граница слова;
  • “\B” = не граница слова.

Вот примеры:

string1 <- 'My name is Ugurcan and I am 25.'
gsub(pattern = "\\d" , replacement = "-" , x = string1)
## [1] "My name is Ugurcan and I am --."

gsub(pattern = "\\s" , replacement = "-" , x = string1)
## [1] "My-name-is-Ugurcan-and-I-am-25."

gsub(pattern = "\\w" , replacement = "-" , x = string1)
## [1] "-- ---- -- ------- --- - -- --."

gsub(pattern = "\\b" , replacement = "-" , x = string1)
## [1] "-M-y- -n-a-m-e- -i-s- -U-g-u-r-c-a-n- -a-n-d- -I- -a-m- -2-5-.-"

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

Еще один метасимвол  —  это “[ ]”, который зачастую служит для формирования комплексных паттернов при анализе сложных и неструктурированных текстовых данных. В эти квадратные скобки можно передать несколько символов, в результате чего будут найдены только они, но не вместе, а по-отдельности, порядок при этом значения не имеет. Также можно указывать диапазон искомых символов с помощью дефиса.

grep(pattern = "[zp]" , x = state.name , value = T)
## [1] "Arizona"       "Mississippi"   "New Hampshire"

grep(pattern = "[b-d]" , x = state.name , value = T)
##  [1] "Alabama"       "Colorado"      "Connecticut"   "Florida"      
##  [5] "Idaho"         "Indiana"       "Kentucky"      "Maryland"     
##  [9] "Massachusetts" "Michigan"      "Nebraska"      "Nevada"       
## [13] "New Mexico"    "Rhode Island"  "Wisconsin"

grep(pattern = "[od]$" , x = state.name , value = T)
## [1] "Colorado"     "Idaho"        "Maryland"     "New Mexico"   "Ohio"        
## [6] "Rhode Island"

В определенных случаях можно заключать в квадратные скобки встроенные имена классов. Ниже приводится полный список таких имен.

  • [:alnum:] = буквенно-цифровые символы: [:alpha:] и [:digit:].
  • [:alpha:]= буквенные символы: [:lower:] и [:upper:].
  • [:blank:]= пустые символы: пробелы и табуляции, а также определяемые языковым стандартом, например неразрывный пробел.
  • [:cntrl:] = управляющие символы в ASCII. Эти символы имеют восьмеричные коды от 000 до 037 и 177 (DEL). В других наборах символов они являются равнозначными, если присутствуют.
  • [:digit:] = цифры: 0 1 2 3 4 5 6 7 8 9.
  • [:graph:] = графические символы: [:alnum:] и [:punct:].
  • [:lower:] = буквы нижнего регистра в текущем языковом стандарте.
  • [:print:] = печатные символы: [:alnum:], [:punct:] и пробел.
  • [:punct:] = знаки препинания: ! “ # $ % & ’ ( ) * + , — / : ; < = > ? @ [ ] ^ _ ` { | } ~. “.
  • [:space:] = пробельные символы: табуляция, новая строка, вертикальная табуляция, возврат каретки, перевод страницы, а также другие символы, определяемые различными языковыми стандартами.
  • [:upper:] = буквы верхнего регистра в текущем языковом стандарте.
  • [:xdigit:] = шестнадцатеричные цифры: 0 1 2 3 4 5 6 7 8 9 A B C D E F a b c d e f.

Группировка и оператор ИЛИ

Последними метасимволами у нас идут “()” и “|”, которые обычно используются вместе. С помощью скобок мы обособляем нужные наборы символов, а оператор ИЛИ позволяет указывать на возможность выбора между ними. Вот примеры:

grep(pattern = "(th|la)" , x = state.name , value = T)
##  [1] "Alabama"        "Alaska"         "Delaware"       "Maryland"      
##  [5] "North Carolina" "North Dakota"   "Oklahoma"       "Rhode Island"  
##  [9] "South Carolina" "South Dakota"

grep(pattern = "^New (Y|J)" , x = state.name , value = T)
## [1] "New Jersey" "New York"

Экранирование

Мы рассмотрели все метасимволы, но что, если искомый паттерн содержит один из них? 

Это очевидный и ожидаемый вопрос. В таком случае нужно сообщить R, что этот символ нужно рассматривать не как метасимвол, а как обычный знак.

Для этого мы добавляем тот же обратный слэш “\” перед нужным метасимволом, экранируя его. А поскольку обратный слэш сам является метасимволом, мы добавляем к нему еще один слэш для экранирования. Вот примеры:

string2 <- c("Lionel Messi\ PSG" , 'file_name$' , "{2022}")
grep(pattern = "\\$" , x = string2  , value = T)
## [1] "file_name$"

grep(pattern = "\\{" , x = string2  , value = T)
## [1] "{2022}"

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

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Uğurcan Demir: A Concise Guide for Strings and Regular Expressions in R

Предыдущая статьяНастройка проекта TypeScript с помощью ESLint, Prettier и VS Code
Следующая статьяКак обеспечить работу современного кода JavaScript во всех браузерах