Треки
/
R
R
/
Салабус
/
Рядки тексту
Ря

Рядки тексту у R

2 вправи

Про концепцію Рядки тексту

Загальні відомості

Рядок тексту (англ. string) - це фрагмент тексту між лапками.

mystring <- "some text"
mystring
#> [1] "some text"
typeof(mystring)
#> [1] "character"

На жаль, ця термінологія може заплутати програмістів, знайомих з іншими мовами.

Хоча type тут character, R не розрізняє одну літеру (у багатьох інших мовах її називають char, але не в R) і довгий рядок тексту. І те, і те є одним елементом, і його можна брати як в одинарні ' ', так і в подвійні " " лапки, без жодної різниці.

Посібник зі стилю радить використовувати подвійні лапки, крім випадків, коли всередині рядка тексту потрібні подвійні лапки без екранування.

str2 <- 'String with "quotes"'  # convenience format
str2
#> [1] "String with \"quotes\""    # standard format

Отже, character vector - це вектор рядків тексту, де кожен рядок є окремим елементом.

s <- c("some", "strings")
s
#> [1] "some"    "strings"
length(s)
#> [1] 2
length(s[1])  # the number of vector elements, not the string length
#> [1] 1

nchar(s[1])  # the number of characters in the string "some"
#> [1] 4

У наведеному вище прикладі length(s[1]) рівнозначно length("some"): ідеться про кількість елементів вектора, а не про довжину рядка тексту.

Маніпулювати окремим рядком тексту, звісно, можна, як у прикладі з nchar() вище, але для цього потрібні спеціальні функції, про які йтиметься далі.

Екранування

Як ми бачили вище, деякі розділові символи потрібно «екранувати», поставивши перед ними зворотну скісну риску \.

Цей список навряд чи здивує досвідчених програмістів, адже ця тема досить стандартизована в різних мовах.

Крім лапок (як показано вище), до списку входять символи нового рядка \n, табуляції \t, зворотна скісна риска \\ і символи Unicode, що починаються з \U або \u.

Щоб не використовувати багато екранованих символів, можна визначати необроблені рядки всередині r"( )":

r"(raw "string")"
#> [1] "raw \"string\""

Для необроблених рядків, що містять дужки ( ), є багато інших варіантів синтаксису.

Локалі

Будь-яка сучасна підтримувана версія R кодує рядки тексту в UTF-8, тож більшість мов світу можна підтримувати.

"आधुनिक मानक हिन्दी"
#> [1] "आधुनिक मानक हिन्दी"

Крім того, багато функцій R зважають на locale і намагаються дотримуватися місцевих узвичаєнь щодо сортування рядків тексту, перетворення на верхній чи нижній регістр, дат і часу тощо.

У цьому силабусі приклади переважно для американської англійської: і тому, що це поширений типовий варіант, і тому, що автори силабусу живуть у США.

Локалі - складна тема, тож варто пошукати документацію під власну ситуацію.

Функції для рядків тексту

У базовому R можливо майже все, але багато програмістів вважають підтримку роботи з рядками тексту досить обмеженою й заплутаною.

На щастя, є пакети tidyverse, значно інтуїтивніші, гнучкіші та самоузгодженіші.

Базовий R

Досі існує багато застарілого коду, що використовує функції для рядків старого зразка. Це стосується й більшості рішень спільноти на Exercism.

Крім того, для невеликих завдань буває корисно використати вбудовані функції замість імпорту великої бібліотеки. Супровідники треку R схильні робити саме так, коли пишуть зразкові рішення для перевірки вправ у нашому репозиторії на GitHub.

Тож певне знання старих підходів усе ще варте вивчення.

Наведені нижче функції показано в найпростішій формі, але більшість із них має ще й необовʼязкові аргументи. Деталі можна знайти в документації.

nchar

Повертає кількість символів у рядку тексту.

nchar("string")
#> [1] 6

nchar("Hrōðgār") # a person in Beowulf, written in Old English
#> [1] 7

Тема того, що означає «символ» (складна!), виходить за межі нашої розповіді. Звернімо увагу лише на кілька моментів у прикладах вище:

  • Хоча рядки тексту в R внутрішньо реалізовано мовою C, вони не завершуються \0 (це додало б 1 до довжини).
  • Багатобайтові символи Unicode обробляються так, як і можна очікувати чи сподіватися. «Hrōðgār» містить 7 символів (їх також називають «рунами» або «кодовими точками»), але для зберігання потребує 10 байтів.

Будьмо уважні до того, що саме передаємо в nchar. Результати все ще можуть бути непередбачуваними для значень не типу character, хоча в останніх версіях R це покращилося.

grep

Шукає зразок у векторі рядків тексту й повертає індекси збігів.

grep("Jav", c("Java", "Python", "Javascript"))
#> [1] 1 3
paste і paste0

Поєднує довільну кількість рядків тексту в один рядок.

За бажанням для paste можна вказати роздільник: типово це пробіл.

paste0(v) рівнозначно paste(v, sep = "").

paste("bits", "of", "a", "string")
#> [1] "bits of a string"
paste0("bits", "of", "a", "string")
#> [1] "bitsofastring"
sprintf

Для тоншого контролю над складанням рядків тексту R запозичує функцію sprintf із C (і з кількох пізніших мов: Вікіпедія налічує близько 30).

r <- 5.3
pi <- 3.14159
sprintf("A circle of radius %.1f has area %.2f", r, pi * r^2)
#> [1] "A circle of radius 5.3 has area 88.25"
trimws

Провідні та/або кінцеві пробільні символи можна прибрати за допомогою trimws():

s <- "    messy string   "
trimws(s)
#> [1] "messy string"

Є параметр which = "left" (або "right"), щоб не обрізати обидва кінці.

substr

Працює з одним рядком тексту й повертає підрядок між двома межами (включно).

substr("Exercism", 5, 7)
#> [1] "cis"
strsplit

Розбиває рядок тексту на список R за рядком-роздільником. Роздільник обовʼязковий (без типового значення), але може бути порожнім рядком.

strsplit("R, Python, Julia", ", ")
[[1]]
#> [1] "R"      "Python" "Julia" 

strsplit("Exercism", "")
[[1]]
#> [1] "E" "x" "e" "r" "c" "i" "s" "m"

Списки розглянемо в окремому концепті. Якщо ми ще не дійшли до цієї частини силабусу, достатньо поки знати, що функція unlist() (у цьому випадку) перетворить список на вектор.

unlist(strsplit("Exercism", ""))
#> [1] "E" "x" "e" "r" "c" "i" "s" "m"
regexpr і regexpr

Це скорочення від «regular expression» (регулярний вираз): ці функції шукають входження зразка в рядку тексту й повертають список (потенційно) корисних даних. Їх згадано лише для повноти: є кращі варіанти!

regexpr повертає лише перше входження, gregexpr повертає всі входження («g» означає «global», тобто глобальний пошук).

Як видно з назв, зразок може бути регулярним виразом, про який ітиметься в пізнішому концепті.

Advanced

Чому ці старі функції для рядків тексту схожі на команди оболонки Unix?

Хоча R створили 1993 року, його задумали значною мірою зворотно сумісним із мовою S.

Мову S розробили в Bell Labs, уперше вона зʼявилася 1976 року. Bell Labs також відповідає за Unix (з 1969 року) і мову C (1972).

Оце так команда з тієї значно давнішої епохи! Залишається лише припустити, що вони спілкувалися між собою й обмінювалися ідеями.

Tidyverse

Якщо бути поблажливими, можна сказати, що старі функції для рядків тексту - не найкраще, що може запропонувати R (є й менш поблажливі думки).

Щоб виправити становище, бібліотеку stringr розробляють із 2009 року, і тепер вона є ключовою частиною збірки Tidyverse.

Імпортуймо її у свій простір імен за допомогою library(stringr) або library(tidyverse).

Серед ключових можливостей:

  • Усі назви функцій мають префікс str_ і намагаються зробити призначення зрозумілим.
  • Порядок аргументів прагне бути самоузгодженим, а вхідні дані стоять першим аргументом.
  • Вхідні дані можуть бути від одного рядка тексту до складних структур даних. Функції намагаються підлаштуватися й зробити найрозумніше.

Як і всі пакети Tidyverse, stringr має добру документацію, яку зазвичай пишуть автори пакета.

У stringr доступні десятки функцій, і вони часто мають різноманітні необовʼязкові аргументи. Спершу це може трохи перевантажувати, тож документація - наш друг.

Подумаймо, що хочемо подати на вхід (один рядок тексту? вектор? щось складніше?) і що хочемо отримати на виході.

Імовірно, є простий спосіб зробити те, що потрібно, якщо знайти правильну функцію та правильні аргументи.

Відступ: короткий вступ до конвеєрів

Використання конвеєрів |> для зʼєднання функцій докладніше розглянемо в інших частинах силабусу.

Поки що корисно знати, що:

  • Результат ліворуч від конвеєра стає першим аргументом функції праворуч (де «праворуч» ігнорує необовʼязкові розриви рядків).
  • Будь-які інші аргументи можна додати у виклик функції.
  • У R ми маємо передавати через конвеєр саме виклик функції, а не лише її назву, тож дужки потрібні завжди (це відрізняється від деяких інших мов).

Наведений нижче приклад ілюструє синтаксис. Окремі функції розглянемо пізніше.

library(stringr)

"Monday-25-February" |> 
  str_split("-") |>     
  unlist() |>               # [1] "Monday"   "25"       "February"
  str_sub(1, 3) |>          # [1] "Mon" "25"  "Feb"
  str_to_upper() |>         # [1] "MON" "25"  "FEB"
  str_flatten(collapse = ", ")
#> [1] "MON, 25, FEB"
Робота з довжиною

str_length рахує кодові точки (подібно до nchars).

Різні інші функції працюють із пробільними символами, зокрема str_trim() і str_squish(). Це часто важливе завдання очищення на початку будь-якого проєкту з науки про дані.

# The first 3 kernels in JuPyteR notebooks:
str_length(c("Julia", "Python", "R"))           #> [1] 1 6 5
#> [1] 5 6 1
str_pad(c("Julia", "Python", "R"), 8, "right")  # add spaces if necessary
#> [1] "Julia   " "Python  " "R       "

str_trim("Julia    Python    R    ")   # remove leading & trailing whitespace
#> [1] "Julia    Python    R"
str_squish("Julia   Python  R    ")  # trim, collapse multiple spaces 
#> [1] "Julia Python R"
Вибір підрядків

Функція str_sub(string, start, end) повертає підрядок від start до end (включно), а str_sub_all() може працювати з кількома рядками тексту.

Тут багато гнучкості. start і end типово вказують на перший і останній символ відповідно. Вони можуть бути векторами індексів. На відміну від індексації векторів (і це приємно), відʼємні значення відраховуються від кінця рядка тексту, як у Python.

s <- "abcdefgh"
str_sub(s, 2, 4)
#> [1] "bcd"
str_sub(s, 5)
#> [1] "efgh"
str_sub(s, -3, -2)
#> [1] "fg"
str_sub(s, c(1, 4), c(2, 5))
#> [1] "ab" "de"
str_sub(s, c(1, 4, 6), c(2, 5, -1))
#> [1] "ab"  "de"  "fgh"

s |> str_sub(2, 4) # these functions are designed for pipes
#> [1] "bcd"

# str_sub_all takes vector input, gives list output
str_sub_all(c(s, "ijklmnop"), c(1, 4, 6), c(2, 5, -1))
[[1]]
#> [1] "ab"  "de"  "fgh"

[[2]]
#> [1] "ij"  "lm"  "nop"
Зʼєднання рядків

str_flatten перетворює вектор character на один рядок тексту. Аргумент collapse вставляється між кожним елементом і типово дорівнює "".

c("R", "Julia", "Python") |> str_flatten()
#> [1] "RJuliaPython"
c("R", "Julia", "Python") |> str_flatten(collapse = " - ")
#> [1] "R - Julia - Python"

str_c перетворює кілька векторів character на один вектор character, за потреби повторюючи елементи.

str_c(LETTERS[1:8], 1:8, sep = ":")
#> [1] "A:1" "B:2" "C:3" "D:4" "E:5" "F:6" "G:7" "H:8"

str_glue приносить у R просту інтерполяцію рядків тексту. Вирази всередині фігурних дужок { } обчислюються, а замість них підставляється їхній текстовий вигляд.

Якщо подати кілька рядків із виразами, їх також зʼєднають, як у str_flatten. Рядки тексту також можна за бажанням очистити від провідних і кінцевих пробільних символів.

r <- 5.3
pi <- 3.14159
str_glue("A circle of radius {r} has area {pi * r^2}")
A circle of radius 5.3 has area 88.2472631

Хоча str_glue гнучка (і доволі складна), ще ширші можливості дає бібліотека glue, на якій вона ґрунтується.

Розбиття рядків

Дуже поширене застосування R - приводити неохайні дані до однорідного формату для аналізу. Щоб це полегшити, є кілька способів розбити текст на підрядки.

str_split_1 найпростіша: перетворює один рядок тексту на вектор частин, розбиваючи його за вказаним зразком (типового зразка немає).

s <- "R Julia Python"
str_split_1(s, " ")
#> [1] "R"      "Julia"  "Python"

str_split_i приймає вектор character та індекс n і повертає вектор із n-м підрядком кожного вхідного рядка тексту.

s2 <- c(s, "C Kotlin, F#")  #> [1] "R Julia Python" "C Kotlin, F#"
str_split_i(s2, " ", 2)  # 2nd element of each
#> [1] "Julia"   "Kotlin,"

str_split приймає вектор character і повертає список векторів. У нашому силабусі є концепт списків, який пояснює цей результат.

str_split(s2, " ")
[[1]]
#> [1] "R"      "Julia"  "Python"

[[2]]
#> [1] "C"       "Kotlin," "F#"     

Є також str_split_fixed, яка повертає матрицю. Матриці розглянемо в пізнішому концепті.

Зіставлення рядків

Щоб зіставити весь рядок тексту, str_equal() може використовувати різні визначення «рівності». Найпростіше: типово враховується регістр, але це можна змінити. Локалі та Unicode відкривають значно більше можливостей.

str_equal("Odin", "odin")
#> [1] FALSE
str_equal("Odin", "odin", ignore_case = TRUE)
#> [1] TRUE

Є багато інших функцій, про які докладніше йтиметься в концепті регулярних виразів.

Зміна рядків

Деякі мови (переважно європейські) розрізняють великі та малі літери. У stringr є набір функцій для взаємного перетворення регістрів, з урахуванням локалі:

  • str_to_lower()
  • str_to_upper()
  • str_to_title() : перша літера кожного слова - велика
  • str_to_sentence() : перша літера речення - велика
str_to_upper("elixir")    # defaults to locale = "en"
#> [1] "ELIXIR"
str_to_upper("ελληνικά")  # infers Greek
#> [1] "ΕΛΛΗΝΙΚΆ"

str_to_title("the cat sat on the mat")
#> [1] "The Cat Sat On The Mat"
str_to_sentence("the cat sat on the mat")
#> [1] "The cat sat on the mat"

Для заміни підрядка на основі зіставлення зі зразком str_replace() змінює перше входження зразка, а str_replace_all() змінює всі входження.

Для заміни підрядка на основі індексації символів є форма присвоєння для str_sub().

 s <- "abcde"
str_sub(s, 3, 4) <- "yz"
s
#> [1] "abyze"
Різне

str_dup створює дублікати, а str_unique прибирає дублікати.

Типово str_dup не має роздільника, але його можна вказати.

str_dup("xyz", 4, sep = ", ")
#> [1] "xyz, xyz, xyz, xyz"

str_unique - розширення unique(). Обидві прибирають повторювані елементи з вектора, але функція з stringr уміє ще й ураховувати регістр і різні локальні зіставлення.

inp <- c("some", "strings", "Some", "Strings")
str_unique(inp)
#> [1] "some"    "strings" "Some"    "Strings"
str_unique(inp, ignore_case = TRUE)
#> [1] "some"    "strings"
Advanced

Бібліотека stringr ґрунтується на бібліотеці stringi із функцій нижчого рівня.

Якщо є конкретні потреби, не розглянуті вище, тоді:

  1. Перегляньмо документацію stringr: вона охоплює більше можливостей, ніж ми можемо згадати навіть у цьому довгому документі концепту.
  2. Перегляньмо документацію stringi, яка описує ще багато функціональності.
Редагувати через GitHub Посилання відкривається в новому вікні або вкладці

Вивчити концепцію Рядки тексту

Практика заблокована

Розблокуйте ще 1 вправу, щоб практикувати концепцію Рядки тексту