عبارتهای باقاعده روشی بسیار انعطافپذیر برای تطبیق الگو روی رشتهها هستند، با استفاده از یک زبان دامنهویژه (DSL) که برای همین کار طراحی شده است.
مانند بسیاری از زبانهای برنامهنویسی دیگر، R هیچ تلاشی برای پیادهسازی کتابخانهی عبارتهای باقاعدهی خود نمیکند. در عوض، موتور عبارتهای باقاعدهی ICU را در بر میگیرد. ICU مشابه کتابخانهی محبوب PCRE2 است که بیشتر زبانهای جدیدتر از آن استفاده میکنند، هرچند برخی موارد خاص متفاوت مدیریت میشوند.
بسیاری از قابلیتهای عبارتهای باقاعده در Base R تعبیه شدهاند و ممکن است نمونههایی از آنها را در کدهای قدیمیتر ببینید.
برای کدهای جدید، بهشدت توصیه میشود از کتابخانهی stringr استفاده کنید.
همانطور که در مفهوم رشتهها اشاره شد، stringr مستندات خوبی دارد که معمولاً خودِ نویسنده(ی) بسته آن را نوشته است.
این برنامهی درسی R فرض میکند که شما از قبل با نحوهی نگارش پایهی عبارتهای باقاعده آشنا هستید. ما تمرکز خود را تنها بر قابلیتهای مخصوص R میگذاریم.
چند منبع برای تازه کردن دانش عبارتهای باقاعدهی شما در ادامه فهرست شده است.
بسیاری از توابعی که پیشتر در مفهوم Strings بحث شدند، بهطور استاندارد برای جستوجوهای عبارت باقاعده طراحی شدهاند.
ما مفهوم رشتهها را نسبتاً ساده نگه داشتیم و فقط روی رشتههای ثابت تطبیق انجام دادیم، اما در عوض میتوان از هر الگوی عبارت باقاعدهای استفاده کرد.
بهطور رسمی، یک عبارت باقاعده در R با فراخوانی regex() ساخته میشود.
برای راحتی، توابع stringr هر رشتهی الگو را بهعنوان یک عبارت باقاعده در نظر میگیرند.
در مثال زیر، اولین str_replace_all() یک شکر نحوی برای نسخهی دوم است:
input <- "Some input string"
# "\\s" matches any whitespace character
input |> str_replace_all("\\s", "_")
[1] "Some_input_string"
input |> str_replace_all(regex("\\s"), "_")
[1] "Some_input_string"
برگهی تقلب راهنمای مفیدی است: همهی توابعی که آرگومان pattern دارند (با رنگ قرمز چاپ شدهاند) یک عبارت باقاعده میپذیرند و صفحهی ۲ خلاصهای از نحوهی نگارش عبارتهای باقاعده را ارائه میدهد.
توجه داشته باشید که تابع regex() همچنان میتواند مهم باشد اگر بخواهید مقادیر پیشفرض را بازنویسی کنید، مثلاً حساسیت به بزرگی و کوچکی حروف یا محیط زبانی.
# default is case sensitive
"A Mixed Assortment" |>
str_replace_all(regex("[aeiou]", ignore_case = TRUE), "*")
#> [1] "* M*x*d *ss*rtm*nt"
بسیاری از کدهای عبارت باقاعده شامل بکاسلش هستند، مانند \s که در بالا استفاده شد.
در رشتههای معمولی R، \ باید با دو برابر کردن escape شود، از این رو "\\s".
بکاسلشها میتوانند زیاد شوند و گیجکننده شوند، بنابراین در موارد پیچیده ممکن است استفاده از رشتههای خام کمک کند.
بهطور پیشفرض، نحوهی نگارش تعریف یک رشتهی خام r"( )" است، اما جداکنندههای دیگری هم مجازند.
از آنجا که هم پرانتزها ( ) و هم کروشهها [ ] در نحوهی نگارش عبارتهای باقاعده رایجاند (برای گروههای ثبت و گروههای گزینه)، آکولادها { } ممکن است انتخاب خوبی باشند.
input |> str_replace_all(r"{\s\w}", "__")
#> [1] "Some__nput__tring"
در ادامه چند پرسش و توابعی آمده است که میتوانند در پاسخ دادن به آنها کمک کنند.
برخی مثالها از fruit استفاده میکنند، یک بردار از ۸۰ اسم میوه که همراه stringr برای تمرین گنجانده شده است.
آیا یک رشته شامل یک تطبیق است؟
str_detect()، یا str_starts()، str_ends() تا نیازی به استفاده از لنگرهای ^ یا $ نباشد.
# berries begining with b
fruit |> str_detect("^b.*berry") |> head(15)
#> [1] FALSE FALSE FALSE FALSE FALSE TRUE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE FALSE
# alternative syntax
fruit |> str_starts("b.*berry") |> head(15)
#> [1] FALSE FALSE FALSE FALSE FALSE TRUE TRUE FALSE FALSE TRUE TRUE FALSE FALSE FALSE FALSE
آیا فقط رشتههای حاوی یک تطبیق بازگردانده شوند؟
از str_subset() برای گرفتن خود رشتهها و از str_which() برای گرفتن اندیسها استفاده کنید.
fruit |> str_subset("a...e")
[1] "apple" "blackberry" "mandarine" "nectarine" "pineapple" "pomegranate" "raspberry" "salal berry"
> fruit |> str_which("a...e")
[1] 1 7 48 51 62 64 70 73
چند تطبیق در هر رشته وجود دارد؟
str_count()
# count vowels
fruit |> str_count("[aeiou]") |> head(10)
#> [1] 2 3 4 3 3 2 2 3 5 3
تطبیق کجاست؟
str_view() برای استفادهی تعاملی، str_locate() یا str_locate_all() بهطور کلیتر.
# str_view only shows matching elements, with match in < > delimiters
fruit |> str_view("a...e")
[1] │ <apple>
[7] │ bl<ackbe>rry
[48] │ mand<arine>
[51] │ nect<arine>
[62] │ pine<apple>
[64] │ pomegr<anate>
[70] │ r<aspbe>rry
[73] │ sal<al be>rry
# str_locate returns all elements
fruit |> str_locate("a...e") |> head(10)
start end
[1,] 1 5
[2,] NA NA
[3,] NA NA
[4,] NA NA
[5,] NA NA
[6,] NA NA
[7,] 3 7
[8,] NA NA
[9,] NA NA
[10,] NA NA
معمولاً میخواهیم بدانیم که چه چیزی تطبیق میدهد. این کار با گنجاندن «گروههای ثبت» داخل پرانتز در عبارت باقاعده انجام میشود.
میتوان در همان عبارت باقاعده با استفاده از \1، \2 و غیره به ثبتها ارجاع داد.
برای مثال، یافتن جفتهای تکراری از حروف:
fruit |> str_subset("(..)\\1")
[1] "banana" "coconut" "cucumber" "jujube" "papaya" "salal berry"
با نحوهی نگارش مشابه، میتوان از ثبتها در رشتههای جایگزینی استفاده کرد:
# double to triple
fruit |>
str_subset("(..)\\1") |>
str_replace("(..)\\1", "\\1\\1\\1")
[1] "bananana" "cococonut" "cucucumber" "jujujube" "papapaya" "salalal berry"
برای پردازش بعدی، تطبیقها را با تابع str_match() فقط برای اولین تطبیق، یا با str_match_all() برای همهی تطبیقها جمعآوری کنید.
گروههای ثبت در هر زبانی تمایل دارند خروجی پیچیدهای تولید کنند.
رویکرد R (که بهعنوان یک چالش اضافه، برداریشده هم هست) این است که از str_match() یک ماتریس تولید کند، با یک سطر برای هر رشتهی ورودی، ستون ۱ شامل کل رشتهی تطبیقخورده و ستونهای باقیمانده شامل گروههای ثبت مختلف.
استفاده از گروههای نامدار میتواند مزیتی باشد، چون به اسم ستونها در ماتریس نتایج تبدیل میشوند.
recipe <- "dissolve 25g sugar in 200ml water"
recipe |> str_match("(\\d+g) .* (\\d+ml)")
[,1] [,2] [,3]
[1,] "25g sugar in 200ml" "25g" "200ml"
# use named capture groups
recipe |> str_match("(?<wt>\\d+g) .* (?<vol>\\d+ml)")
wt vol
[1,] "25g sugar in 200ml" "25g" "200ml"
در این برنامهی درسی زیاد دربارهی ماتریسها صحبت نکردهایم، هرچند یک سند مفهومی وجود دارد که میتوانید به آن دسترسی داشته باشید. ماتریسها با اکوسیستم Tidyverse چندان سازگار نیستند، با اینکه مدت زیادی بخشی از Base R بودهاند.
بهطور خلاصه: با ماتریسها مثل بردارهایی با ۲ اندیس رفتار کنید، به ترتیب [rows, cols].
هر کدام از اندیسها را خالی بگذارید تا کل سطر/ستون را بگیرید.
وقتی یک سطر تنها r را با m[r,] یا یک ستون تنها c را با m[, c] میگیرید، مقدار بازگرداندهشده بهطور پیشفرض به یک بردار تخت میشود.
> amounts <- recipe |> str_match("(?<wt>\\d+g) .* (?<vol>\\d+ml)")
# get all of row 1, as a named vector
> amt1 <- amounts[1,]
> amt1
wt vol
"25g sugar in 200ml" "25g" "200ml"
# get a vector element
amt1["wt"] |> unname()
[1] "25g"
بهناچار، str_match_all() خروجیای با ساختار حتی بیشتری تولید میکند:
[[1]] از لیست خارج کنید، همانطور که در مفهوم لیستها توضیح داده شده است.match().mix <- "Mix 200g of sugar, 180g of flour and 150g of butter"
# raw output
> str_match_all(mix, "(\\d+)g")
[[1]]
[,1] [,2]
[1,] "200g" "200"
[2,] "180g" "180"
[3,] "150g" "150"
# we just want the first list element
> str_match_all(mix, "(\\d+)g")[[1]]
[,1] [,2]
[1,] "200g" "200"
[2,] "180g" "180"
[3,] "150g" "150"
# captures are in column 2, so get that as a vector
> str_match_all(mix, "(\\d+)g")[[1]][,2]
[1] "200" "180" "150"
البته، تطبیقها میتوانند شکست بخورند.
در این صورت نتیجه مقدار ویژهی NA خواهد بود.
آماده باشید که این را بررسی کنید، همانطور که در مفهوم نیستی توضیح داده شده است.
# failed match
recipe |> str_match("(not here)")
[,1] [,2]
[1,] NA NA
R for Data science چنین میگوید:
str_match()یک ماتریس برمیگرداند، بنابراین کار کردن با آن چندان آسان نیست.
با این پانویس:
بیشتر به این دلیل که ما هرگز در این کتاب دربارهی ماتریسها صحبت نمیکنیم!
یکی از نویسندگان این کتاب، کتابخانهی stringr را هم نوشته است و بهطور ضمنی از کار خودش انتقاد میکند.
جالب خواهد بود ببینیم پیادهسازی این تابع در نسخههای آینده چگونه تکامل مییابد.