رش

رشته در R

2 تمرین

درباره‌ی رشته

پیشینه

«رشته» تکه‌ای از متن است که بین دو علامت نقل قول قرار می‌گیرد.

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

متأسفانه این اصطلاحات ممکن است برای برنامه‌نویسانی که با زبان‌های دیگر آشنا هستند گیج‌کننده باشد.

با اینکه type برابر character است، R بین یک حرف تکی (که در چندین زبان دیگر char نامیده می‌شود، اما در R نه) و یک رشته‌ی بلند هیچ تفاوتی نمی‌گذارد. هر دو یک عنصر واحد هستند و می‌توانند به‌جای یکدیگر در گیومه‌ی تکی ' ' یا دوگانه " " قرار بگیرند.

راهنمای سبک توصیه می‌کند از گیومه‌های دوگانه استفاده کنید، مگر زمانی که می‌خواهید از گیومه‌های دوگانه درون رشته استفاده کنید بدون آنکه آن‌ها را «escape» کنید.

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() در مثال بالا دیدیم، اما به توابع خاصی نیاز دارد که در ادامه بحث خواهیم کرد.

escape کردن

همان‌طور که در بالا دیدیم، بعضی از کاراکترهای نشانه‌گذاری باید با یک بک‌اسلش \ در ابتدایشان escape شوند.

این فهرست برای بیشتر برنامه‌نویسان باتجربه غافلگیرکننده نخواهد بود، چون این موضوع در میان زبان‌ها نسبتاً یکسان‌سازی شده است.

علاوه بر گیومه‌ها (همان‌طور که در بالا نشان داده شد)، این فهرست شامل خط جدید \n، تب \t، بک‌اسلش \\ و نمادهای یونیکد است که با \U یا \u شروع می‌شوند.

برای اینکه از استفاده‌ی زیاد از کاراکترهای escape پرهیز کنید، می‌توانید رشته‌های خام را درون r"( )" تعریف کنید:

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

برای رشته‌های خامی که پرانتز ( ) دارند، گزینه‌های نحوه‌ی نگارش دیگری هم وجود دارد.

locale

هر نسخه‌ای از R که امروز پشتیبانی می‌شود، رشته‌ها را در UTF-8 رمزگذاری می‌کند، بنابراین بیشتر زبان‌های جهان پشتیبانی می‌شوند.

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

علاوه بر این، بسیاری از توابع R از locale آگاه هستند و می‌کوشند از قواعد محلی برای مرتب‌سازی رشته، تبدیل به حروف بزرگ/کوچک، تاریخ و زمان و غیره پیروی کنند.

این سرفصل نمونه‌ها را عمدتاً برای انگلیسی آمریکایی نشان می‌دهد: هم به این دلیل که این یک پیش‌فرض رایج است، و هم به این دلیل که نویسندگان سرفصل به‌طور اتفاقی در آمریکا زندگی می‌کنند.

موضوع 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

موضوع (پیچیده‌ی!) اینکه «کاراکتر» یعنی چه، خارج از حوصله‌ی این بحث است. فقط به چند نکته در مثال‌های بالا توجه کنید:

  • اگرچه درون خود به زبان C پیاده‌سازی شده‌اند، رشته‌ها در R با \0 پایان نمی‌یابند (که می‌توانست ۱ به طول اضافه کند).
  • کاراکترهای یونیکد چندبایتی همان‌طور که انتظار دارید (یا امیدوارید) مدیریت می‌شوند. «Hrōðgār» ۷ کاراکتر دارد (که «رون» یا «نقطه‌ی کد» هم نامیده می‌شوند)، اما برای ذخیره‌سازی به ۱۰ بایت نیاز دارد.

مراقب باشید چه چیزی را به 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 کپی کرده است (و چندین زبان بعدی هم آن را برداشته‌اند: ویکی‌پدیا حدود ۳۰ مورد را فهرست می‌کند).

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

این توابع که کوتاه‌شده‌ی «عبارت باقاعده» هستند، به دنبال رخدادهای یک الگو در یک رشته می‌گردند و لیستی از داده‌های (احتمالاً) مفید را برمی‌گردانند. این‌ها فقط برای کامل بودن ذکر شده‌اند: گزینه‌های بهتری هم وجود دارد!

regexpr فقط اولین رخداد را برمی‌گرداند و gregexpr همه‌ی رخدادها را («g» به معنی جست‌وجوی «global»).

همان‌طور که از نام‌ها پیداست، الگو می‌تواند یک regex باشد که در یک مفهوم بعدی پوشش داده می‌شود.

Advanced

چرا این توابع رشته‌ی قدیمی شبیه دستورهای پوسته‌ی یونیکس هستند؟

با اینکه R در سال ۱۹۹۳ ساخته شد، طوری طراحی شد که تا حد زیادی با زبان S سازگار به عقب باشد.

زبان S در آزمایشگاه‌های بل طراحی شد و نخستین بار در ۱۹۷۶ ظاهر شد. آزمایشگاه‌های بل مسئول یونیکس (از ۱۹۶۹) و زبان C (۱۹۷۲) هم بودند.

تیم قابل‌توجهی از آن دوران بسیار پیش‌تر! فقط می‌توانیم فرض کنیم که با هم صحبت می‌کردند و ایده رد و بدل می‌کردند.

Tidyverse

اگر خوش‌بین باشیم، می‌توانیم بگوییم توابع رشته‌ی سبک قدیمی بهترین چیزی نیستند که R ارائه می‌دهد (دیدگاه‌های کم‌خوش‌بینانه‌تر هم وجود دارد).

برای بهبود این وضعیت، کتابخانه‌ی stringr از سال ۲۰۰۹ در حال توسعه بوده است و اکنون بخش کلیدی از مجموعه‌ی 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 به‌ترتیب پیش‌فرض‌شان اولین و آخرین کاراکتر هستند. می‌توانند بردارهایی از اندیس‌ها باشند. برخلاف اندیس‌گذاری بردار (و این تفاوت خوشایند است)، مقادیر منفی از انتهای رشته به عقب شمرده می‌شوند، به سبک پایتون.

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 یک بردار کاراکتری را به یک رشته‌ی واحد تبدیل می‌کند. آرگومان collapse بین هر عنصر قرار می‌گیرد و پیش‌فرضش "" است.

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

str_c چندین بردار کاراکتری را به یک بردار کاراکتری واحد تبدیل می‌کند و در صورت نیاز بازیافت انجام می‌دهد.

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 یک بردار کاراکتری و یک اندیس 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 یک بردار کاراکتری می‌گیرد و لیستی از بردارها را برمی‌گرداند. یک مفهوم لیست‌ها در سرفصل ما وجود دارد که خروجی را توضیح می‌دهد.

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

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

همچنین str_split_fixed وجود دارد که یک ماتریس برمی‌گرداند. ماتریس‌ها در یک مفهوم بعدی پوشش داده می‌شوند.

تطبیق رشته‌ها

برای تطبیق کل رشته، str_equal() می‌تواند از تعریف‌های مختلفی از «برابری» استفاده کند. ساده‌ترین حالت این است که پیش‌فرض به بزرگی و کوچکی حروف حساس است، اما می‌توان آن را تغییر داد. locale و یونیکد امکانات بسیار بیشتری ایجاد می‌کنند.

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

توابع دیگر بسیاری هستند که در مفهوم عبارت‌های باقاعده با جزئیات بیشتری بحث خواهند شد.

تغییر رشته‌ها

بعضی زبان‌ها (عمدتاً اروپایی) بین حروف بزرگ و کوچک تفاوت قائل می‌شوند. مجموعه‌ای از توابع stringr برای تبدیل این‌ها به یکدیگر، به شیوه‌های متناسب با locale، وجود دارد:

  • 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 می‌تواند حساسیت به بزرگی و کوچکی حروف و تطبیق‌های مختلف مخصوص locale را هم مدیریت کند.

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 تمرین دیگر را باز کنید