15  التعبيرات النمطية

15.1 مقدمة

في الفصل 14، تعلمت مجموعة كاملة من الدوال المفيدة للعمل مع النصوص السلسلية. سيركز هذا الفصل على الدوال التي تستخدم التعبيرات النمطية (Regular expressions)، وهي لغة موجزة وقوية لوصف الأنماط داخل النصوص. مصطلح “التعبير النمطي” طويل نوعاً ما، لذا يختصره معظم الناس إلى “regex”1 أو “regexp”.

يبدأ الفصل بأساسيات التعبيرات النمطية وأكثر دوال stringr فائدة لتحليل البيانات. سنقوم بعد ذلك بتوسيع معرفتك بالأنماط وتغطية سبعة مواضيع جديدة مهمة (الإفلات، الإرساء، فئات المحارف، الفئات المختصرة، المحددات الكمية، الأسبقية، والتجميع). بعد ذلك، سنتحدث عن بعض أنواع الأنماط الأخرى التي يمكن لدوال stringr العمل معها و”الأعلام” المختلفة التي تتيح لك تعديل عمل التعبيرات النمطية. سنختم باستعراض للأماكن الأخرى في tidyverse و R الأساسية حيث قد تستخدم التعبيرات النمطية.

15.1.1 المتطلبات المسبقة

في هذا الفصل، سنستخدم دوال التعبيرات النمطية من حزمتي stringr و tidyr، وكلاهما عضوين أساسيين في tidyverse، بالإضافة إلى بيانات من حزمة babynames.

خلال هذا الفصل، سنستخدم مزيجاً من الأمثلة المباشرة البسيطة للغاية حتى تتمكن من الحصول على الفكرة الأساسية، وبيانات أسماء الأطفال، وثلاثة متجهات حرفية من stringr:

  • fruit يحتوي على أسماء 80 فاكهة.
  • words يحتوي على 980 كلمة إنجليزية شائعة.
  • sentences يحتوي على 720 جملة قصيرة.

15.2 أساسيات الأنماط

سنستخدم str_view() لتعلم كيفية عمل أنماط التعبيرات النمطية. استخدمنا str_view() في الفصل الماضي لفهم النص السلسلي مقابل تمثيله المطبوع بشكل أفضل، والآن سنستخدمها مع الوسيط الثاني، وهو تعبير نمطي. عند توفير هذا الوسيط، ستعرض str_view() فقط عناصر المتجه النصي التي تتطابق، مع إحاطة كل تطابق بـ <>، وحيثما أمكن، إبراز التطابق باللون الأزرق.

تتكون أبسط الأنماط من أحرف وأرقام تطابق تلك المحارف تماماً:

str_view(fruit, "berry")
#>  [6] │ bil<berry>
#>  [7] │ black<berry>
#> [10] │ blue<berry>
#> [11] │ boysen<berry>
#> [19] │ cloud<berry>
#> [21] │ cran<berry>
#> ... and 8 more

تتطابق الأحرف والأرقام تماماً وتسمى المحارف الحرفية (literal characters). معظم علامات الترقيم، مثل .، +، *، [، ]، و ?، لها معانٍ خاصة2 وتسمى المحارف الفوقية (metacharacters). على سبيل المثال، . سوف يطابق أي محرف3، لذا فإن "a." سيتطابق مع أي نص يحتوي على “a” متبوعة بمحرف آخر:

str_view(c("a", "ab", "ae", "bd", "ea", "eab"), "a.")
#> [2] │ <ab>
#> [3] │ <ae>
#> [6] │ e<ab>

أو يمكننا العثور على جميع الفواكه التي تحتوي على “a”، متبوعة بثلاثة أحرف، متبوعة بـ “e”:

str_view(fruit, "a...e")
#>  [1] │ <apple>
#>  [7] │ bl<ackbe>rry
#> [48] │ mand<arine>
#> [51] │ nect<arine>
#> [62] │ pine<apple>
#> [64] │ pomegr<anate>
#> ... and 2 more

تتحكم المحددات الكمية (Quantifiers) في عدد المرات التي يمكن أن يتطابق فيها النمط:

  • ? يجعل النمط اختيارياً (أي يتطابق 0 أو 1 مرة)
  • + يتيح للنمط التكرار (أي يتطابق مرة واحدة على الأقل)
  • * يتيح للنمط أن يكون اختيارياً أو يتكرر (أي يتطابق أي عدد من المرات، بما في ذلك 0).
# يطابق النمط ab? الحرف "أ"، متبوعاً اختيارياً بالحرف "ب".
str_view(c("a", "ab", "abb"), "ab?")
#> [1] │ <a>
#> [2] │ <ab>
#> [3] │ <ab>b

# يطابق النمط ab+ الحرف "أ"، متبوعاً بحرف "ب" واحد على الأقل.
str_view(c("a", "ab", "abb"), "ab+")
#> [2] │ <ab>
#> [3] │ <abb>

# يطابق النمط ab* الحرف "أ"، متبوعاً بأي عدد من أحرف "ب".
str_view(c("a", "ab", "abb"), "ab*")
#> [1] │ <a>
#> [2] │ <ab>
#> [3] │ <abb>

يتم تعريف فئات المحارف (Character classes) بواسطة [] وتسمح لك بمطابقة مجموعة من المحارف، على سبيل المثال، [abcd] تطابق “a”، “b”، “c”، أو “d”. يمكنك أيضاً عكس التطابق عن طريق البدء بـ [^abcd]: ^ يطابق أي شيء باستثناء “a”، “b”، “c”، أو “d”. يمكننا استخدام هذه الفكرة للعثور على الكلمات التي تحتوي على “x” محاطة بحروف علة (vowels)، أو “y” محاطة بحروف ساكنة (consonants):

str_view(words, "[aeiou]x[aeiou]")
#> [284] │ <exa>ct
#> [285] │ <exa>mple
#> [288] │ <exe>rcise
#> [289] │ <exi>st
str_view(words, "[^aeiou]y[^aeiou]")
#> [836] │ <sys>tem
#> [901] │ <typ>e

يمكنك استخدام التبادل (Alternation)، |، للاختيار بين نمطين بديليين أو أكثر. على سبيل المثال، تبحث الأنماط التالية عن الفواكه التي تحتوي على “apple” أو “melon” أو “nut”، أو حرف علة متكرر.

str_view(fruit, "apple|melon|nut")
#>  [1] │ <apple>
#> [13] │ canary <melon>
#> [20] │ coco<nut>
#> [52] │ <nut>
#> [62] │ pine<apple>
#> [72] │ rock <melon>
#> ... and 1 more
str_view(fruit, "aa|ee|ii|oo|uu")
#>  [9] │ bl<oo>d orange
#> [33] │ g<oo>seberry
#> [47] │ lych<ee>
#> [66] │ purple mangost<ee>n

التعبيرات النمطية مدمجة للغاية وتستخدم الكثير من علامات الترقيم، لذا قد تبدو مربكة وصعبة القراءة في البداية. لا تقلق؛ ستتحسن مع الممارسة، وستصبح الأنماط البسيطة قريباً طبيعة ثانية بالنسبة لك. لنبدأ هذه العملية بالممارسة مع بعض دوال stringr المفيدة.

15.3 الدوال الرئيسية

الآن بعد أن أتقنت أساسيات التعبيرات النمطية، لنستخدمها مع بعض دوال stringr و tidyr. في القسم التالي، ستتعلم كيفية الكشف عن وجود التطابق أو غيابه، وكيفية حساب عدد التطابقات، وكيفية استبدال التطابق بنص ثابت، وكيفية استخراج النص باستخدام نمط معين.

15.3.1 الكشف عن التطابقات (Detect matches)

str_detect() ترجع متجهاً منطقياً يكون TRUE إذا كان النمط يتطابق مع عنصر من المتجه الحرفي و FALSE بخلاف ذلك:

str_detect(c("a", "b", "c"), "[aeiou]")
#> [1]  TRUE FALSE FALSE

بما أن str_detect() ترجع متجهاً منطقياً بنفس طول المتجه الأولي، فإنها تتكامل بشكل ممتاز مع filter(). على سبيل المثال، يجد هذا الكود جميع الأسماء الأكثر شعبية التي تحتوي على حرف “x” صغير:

babynames |> 
  filter(str_detect(name, "x")) |> 
  count(name, wt = n, sort = TRUE)
#> # A tibble: 974 × 2
#>   name           n
#>   <chr>      <int>
#> 1 Alexander 665492
#> 2 Alexis    399551
#> 3 Alex      278705
#> 4 Alexandra 232223
#> 5 Max       148787
#> 6 Alexa     123032
#> # ℹ 968 more rows

يمكننا أيضاً استخدام str_detect() مع summarize() عن طريق دمجها مع sum() أو mean(): sum(str_detect(x, pattern)) تخبرك بعدد المشاهدات التي تتطابق، و mean(str_detect(x, pattern)) تخبرك بالنسبة المئوية للمشاهدات التي تتطابق. على سبيل المثال، المقتطع البرمجي التالي يحسب ويصور نسبة أسماء الأطفال4 التي تحتوي على “x”، مقسمة حسب السنة. يبدو أنها زادت بشكل كبير في الشعبية مؤخراً!

babynames |> 
  group_by(year) |> 
  summarize(prop_x = mean(str_detect(name, "x"))) |> 
  ggplot(aes(x = year, y = prop_x)) + 
  geom_line()

سلسلة زمنية تعرض نسبة أسماء الأطفال التي تحتوي على الحرف x. تنخفض النسبة تدريجياً من 8 لكل 1000 في عام 1880 إلى 4 لكل 1000 في عام 1980، ثم تزداد بسرعة إلى 16 لكل 1000 في عام 2019.

هناك دالتان ترتبطان ارتباطاً وثيقاً بالدالة str_detect()، وهما: str_subset() و str_which(). تعيد الدالة str_subset() متجهاً حرفياً (character vector) يحتوي فقط على السلاسل النصية التي تتطابق مع النمط. بينما تعيد الدالة str_which() متجهاً صحيحاً (integer vector) يعطي مواضع (positions) السلاسل النصية التي تتطابق مع النمط.

15.3.2 عد التطابقات (Count matches)

الخطوة التالية في التعقيد بعد str_detect() هي str_count(): بدلاً من إعطاء صح أو خطأ، تخبرك بعدد التطابقات الموجودة في كل نص سلسلي.

x <- c("apple", "banana", "pear")
str_count(x, "p")
#> [1] 2 0 1

لاحظ أن كل تطابق يبدأ عند نهاية التطابق السابق، أي أن تطابقات التعبيرات النمطية لا تتداخل أبداً. على سبيل المثال، في "abababa"، كم مرة سيتطابق النمط "aba"؟ تقول التعبيرات النمطية مرتين، وليس ثلاث مرات:

str_count("abababa", "aba")
#> [1] 2
str_view("abababa", "aba")
#> [1] │ <aba>b<aba>

من الطبيعي استخدام str_count() مع mutate(). يستخدم المثال التالي str_count() مع فئات المحارف لعد عدد حروف العلة والحروف الساكنة في كل اسم.

babynames |> 
  count(name) |> 
  mutate(
    vowels = str_count(name, "[aeiou]"),
    consonants = str_count(name, "[^aeiou]")
  )
#> # A tibble: 97,310 × 4
#>   name          n vowels consonants
#>   <chr>     <int>  <int>      <int>
#> 1 Aaban        10      2          3
#> 2 Aabha         5      2          3
#> 3 Aabid         2      2          3
#> 4 Aabir         1      2          3
#> 5 Aabriella     5      4          5
#> 6 Aada          1      2          2
#> # ℹ 97,304 more rows

إذا نظرت عن كثب، ستلاحظ أن هناك شيئاً غير دقيق في حساباتنا: اسم “Aaban” يحتوي على ثلاثة أحرف “a”، لكن ملخصنا يذكر وجود حرفي علة فقط. وذلك لأن التعبيرات النمطية حساسة لحالة الأحرف (case sensitive). هناك ثلاث طرق يمكننا من خلالها إصلاح ذلك:

  • إضافة حروف العلة الكبيرة إلى فئة المحارف: str_count(name, "[aeiouAEIOU]").
  • توجيه التعبير النمطي لتجاهل حالة الأحرف: str_count(name, regex("[aeiou]", ignore_case = TRUE)). سنتحدث عن هذا المفهوم أكثر في قسم 15.5.1.
  • استخدام str_to_lower() لتحويل الأسماء إلى حالة الأحرف الصغيرة: str_count(str_to_lower(name), "[aeiou]").

هذا التنوع في الأساليب نموذجي جداً عند العمل مع النصوص السلسلية — غالباً ما تكون هناك طرق متعددة للوصول إلى هدفك، إما بجعل نمطك أكثر تعقيداً أو عن طريق إجراء بعض المعالجة الأولية على النص. إذا علقت أثناء تجربة نهج معين، فقد يكون من المفيد تغيير المسار والتعامل مع المشكلة من منظور مختلف.

في هذه الحالة، وبما أننا نطبق دالتين على الاسم، أعتقد أنه من الأسهل تحويله أولاً:

babynames |> 
  count(name) |> 
  mutate(
    name = str_to_lower(name),
    vowels = str_count(name, "[aeiou]"),
    consonants = str_count(name, "[^aeiou]")
  )
#> # A tibble: 97,310 × 4
#>   name          n vowels consonants
#>   <chr>     <int>  <int>      <int>
#> 1 aaban        10      3          2
#> 2 aabha         5      3          2
#> 3 aabid         2      3          2
#> 4 aabir         1      3          2
#> 5 aabriella     5      5          4
#> 6 aada          1      3          1
#> # ℹ 97,304 more rows

15.3.3 استبدال القيم (Replace values)

بالإضافة إلى الكشف عن التطابقات وعدّها، يمكننا أيضاً تعديلها باستخدام str_replace() و str_replace_all(). تقوم str_replace() باستبدال التطابق الأول فقط، وكما يوحي الاسم، تقوم str_replace_all() باستبدال جميع التطابقات.

x <- c("apple", "pear", "banana")
str_replace_all(x, "[aeiou]", "-")
#> [1] "-ppl-"  "p--r"   "b-n-n-"

تعد str_remove() و str_remove_all() اختصارات مفيدة لـ str_replace(x, pattern, ""):

x <- c("apple", "pear", "banana")
str_remove_all(x, "[aeiou]")
#> [1] "ppl" "pr"  "bnn"

تقترن هذه الدوال بشكل طبيعي مع mutate() عند تنظيف البيانات، وغالباً ما تطبقها بشكل متكرر لإزالة طبقات التنسيق غير المتناسقة.

15.3.4 استخراج المتغيرات

آخر دالة سنناقشها تستخدم التعبيرات النمطية لاستخراج البيانات من عمود واحد إلى عمود جديد أو أكثر: separate_wider_regex(). إنها قرينة لدالتي separate_wider_position() و separate_wider_delim() اللتين تعلمتهما في قسم 14.4.2. توجد هذه الدوال في حزمة tidyr لأنها تعمل على (أعمدة) أطر البيانات، بدلاً من المتجهات الفردية.

لِنَقُمْ بِإِنْشَاءِ مجموعة بيانات بسيطة لتوضيح كيفية عملها. هنا لدينا بعض البيانات المشتقة من babynames حيث لدينا الاسم والجنس والعمر لمجموعة من الأشخاص بتنسيق غريب نوعاً ما5:

df <- tribble(
  ~str,
  "<Sheryl>-F_34",
  "<Kisha>-F_45",
  "<Brandon>-N_33",
  "<Sharon>-F_38",
  "<Penny>-F_58",
  "<Justin>-M_41",
  "<Patricia>-F_84",
)

لاستخراج هذه البيانات باستخدام separate_wider_regex() نحتاج فقط إلى بناء تسلسل من التعبيرات النمطية التي تطابق كل جزء. إذا أردنا أن تظهر محتويات هذا الجزء في المخرجات، فإننا نمنحه اسماً:

df |> 
  separate_wider_regex(
    str,
    patterns = c(
      "<", 
      name = "[A-Za-z]+", 
      ">-", 
      gender = ".",
      "_",
      age = "[0-9]+"
    )
  )
#> # A tibble: 7 × 3
#>   name    gender age  
#>   <chr>   <chr>  <chr>
#> 1 Sheryl  F      34   
#> 2 Kisha   F      45   
#> 3 Brandon N      33   
#> 4 Sharon  F      38   
#> 5 Penny   F      58   
#> 6 Justin  M      41   
#> # ℹ 1 more row

إذا فشل التطابق، يمكنك استخدام too_few = "debug" لمعرفة الخطأ الذي حدث، تماماً مثل separate_wider_delim() و separate_wider_position().

15.3.5 تمارين

  1. ما هو اسم الطفل الذي يحتوي على أكبر عدد من حروف العلة؟ ما هو الاسم الذي يحتوي على أعلى نسبة من حروف العلة؟ (تلميح: ما هو المقام؟)

  2. استبدل جميع الشرطات المائلة للأمام (forward slashes) في "a/b/c/d/e" بشرطات مائلة للخلف (backslashes). ماذا يحدث إذا حاولت التراجع عن التحويل عن طريق استبدال جميع الشرطات المائلة للخلف بشرطات مائلة للأمام؟ (سنناقش هذه المشكلة قريباً جداً).

  3. قم بتنفيذ نسخة بسيطة من str_to_lower() باستخدام str_replace_all().

  4. أنشئ تعبيراً نمطياً يطابق أرقام الهواتف كما هي مكتوبة عادةً في بلدك.

15.4 تفاصيل الأنماط

الآن بعد أن أصبحت تفهم أساسيات لغة الأنماط وكيفية استخدامها مع بعض دوال stringr و tidyr، حان الوقت للتعمق في المزيد من التفاصيل. أولاً، سنبدأ بـ الإفلات (escaping)، والذي يتيح لك مطابقة المحارف الفوقية (metacharacters) التي يتم التعامل معها بشكل خاص في الحالات العادية. بعد ذلك، ستتعلم عن المراسي (anchors) التي تتيح لك مطابقة بداية النص السلسلي أو نهايته. ثم ستتعلم المزيد عن فئات المحارف (character classes) واختصاراتها التي تسمح لك بمطابقة أي محرف من مجموعة معينة. بعد ذلك، ستتعلم التفاصيل النهائية لـ المحددات الكمية (quantifiers) التي تتحكم في عدد المرات التي يمكن أن يتطابق فيها النمط. ثم يتعين علينا تغطية موضوع مهم (ولكنه معقد) وهو أسبقية العمليات (operator precedence) والأقواس. وسنختم ببعض تفاصيل التجميع (grouping) لمكونات النمط.

المصطلحات التي نستخدمها هنا هي الأسماء التقنية لكل مكون. قد لا تكون دائماً أكثر المصطلحات إيحاءً بغرضها، ولكن من المفيد جداً معرفة المصطلحات الصحيحة إذا أردت لاحقاً البحث في جوجل عن مزيد من التفاصيل.

15.4.1 الإفلات

من أجل مطابقة محرف النقطة الحرفي .، فأنت بحاجة إلى الإفلات (escape) الذي يوجه التعبير النمطي لمطابقة المحارف الفوقية6 بشكل حرفي. مثل النصوص السلسلية، تستخدم التعبيرات النمطية الشرطة المائلة للخلف للإفلات. لذا، لمطابقة .، فأنت بحاجة إلى التعبير النمطي \.. للأسف هذا يخلق مشكلة؛ فنحن نستخدم النصوص السلسلية لتمثيل التعبيرات النمطية، و \ تُستخدم أيضاً كرمز إفلات في النصوص السلسلية. لذا لإنشاء التعبير النمطي \. نحتاج إلى النص السلسلي "\\."، كما يوضح المثال التالي:

# لإنشاء التعبير النمطي .\، نحتاج إلى استخدام \\.
dot <- "\\."

# لكن التعبير نفسه يحتوي على شرطة مائلة واحدة فقط \
str_view(dot)
#> [1] │ \.

# وهذا يوجه R للبحث عن نقطة صريحة .
str_view(c("abc", "a.c", "bef"), "a\\.c")
#> [2] │ <a.c>

في هذا الكتاب، سنكتب التعبيرات النمطية عادةً بدون علامات تنصيص، مثل \.. إذا احتجنا إلى التأكيد على ما ستكتبه بالفعل، فسنحيطه بعلامات تنصيص ونضيف إفلاتات إضافية، مثل "\\.".

إذا كانت \ تُستخدم كرمز إفلات في التعبيرات النمطية، فكيف تطابق الشرطة المائلة للخلف الحرفية \؟ حسناً، تحتاج إلى إفلاتها لإنشاء التعبير النمطي \\. ولإنشاء هذا التعبير النمطي، تحتاج إلى استخدام نص سلسلي يحتاج هو الآخر إلى إفلات \. هذا يعني أنه لمطابقة \ حرفية واحدة تحتاج إلى كتابة "\\\\" — أي أربع شرطات مائلة للخلف لمطابقة واحدة فقط!

x <- "a\\b"
str_view(x)
#> [1] │ a\b
str_view(x, "\\\\")
#> [1] │ a<\>b

بدلاً من ذلك، قد تجد أنه من الأسهل استخدام النصوص الخام (raw strings) التي تعلمتها في قسم 14.2.2. يتيح لك ذلك تجنب طبقة واحدة من الإفلات:

str_view(x, r"{\\}")
#> [1] │ a<\>b

إذا كنت تحاول مطابقة القيم الحرفية لـ .، $، |، *، +، ?، {, }، (, )، فهناك بديل لاستخدام الإفلات بالشرطة المائلة للخلف: يمكنك استخدام فئة المحارف: [.]، [$]، [|]، … كلها تطابق القيم الحرفية.

str_view(c("abc", "a.c", "a*c", "a c"), "a[.]c")
#> [2] │ <a.c>
str_view(c("abc", "a.c", "a*c", "a c"), ".[*]c")
#> [3] │ <a*c>

15.4.2 المراسي (Anchors)

بشكل افتراضي، ستطابق التعبيرات النمطية أي جزء من النص السلسلي. إذا كنت تريد المطابقة عند البداية أو النهاية، فأنت بحاجة إلى إرساء (anchor) التعبير النمطي باستخدام ^ لمطابقة البداية أو $ لمطابقة النهاية:

str_view(fruit, "^a")
#> [1] │ <a>pple
#> [2] │ <a>pricot
#> [3] │ <a>vocado
str_view(fruit, "a$")
#>  [4] │ banan<a>
#> [15] │ cherimoy<a>
#> [30] │ feijo<a>
#> [36] │ guav<a>
#> [56] │ papay<a>
#> [74] │ satsum<a>

من المغري الاعتقاد بأن $ يجب أن تطابق بداية النص السلسلي، لأن هذه هي الطريقة التي نكتب بها المبالغ بالدولار، ولكن ليس هذا ما تطلبه التعبيرات النمطية.

لإجبار التعبير النمطي على مطابقة النص السلسلي الكامل فقط، قم بإرسائه باستخدام كل من ^ و $:

str_view(fruit, "apple")
#>  [1] │ <apple>
#> [62] │ pine<apple>
str_view(fruit, "^apple$")
#> [1] │ <apple>

يمكنك أيضاً مطابقة الحدود بين الكلمات (أي بداية الكلمة أو نهايتها) باستخدام \b. يمكن أن يكون هذا مفيداً بشكل خاص عند استخدام أداة البحث والاستبدال في RStudio. على سبيل المثال، للبحث عن جميع استخدامات sum()، يمكنك البحث عن \bsum\b لتجنب مطابقة summarize و summary و rowsum وما إلى ذلك:

x <- c("summary(x)", "summarize(df)", "rowsum(x)", "sum(x)")
str_view(x, "sum")
#> [1] │ <sum>mary(x)
#> [2] │ <sum>marize(df)
#> [3] │ row<sum>(x)
#> [4] │ <sum>(x)
str_view(x, "\\bsum\\b")
#> [4] │ <sum>(x)

عند استخدامها بمفردها، تنتج المراسي تطابقاً بعرض صفر (zero-width match):

str_view("abc", c("$", "^", "\\b"))
#> [1] │ abc<>
#> [2] │ <>abc
#> [3] │ <>abc<>

يساعدك هذا في فهم ما يحدث عند استبدال مرساة قائمة بذاتها:

str_replace_all("abc", c("$", "^", "\\b"), "--")
#> [1] "abc--"   "--abc"   "--abc--"

15.4.3 فئات المحارف (Character classes)

تسمح لك فئة المحارف (character class)، أو مجموعة المحارف، بمطابقة أي محرف ضمن مجموعة. كما ناقشنا أعلاه، يمكنك بناء مجموعاتك الخاصة باستخدام []، حيث [abc] تطابق “a”، أو “b”، أو “c”، و [^abc] تطابق أي محرف باستثناء “a”، أو “b”، أو “c”. بصرف النظر عن ^ هناك محرفان آخران لهما معنى خاص داخل []:

  • - يحدد نطاقاً، على سبيل المثال، [a-z] تطابق أي حرف صغير و [0-9] تطابق أي رقم.
  • \ يفلِت المحارف الخاصة، لذا فإن [\^\-\]] تطابق ^، أو -، أو ].

إليك بضع أمثلة:

x <- "abcd ABCD 12345 -!@#%."
str_view(x, "[abc]+")
#> [1] │ <abc>d ABCD 12345 -!@#%.
str_view(x, "[a-z]+")
#> [1] │ <abcd> ABCD 12345 -!@#%.
str_view(x, "[^a-z0-9]+")
#> [1] │ abcd< ABCD >12345< -!@#%.>

# تحتاج إلى إفلات لمطابقة المحارف التي تكون ذات معنى خاص داخل []
str_view("a-b-c", "[a-c]")
#> [1] │ <a>-<b>-<c>
str_view("a-b-c", "[a\\-c]")
#> [1] │ <a><->b<-><c>

تُستخدم بعض فئات المحارف بشكل شائع لدرجة أنها تحصل على اختصار خاص بها. لقد رأيت بالفعل .، والتي تطابق أي محرف باستثناء السطر الجديد. هناك ثلاثة أزواج أخرى مفيدة بشكل خاص7:

  • \d تطابق أي رقم؛
    \D تطابق أي شيء ليس رقماً.
  • \s تطابق أي مسافة خالية (مثل المسافة، علامة التبويب tab، السطر الجديد)؛
    \S تطابق أي شيء ليس مسافة خالية.
  • \w تطابق أي محرف “كلمة”، أي الأحرف والأرقام؛
    \W تطابق أي محرف “غير كلمة”.

يعرض الكود التالي الاختصارات الستة مع مجموعة مختارة من الأحرف والأرقام وعلامات الترقيم:

x <- "abcd ABCD 12345 -!@#%."
str_view(x, "\\d+")
#> [1] │ abcd ABCD <12345> -!@#%.
str_view(x, "\\D+")
#> [1] │ <abcd ABCD >12345< -!@#%.>
str_view(x, "\\s+")
#> [1] │ abcd< >ABCD< >12345< >-!@#%.
str_view(x, "\\S+")
#> [1] │ <abcd> <ABCD> <12345> <-!@#%.>
str_view(x, "\\w+")
#> [1] │ <abcd> <ABCD> <12345> -!@#%.
str_view(x, "\\W+")
#> [1] │ abcd< >ABCD< >12345< -!@#%.>

15.4.4 المحددات الكمية

تتحكم المحددات الكمية (Quantifiers) في عدد المرات التي يتطابق فيها النمط. في قسم 15.2 تعلمت عن ? (0 أو 1 تطابق)، و + (1 أو أكثر من التطابقات)، و * (0 أو أكثر من التطابقات). على سبيل المثال، colou?r ستطابق الهجاء الأمريكي أو البريطاني، و \d+ ستطابق رقماً واحداً أو أكثر، و \s? ستطابق اختيارياً مسافة خالية واحدة. يمكنك أيضاً تحديد عدد التطابقات بدقة باستخدام {}:

  • {n} تطابق n من المرات بالضبط.
  • {n,} تطابق n من المرات على الأقل.
  • {n,m} تطابق ما بين n و m من المرات.

15.4.5 أسبقية العمليات والأقواس

ما الذي يطابقه ab+؟ هل يطابق “a” متبوعة بـ “b” واحدة أو أكثر، أم يطابق “ab” مكررة أي عدد من المرات؟ ما الذي يطابقه ^a|b$؟ هل يطابق النص السلسلي الكامل a أو النص السلسلي الكامل b، أم يطابق نصاً يبتدئ بـ a أو نصاً ينتهي بـ b؟

الإجابة على هذه الأسئلة تتحدد بواسطة أسبقية العمليات، مثل قواعد ترتيب العمليات الحسابية التي ربما تعلمتها في المدرسة. أنت تعلم أن a + b * c تكافئ a + (b * c) وليس (a + b) * c لأن لـ * أسبقية أعلى ولـ + أسبقية أقل: فأنت تحسب * قبل +.

بالمثل، فإن للتعبيرات النمطية قواعد الأسبقية الخاصة بها: للمحددات الكمية أسبقية عالية وللتبادل أسبقية منخفضة، مما يعني أن ab+ تكافئ a(b+)، وأن ^a|b$ تكافئ (^a)|(b$). تماماً كما هو الحال مع الجبر، يمكنك استخدام الأقواس لتجاوز الترتيب المعتاد. ولكن على عكس الجبر، فمن غير المرجح أن تتذكر قواعد الأسبقية للتعبيرات النمطية، لذا لا تتردد في استخدام الأقواس بحرية.

15.4.6 التجميع والالتقاط (Grouping and capturing)

بالإضافة إلى تجاوز أسبقية العمليات، للأقواس تأثير مهم آخر: فهي تنشئ مجموعات التقاط (capturing groups) تتيح لك استخدام المكونات الفرعية للتطابق.

الطريقة الأولى لاستخدام مجموعة الالتقاط هي الإشارة إليها مجدداً داخل التطابق باستخدام المرجع الخلفي (back reference): تشير \1 إلى التطابق المحتوى في القوس الأول، و \2 في القوس الثاني، وهكذا. على سبيل المثال، يجد النمط التالي جميع الفواكه التي تحتوي على زوج مكرر من الأحرف:

str_view(fruit, "(..)\\1")
#>  [4] │ b<anan>a
#> [20] │ <coco>nut
#> [22] │ <cucu>mber
#> [41] │ <juju>be
#> [56] │ <papa>ya
#> [73] │ s<alal> berry

وهذا المثال يجد جميع الكلمات التي تبدأ وتنتهي بنفس الزوج من الأحرف:

str_view(words, "^(..).*\\1$")
#> [152] │ <church>
#> [217] │ <decide>
#> [617] │ <photograph>
#> [699] │ <require>
#> [739] │ <sense>

يمكنك أيضاً استخدام المراجع الخلفية في str_replace(). على سبيل المثال، هذا الكود يبدل ترتيب الكلمتين الثانية والثالثة في sentences:

sentences |> 
  str_replace("(\\w+) (\\w+) (\\w+)", "\\1 \\3 \\2") |> 
  str_view()
#> [1] │ The canoe birch slid on the smooth planks.
#> [2] │ Glue sheet the to the dark blue background.
#> [3] │ It's to easy tell the depth of a well.
#> [4] │ These a days chicken leg is a rare dish.
#> [5] │ Rice often is served in round bowls.
#> [6] │ The of juice lemons makes fine punch.
#> ... and 714 more

إذا كنت تريد استخراج التطابقات لكل مجموعة، يمكنك استخدام str_match(). لكن str_match() ترجع مصفوفة (matrix)، لذا ليس من السهل التعامل معها8:

sentences |> 
  str_match("the (\\w+) (\\w+)") |> 
  head()
#>      [,1]                [,2]     [,3]    
#> [1,] "the smooth planks" "smooth" "planks"
#> [2,] "the sheet to"      "sheet"  "to"    
#> [3,] "the depth of"      "depth"  "of"    
#> [4,] NA                  NA       NA      
#> [5,] NA                  NA       NA      
#> [6,] NA                  NA       NA

يمكنك تحويلها إلى tibble وتسمية الأعمدة:

sentences |> 
  str_match("the (\\w+) (\\w+)") |> 
  as_tibble(.name_repair = "minimal") |> 
  set_names("match", "word1", "word2")
#> # A tibble: 720 × 3
#>   match             word1  word2 
#>   <chr>             <chr>  <chr> 
#> 1 the smooth planks smooth planks
#> 2 the sheet to      sheet  to    
#> 3 the depth of      depth  of    
#> 4 <NA>              <NA>   <NA>  
#> 5 <NA>              <NA>   <NA>  
#> 6 <NA>              <NA>   <NA>  
#> # ℹ 714 more rows

لكنك بذلك أعدت عملياً إنشاء نسختك الخاصة من separate_wider_regex(). في الواقع، خلف الكواليس، تحول separate_wider_regex() متجه الأنماط الخاص بك إلى تعبير نمطي واحد يستخدم التجميع لالتقاط المكونات المسمات.

في بعض الأحيان، ستلاحظ أنك تريد استخدام الأقواس دون إنشاء مجموعات مطابقة. يمكنك إنشاء مجموعة غير ملتقطة باستخدام (?:).

x <- c("a gray cat", "a grey dog")
str_match(x, "gr(e|a)y")
#>      [,1]   [,2]
#> [1,] "gray" "a" 
#> [2,] "grey" "e"
str_match(x, "gr(?:e|a)y")
#>      [,1]  
#> [1,] "gray"
#> [2,] "grey"

15.4.7 تمارين

  1. كيف تطابق النص الحرفي "'\؟ وماذا عن "$^$"؟

  2. اشرح سبب عدم مطابقة كل من الأنماط التالية لـ \: "\" و "\\" و "\\\".

  3. بالنظر إلى متن الكلمات الشائعة في stringr::words، أنشئ تعبيرات نمطية تجد جميع الكلمات التي:

    أ. تبدأ بحرف “y”.
    ب. لا تبدأ بحرف “y”.
    ج. تنتهي بحرف “x”.
    د. تتكون من ثلاثة أحرف بالضبط. (لا تغش باستخدام الدالة str_length()!).
    هـ. تحتوي على سبعة أحرف أو أكثر.
    و. تحتوي على زوج من (حرف متحرك - حرف ساكن).
    ز. تحتوي على زوجين متتاليين على الأقل من (حرف متحرك - حرف ساكن).
    ح. تتكون فقط من أزواج متكررة من (حرف متحرك - حرف ساكن).

  4. أنشئ 11 تعبيراً نمطياً تطابق الهجاء البريطاني أو الأمريكي لكل من الكلمات التالية: airplane/aeroplane, aluminum/aluminium, analog/analogue, ass/arse, center/centre, defense/defence, donut/doughnut, gray/grey, modeling/modelling, skeptic/sceptic, summarize/summarise. حاول إنشاء أقصر تعبير نمطي ممكن!

  5. قم بتبديل الحرفين الأول والأخير في words. أي من تلك النصوص السلسلية الناشئة ما زال يتواجد ضمن قائمة words؟

  6. صف بالكلمات ما تطابقه هذه التعبيرات النمطية: (اقرأ بعناية لمعرفة ما إذا كان كل إدخال تعبيراً نمطياً أم نصاً سلسلياً يحدد تعبيراً نمطياً.)

    1. ^.*$
    2. "\\{.+\\}"
    3. \d{4}-\d{2}-\d{2}
    4. "\\\\{4}"
    5. \..\..\..
    6. (.)\1\1
    7. "(..)\\1"
  7. قم بحل ألغاز التعبيرات النمطية للمبتدئين على https://regexcrossword.com/challenges/beginner.

15.5 التحكم في الأنماط (Pattern control)

من الممكن ممارسة تحكم إضافي في تفاصيل التطابق باستخدام كائن نمط (pattern object) بدلاً من مجرد نص سلسلي عادي. يتيح لك ذلك التحكم فيما يُعرف بـ “أعلام التعبيرات النمطية” (regex flags) ومطابقة أنواع مختلفة من النصوص الثابتة، كما هو موضح أدناه.

15.5.1 أعلام التعبيرات النمطية

هناك عدد من الإعدادات التي يمكن استخدامها للتحكم في تفاصيل التعبير النمطي. غالباً ما تُسمى هذه الإعدادات أعلاماً (flags) في لغات البرمجة الأخرى. في stringr، يمكنك استخدامها عن طريق تغليف النمط في استدعاء لـ regex(). ربما يكون العلم الأكثر فائدة هو ignore_case = TRUE لأنه يتيح للأحرف مطابقة إما صيغتها الكبيرة أو الصغيرة:

bananas <- c("banana", "Banana", "BANANA")
str_view(bananas, "banana")
#> [1] │ <banana>
str_view(bananas, regex("banana", ignore_case = TRUE))
#> [1] │ <banana>
#> [2] │ <Banana>
#> [3] │ <BANANA>

إذا كنت تقوم بالكثير من العمل مع النصوص متعددة الأسطر (أي النصوص التي تحتوي على \n)، فقد يكون dotall و multiline مفيدين أيضاً:

  • dotall = TRUE يتيح لـ . مطابقة كل شيء، بما في ذلك \n:
x <- "Line 1\nLine 2\nLine 3"
str_view(x, ".Line")
#> ✖ Empty `string` provided.
str_view(x, regex(".Line", dotall = TRUE))
#> [1] │ Line 1<
#>     │ Line> 2<
#>     │ Line> 3
  • multiline = TRUE يجعل ^ و $ تطابقان بداية ونهاية كل سطر بدلاً من بداية ونهاية النص السلسلي الكامل:
x <- "Line 1\nLine 2\nLine 3"
str_view(x, "^Line")
#> [1] │ <Line> 1
#>     │ Line 2
#>     │ Line 3
str_view(x, regex("^Line", multiline = TRUE))
#> [1] │ <Line> 1
#>     │ <Line> 2
#>     │ <Line> 3

أخيراً، إذا كنت تكتب تعبيراً نمطياً معقداً وتخشى ألا تفهمه في المستقبل، فقد تجرب comments = TRUE. إنه يعدل لغة الأنماط لتجاهل المسافات والأسطر الجديدة، بالإضافة إلى كل شيء بعد #. يتيح لك هذا استخدام التعليقات والمسافات الخالية لجعل التعبيرات النمطية المعقدة أكثر قابلية للفهم9، كما في المثال التالي:

phone <- regex(
  r"(
    \(?     # optional opening parens
    (\d{3}) # area code
    [)\-]?  # optional closing parens or dash
    \ ?     # optional space
    (\d{3}) # another three numbers
    [\ -]?  # optional space or dash
    (\d{4}) # four more numbers
  )", 
  comments = TRUE
)

str_extract(c("514-791-8141", "(123) 456 7890", "123456"), phone)
#> [1] "514-791-8141"   "(123) 456 7890" NA

إذا كنت تستخدم التعليقات وتريد مطابقة مسافة أو سطر جديد أو #، فستحتاج إلى إفلاته باستخدام \.

15.5.2 التطابقات الثابتة (Fixed matches)

يمكنك إلغاء الاشتراك في قواعد التعبيرات النمطية باستخدام fixed():

str_view(c("", "a", "."), fixed("."))
#> [3] │ <.>

تمنحك fixed() أيضاً القدرة على تجاهل حالة الأحرف:

str_view("x X", "X")
#> [1] │ x <X>
str_view("x X", fixed("X", ignore_case = TRUE))
#> [1] │ <x> <X>

إذا كنت تعمل مع نص بلغة غير الإنجليزية، فمن المحتمل أن ترغب في استخدام coll() بدلاً من fixed()، لأنها تطبق القواعد الكاملة لتكبير الأحرف كما هي مستخدمة في الإعدادات المحلية locale التي تحددها. انظر قسم 14.6 لمزيد من التفاصيل حول الإعدادات المحلية.

str_view("i İ ı I", fixed("İ", ignore_case = TRUE))
#> [1] │ i <İ> ı I
str_view("i İ ı I", coll("İ", ignore_case = TRUE, locale = "tr"))
#> [1] │ <i> <İ> ı I

15.6 ممارسة عملية (Practice)

لوضع هذه الأفكار موضع التنفيذ، سنحل بضع مشكلات شبه واقعية بعد ذلك. سنناقش ثلاث تقنيات عامة:

  1. التحقق من عملك عن طريق إنشاء عناصر تحكم إيجابية وسلبية بسيطة.
  2. دمج التعبيرات النمطية مع الجبر البولياني (Boolean algebra).
  3. إنشاء أنماط معقدة باستخدام معالجة النصوص السلسلية.

15.6.1 تحقق من عملك

أولاً، لنجد جميع الجمل التي تبدأ بـ “The”. استخدام مرساة ^ بمفردها ليس كافياً:

str_view(sentences, "^The")
#>  [1] │ <The> birch canoe slid on the smooth planks.
#>  [4] │ <The>se days a chicken leg is a rare dish.
#>  [6] │ <The> juice of lemons makes fine punch.
#>  [7] │ <The> box was thrown beside the parked truck.
#>  [8] │ <The> hogs were fed chopped corn and garbage.
#> [11] │ <The> boy was there when the sun rose.
#> ... and 271 more

لأن هذا النمط يطابق أيضاً الجمل التي تبدأ بكلمات مثل They أو These. نحتاج إلى التأكد من أن الحرف “e” هو آخر حرف في الكلمة، وهو ما يمكننا القيام به عن طريق إضافة حد كلمة (word boundary):

str_view(sentences, "^The\\b")
#>  [1] │ <The> birch canoe slid on the smooth planks.
#>  [6] │ <The> juice of lemons makes fine punch.
#>  [7] │ <The> box was thrown beside the parked truck.
#>  [8] │ <The> hogs were fed chopped corn and garbage.
#> [11] │ <The> boy was there when the sun rose.
#> [13] │ <The> source of the huge river is the clear spring.
#> ... and 250 more

ماذا عن العثور على جميع الجمل التي تبدأ بضمير؟

str_view(sentences, "^She|He|It|They\\b")
#>  [3] │ <It>'s easy to tell the depth of a well.
#> [15] │ <He>lp the woman get back to her feet.
#> [27] │ <He>r purse was full of useless trash.
#> [29] │ <It> snowed, rained, and hailed the same morning.
#> [63] │ <He> ran half way to the hardware store.
#> [90] │ <He> lay prone and hardly moved a limb.
#> ... and 57 more

يظهر الفحص السريع للنتائج أننا نحصل على بعض التطابقات الزائفة. ذلك لأننا نسينا استخدام الأقواس:

str_view(sentences, "^(She|He|It|They)\\b")
#>   [3] │ <It>'s easy to tell the depth of a well.
#>  [29] │ <It> snowed, rained, and hailed the same morning.
#>  [63] │ <He> ran half way to the hardware store.
#>  [90] │ <He> lay prone and hardly moved a limb.
#> [116] │ <He> ordered peach pie with ice cream.
#> [127] │ <It> caught its hind paw in a rusty trap.
#> ... and 51 more

قد تتساءل كيف يمكنك اكتشاف مثل هذا الخطأ إذا لم يظهر في التطابقات القليلة الأولى. التقنية الجيدة هي إنشاء بضع تطابقات إيجابية وسلبية واستخدامها لاختبار أن نمطك يعمل كما هو متوقع:

pos <- c("He is a boy", "She had a good time")
neg <- c("Shells come from the sea", "Hadley said 'It's a great day'")

pattern <- "^(She|He|It|They)\\b"
str_detect(pos, pattern)
#> [1] TRUE TRUE
str_detect(neg, pattern)
#> [1] FALSE FALSE

عادة ما يكون من الأسهل بكثير تقديم أمثلة إيجابية جيدة مقارنة بالأمثلة السلبية، لأن الأمر يستغرق وقتاً قبل أن تصبح جيداً بما يكفي في التعبيرات النمطية للتنبؤ بنقاط ضعفك. ومع ذلك، فهي لا تزال مفيدة: أثناء عملك على المشكلة، يمكنك تجميع مجموعة من أخطائك ببطء، مما يضمن عدم وقوعك في نفس الخطأ مرتين.

15.6.2 العمليات البوليانية

تخيل أننا نريد العثور على الكلمات التي تحتوي فقط على حروف ساكنة (consonants). إحدى التقنيات هي إنشاء فئة محارف تحتوي على جميع الأحرف باستثناء حروف العلة ([^aeiou])، ثم السماح لها بمطابقة أي عدد من الأحرف ([^aeiou]+)، ثم إجبارها على مطابقة النص السلسلي بأكمله عن طريق الإرساء بالبداية والنهاية (^[^aeiou]+$):

str_view(words, "^[^aeiou]+$")
#> [123] │ <by>
#> [249] │ <dry>
#> [328] │ <fly>
#> [538] │ <mrs>
#> [895] │ <try>
#> [952] │ <why>

ولكن يمكنك جعل هذه المشكلة أسهل قليلاً عن طريق قلب المشكلة رأساً على عقب. بدلاً من البحث عن الكلمات التي تحتوي فقط على حروف ساكنة، يمكننا البحث عن الكلمات التي لا تحتوي على أي حروف علة:

str_view(words[!str_detect(words, "[aeiou]")])
#> [1] │ by
#> [2] │ dry
#> [3] │ fly
#> [4] │ mrs
#> [5] │ try
#> [6] │ why

هذه تقنية مفيدة كلما كنت تتعامل مع تركيبة منطقية، خاصة تلك التي تتضمن “و” (AND) أو “ليس” (NOT). على سبيل المثال، تخيل لو كنت تريد العثور على جميع الكلمات التي تحتوي على “a” و “b”. لا يوجد معامل “و” مدمج في التعبيرات النمطية، لذا يتعين علينا معالجتها عن طريق البحث عن جميع الكلمات التي تحتوي على “a” متبوعة بـ “b”، أو “b” متبوعة بـ “a”:

str_view(words, "a.*b|b.*a")
#>  [2] │ <ab>le
#>  [3] │ <ab>out
#>  [4] │ <ab>solute
#> [62] │ <availab>le
#> [66] │ <ba>by
#> [67] │ <ba>ck
#> ... and 24 more

من الأبسط دمج نتائج استدعاءين لـ str_detect():

words[str_detect(words, "a") & str_detect(words, "b")]
#>  [1] "able"      "about"     "absolute"  "available" "baby"      "back"     
#>  [7] "bad"       "bag"       "balance"   "ball"      "bank"      "bar"      
#> [13] "base"      "basis"     "bear"      "beat"      "beauty"    "because"  
#> [19] "black"     "board"     "boat"      "break"     "brilliant" "britain"  
#> [25] "debate"    "husband"   "labour"    "maybe"     "probable"  "table"

ماذا لو أردنا معرفة ما إذا كانت هناك كلمة تحتوي على جميع حروف العلة؟ إذا فعلنا ذلك بالأنماط، فسنحتاج إلى إنتاج !5 (120) نمطاً مختلفاً:

words[str_detect(words, "a.*e.*i.*o.*u")]
# ...
words[str_detect(words, "u.*o.*i.*e.*a")]

من الأبسط بكثير دمج خمسة استدعاءات لـ str_detect():

words[
  str_detect(words, "a") &
  str_detect(words, "e") &
  str_detect(words, "i") &
  str_detect(words, "o") &
  str_detect(words, "u")
]
#> character(0)

بشكل عام، إذا علقت في محاولة إنشاء تعبير نمطي واحد يحل مشكلتك، فارجع خطوة إلى الخلف وفكر فيما إذا كان بإمكانك تقسيم المشكلة إلى أجزاء أصغر، وحل كل تحدٍ قبل الانتقال إلى التحدي التالي.

15.6.3 إنشاء نمط بواسطة البرمجة

ماذا لو أردنا العثور على جميع الجمل في sentences التي تذكر لوناً؟ الفكرة الأساسية بسيطة: نحن فقط ندمج التبادل مع حدود الكلمات.

str_view(sentences, "\\b(red|green|blue)\\b")
#>   [2] │ Glue the sheet to the dark <blue> background.
#>  [26] │ Two <blue> fish swam in the tank.
#>  [92] │ A wisp of cloud hung in the <blue> air.
#> [148] │ The spot on the blotter was made by <green> ink.
#> [160] │ The sofa cushion is <red> and of light weight.
#> [174] │ The sky that morning was clear and bright <blue>.
#> ... and 20 more

ولكن مع نمو عدد الألوان، سيمسي من الممل سريعاً بناء هذا النمط يدوياً. أليس من الرائع إذا كان بإمكاننا تخزين الألوان في متجه؟

rgb <- c("red", "green", "blue")

حسناً، يمكننا ذلك! نحتاج فقط إلى إنشاء النمط من المتجه باستخدام str_c() و str_flatten():

str_c("\\b(", str_flatten(rgb, "|"), ")\\b")
#> [1] "\\b(red|green|blue)\\b"

يمكننا جعل هذا النمط أكثر شمولاً إذا كانت لدينا قائمة جيدة بالألوان. إحدى نقاط البداية هي قائمة الألوان المدمجة التي يمكن لـ R استخدامها للرسومات البيانية:

str_view(colors())
#> [1] │ white
#> [2] │ aliceblue
#> [3] │ antiquewhite
#> [4] │ antiquewhite1
#> [5] │ antiquewhite2
#> [6] │ antiquewhite3
#> ... and 651 more

ولكن لنستبعد أولاً المتغيرات المرقمة:

cols <- colors()
cols <- cols[!str_detect(cols, "\\d")]
str_view(cols)
#> [1] │ white
#> [2] │ aliceblue
#> [3] │ antiquewhite
#> [4] │ aquamarine
#> [5] │ azure
#> [6] │ beige
#> ... and 137 more

ثم يمكننا تحويل هذا إلى نمط عملاق واحد. لن نعرض النمط هنا لأنه ضخم للغاية، ولكن يمكنك رؤيته يعمل:

pattern <- str_c("\\b(", str_flatten(cols, "|"), ")\\b")
str_view(sentences, pattern)
#>   [2] │ Glue the sheet to the dark <blue> background.
#>  [12] │ A rod is used to catch <pink> <salmon>.
#>  [26] │ Two <blue> fish swam in the tank.
#>  [66] │ Cars and busses stalled in <snow> drifts.
#>  [92] │ A wisp of cloud hung in the <blue> air.
#> [112] │ Leaves turn <brown> and <yellow> in the fall.
#> ... and 57 more

في هذا المثال، تحوي cols فقط أرقاماً وأحرفاً، لذا لا داعي للقلق بشأن المحارف الفوقية. ولكن بشكل عام، كلما أنشأت أنماطاً من نصوص سلسلية موجودة، فمن الحكمة إمراره عبر str_escape() لضمان مطابقتها حرفياً.

15.6.4 تمارين

  1. لكل من التحديات التالية، حاول حلها باستخدام تعبير نمطي واحد، ثم باستخدام تركيبة من استدعاءات str_detect() المتعددة.

    أ. إيجاد جميع الكلمات في words التي تبدأ أو تنتهي بـ x.
    ب. إيجاد جميع الكلمات في words التي تبدأ بحرف علة وتنتهي بحرف ساكن.
    ج. هل هناك أي كلمات في words تحتوي على حرف واحد على الأقل من كل حرف علة مختلف؟

  2. قم بإنشاء أنماط للعثور على أدلة تؤيد وتناهض القاعدة اللغوية “i before e except after c”؟

  3. تحتوي colors() على عدد من المعدِّلات مثل “lightgray” و “darkblue”. كيف يمكنك التعرف على هذه المعدِّلات تلقائياً؟ (فكر في كيفية اكتشاف الألوان المعدَّلة ثم إزالتها).

  4. أنشئ تعبيراً نمطياً (regular expression) يعثر على أي مجموعة بيانات في حزمة R الأساسية (base R dataset). يمكنك الحصول على قائمة بمجموعات البيانات هذه عبر استخدام خاص للدالة data() كالتالي: data(package = "datasets")$results[, "Item"]. لاحظ أن عدداً من مجموعات البيانات القديمة عبارة عن متجهات مستقلة؛ وتحتوي هذه المتجهات على اسم المجموعة “data frame” بين قوسين، لذا ستحتاج إلى إزالتها.

15.7 التعبيرات النمطية في أماكن أخرى

تماماً كما هو الحال في دوال stringr و tidyr، هناك العديد من الأماكن الأخرى في R حيث يمكنك استخدام التعبيرات النمطية. تصف الأقسام التالية بعض الدوال المفيدة الأخرى في tidyverse و R الأساسية.

15.7.1 tidyverse

هناك ثلاثة أماكن أخرى مفيدة بشكل خاص حيث قد ترغب في استخدام التعبيرات النمطية:

  • matches(pattern) ستحدد جميع المتغيرات التي يطابق اسمها النمط الموفر. إنها دالة “tidyselect” يمكنك استخدامها في أي مكان في أي دالة tidyverse تحدد المتغيرات (مثل select() و rename_with() و across()).

  • وسيط names_pattern في pivot_longer() يأخذ متجهاً من التعبيرات النمطية، تماماً مثل separate_wider_regex(). وهو مفيد عند استخراج البيانات من أسماء المتغيرات ذات البنية المعقدة.

  • وسيط delim في separate_longer_delim() و separate_wider_delim() عادة ما يطابق نصاً ثابتاً، ولكن يمكنك استخدام regex() لجعله يطابق نمطاً. هذا مفيد، على سبيل المثال، إذا كنت تريد مطابقة فاصلة متبوعة اختيارياً بمسافة، أي regex(", ?").

15.7.2 R الأساسية (Base R)

apropos(pattern) تبحث عن جميع الكائنات المتاحة في البيئة العالمية (global environment) التي تطابق النمط الموفر. هذا مفيد إذا كنت لا تستطيع تذكر اسم الدالة بالتحديد:

apropos("replace")
#>  [1] "%+replace%"       "replace"          "replace_na"      
#>  [4] "replace_theme"    "replace_values"   "replace_when"    
#>  [7] "setReplaceMethod" "str_replace"      "str_replace_all" 
#> [10] "str_replace_na"   "theme_replace"

list.files(path, pattern) تعرض جميع الملفات في path التي تطابق التعبير النمطي pattern. على سبيل المثال، يمكنك العثور على جميع ملفات R Markdown في الدليل الحالي باستخدام:

head(list.files(pattern = "\\.Rmd$"))
#> character(0)

تجدر الإشارة إلى أن لغة الأنماط المستخدمة بواسطة R الأساسية مختلفة قليلاً جداً عن تلك المستخدمة بواسطة stringr. ذلك لأن stringr مبنية فوق حزمة stringi، والتي بدورها مبنية فوق محرك ICU، بينما تستخدم دوال R الأساسية إما محرك TRE أو محرك PCRE، اعتماداً على ما إذا كنت قد ضبطت perl = TRUE أم لا. لحسن الحظ، فإن أساسيات التعبيرات النمطية راسخة لدرجة أنك ستواجه بدائل قليلة جداً عند العمل مع الأنماط التي ستتعلمها في هذا الكتاب. تحتاج فقط إلى أن تكون على دراية بالفرق عندما تبدأ في الاعتماد على ميزات متقدمة مثل نطاقات محارف يونيكود المعقدة أو الميزات الخاصة التي تستخدم بناء الجملة (?…).

15.8 ملخص

مع تحميل كل محرف ترقيم بالعديد من المعاني المحتملة، تعد التعبيرات النمطية واحدة من أكثر اللغات إيجازاً ومباشرة. إنها مربكة بالتأكيد في البداية، ولكن بينما تدرب عينيك على قراءتها وعقلك على فهمها، فإنك تفتح مهارة قوية يمكنك استخدامها في R وفي العديد من الأماكن الأخرى.

في هذا الفصل، بدأت رحلتك لتصبح متمكناً في التعبيرات النمطية من خلال تعلم دوال stringr الأكثر فائدة وأهم مكونات لغة التعبيرات النمطية. وهناك الكثير من الموارد لمعرفة المزيد.

مكان جيد للبدء هو vignette("regular-expressions", package = "stringr"): فهو يوثق المجموعة الكاملة من القواعد والمباني التي تدعمها stringr. مرجع مفيد آخر هو https://www.regular-expressions.info/. إنه ليس خاصاً بـ R، ولكن يمكنك استخدامه للتعلم عن الميزات الأكثر تقدمة للتعبيرات النمطية وكيفية عملها خلف الكواليس.

من الجيد أيضاً معرفة أن stringr مجهزة فوق حزمة stringi التي طوّرها Marek Gagolewski. إذا كنت تواجه صعوبة في العثور على دالة تفعل ما تحتاجه في stringr، فلا تتردد في البحث في stringi. ستجد أن تعلم stringi سهل للغاية لأنها تتبع العديد من الاتفاقيات نفسها في stringr.

في الفصل التالي، سنتحدث عن بنية بيانات مرتبطة وثيقاً بالنصوص السلسلية: العوامل (factors). تُستخدم العوامل لتمثيل البيانات الفئوية (categorical data) في R، أي البيانات ذات المجموعة الثابتة والمعروفة من القيم المحتملة والمحددة بمتجه من النصوص السلسلية.


  1. يمكنك نطقها بـ g صلبة (reg-x) أو g ناعمة (rej-x).↩︎

  2. ستتعلم كيفية إفلات وتجاوز هذه المعاني الخاصة في قسم 15.4.1.↩︎

  3. في الواقع، أي محرف باستثناء محرف السطر الجديد \n.↩︎

  4. هذا يعطينا نسبة الأسماء التي تحتوي على “x”؛ إذا كنت تريد نسبة الأطفال الذين يحملون اسماً يحتوي على x، فستحتاج إلى حساب متوسط موزون.↩︎

  5. نتمنى أن نؤكد لك أنك لن ترى شيئاً بهذا القدر من الغرابة في الحياة العملية، ولكن للأسف على مدار حياتك المهنية من المرجح أن ترى أشياء أكثر غرابة بكثير!↩︎

  6. المجموعة الكاملة للمحارف الفوقية هي .^$\|*+?{}[]()↩︎

  7. تذكر لإنشاء تعبير نمطي يحتوي على \d أو \s، ستلاحظ أنك بحاجة إلى إفلات \ للنص السلسلي، لذا ستكتب "\\d" أو "\\s".↩︎

  8. في الغالب لأننا لا نناقش المصفوفات أبداً في هذا الكتاب!↩︎

  9. comments = TRUE يكون فعالاً بشكل خاص بالاقتران مع نص خام (raw string)، كما نستخدم هنا.↩︎