14  النصوص السلسلية

14.1 مقدمة

حتى الآن، استخدمت العديد من النصوص السلسلية (Strings) دون التعمق في تفاصيلها. حان الوقت الآن للغوص فيها، والتعرف على كيفية عملها، وإتقان بعض أدوات المعالجة النصية القوية المتاحة تحت تصرفك.

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

سنواصل العمل مع النصوص السلسلية في الفصل القادم، حيث ستتعلم المزيد عن قوة التعبيرات النمطية (Regular expressions).

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

في هذا الفصل، سنستخدم دوالاً من حزمة stringr، والتي تُعد جزءاً من منظومة tidyverse الأساسية. سنستخدم أيضاً بيانات babynames لأنها توفر بعض النصوص الممتعة للتعامل معها ومعالجتها.

يمكنك معرفة ما إذا كنت تستخدم دالة من حزمة stringr بسهولة لأن جميع دوالها تبدأ بـ str_. هذا مفيد بشكل خاص إذا كنت تستخدم RStudio لأن كتابة str_ ستفعل الإكمال التلقائي، مما يساعدك على تذكر الدوال المتاحة.

كتابة str_c في موجه أوامر RStudio مع عرض تلميح الإكمال التلقائي في الأعلى، والذي يعرض الدوال التي تبدأ بـ str_c. تظهر بنية الدالة وبداية صفحة الوثائق للدالة المحددة من قائمة الإكمال التلقائي في لوحة على اليمين.

14.2 إنشاء نص سلسلي

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

string1 <- "هذا نص سلسلي"
string2 <- 'إذا أردت تضمين "اقتباس" داخل النص، أستخدم علامات تنصيص مفردة'

إذا نسيت إغلاق علامة التنصيص، فسترى رمز + وهو موجه الاستمرار:

> "هذا نص سلسلي بدون علامة تنصيص ختامية
+ 
+ 
+ نجدة! أنا عالق داخل نص سلسلي

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

14.2.1 رمُوز الهروب (Escapes)

لتضمين علامة تنصيص مفردة أو مزدوجة حقيقية داخل النص، يمكنك استخدام \ لـ “هروبها”:

double_quote <- "\"" # أو '"'
single_quote <- '\'' # أو "'"

لذا إذا أردت تضمين مائل عكسي حقيقي في النص الخاص بك، فستحتاج إلى تهريبه: "\\":

backslash <- "\\"

احذر من أن التمثيل المطبوع للنص ليس هو النص نفسه لأن التمثيل المطبوع يظهر رمُوز الهروب (بمعنى آخر، عند طباعة نص، يمكنك نسخ المخرجات ولصقها لإعادة إنشاء ذلك النص). لرؤية المحتويات الخام للنص، استخدم str_view()1:

x <- c(single_quote, double_quote, backslash)
x
#> [1] "'"  "\"" "\\"
str_view(x)
#> [1] │ '
#> [2] │ "
#> [3] │ \

14.2.2 النصوص الخام (Raw strings)

إنشاء نص يحتوي على علامات تنصيص متعددة أو شرائط مائلة عكسية يصبح مربكاً بسرعة. لتوضيح المشكلة، لننشئ نصاً يحتوي على محتويات كتلة الكود التي عرفنا فيها متغيري double_quote و single_quote:

tricky <- "double_quote <- \"\\\"\" # or '\"'
single_quote <- '\\'' # or \"'\""
str_view(tricky)
#> [1] │ double_quote <- "\"" # or '"'
#>     │ single_quote <- '\'' # or "'"

هذا الكثير من الشرائط المائلة العكسية! (يُطلق على هذا أحياناً متلازمة أعواد الأسنان المائلة.) لإلغاء رمُوز الهروب، يمكنك بدلاً من ذلك استخدام نص خام2:

tricky <- r"(double_quote <- "\"" # or '"'
single_quote <- '\'' # or "'")"
str_view(tricky)
#> [1] │ double_quote <- "\"" # or '"'
#>     │ single_quote <- '\'' # or "'"

عادةً ما تبدأ السلسلة النصية الخام (raw string) بـ r"( وتنتهي بـ )". ولكن إذا كانت السلسلة النصية الخاصة بك تحتوي على )" فيمكنك بدلاً من ذلك استخدام r"[]" أو r"{}"، وإذا لم يكن ذلك كافياً، يمكنك إدراج أي عدد من الشرطات (dashes) لجعل زوجي الفتح والإغلاق فريدين، مثل r"--()--"، أو r"---()---"، إلخ. وتتميز السلاسل النصية الخام بمرونة كافية للتعامل مع أي نص.

14.2.3 رمُوز خاصة أخرى

بالإضافة إلى " و ' و \، هناك مجموعة مقتضبة من المحارف الخاصة الأخرى التي قد تكون مفيدة. والأكثر شيوعاً منها هي \n لإنشاء سطر جديد، و \t لعلامة التبويب (tab). وسترى أيضاً في بعض الأحيان سلاسل نصية تحتوي على محارف هروب يونيكود (Unicode escapes) تبدأ بـ \u أو \U؛ وهي طريقة لكتابة المحارف غير الإنجليزية تعمل على جميع الأنظمة. يمكنك الاطلاع على القائمة الكاملة للمحارف الخاصة الأخرى عبر الأمر ?Quotes.

x <- c("one\ntwo", "one\ttwo", "\u00b5", "\U0001f604")
x
#> [1] "one\ntwo" "one\ttwo" "µ"        "😄"
str_view(x)
#> [1] │ one
#>     │ two
#> [2] │ one{\t}two
#> [3] │ µ
#> [4] │ 😄

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

14.2.4 تمارين

  1. أنشئ نصوصاً سلسلية تحتوي على القيم التالية:

    1. He said "That's amazing!"

    2. \a\b\c\d

    3. \\\\\\

  2. أنشئ النص في جلسة R الخاصة بك واطبعه. ماذا يحدث للرمز الخاص ‏“u00a0\”؟ كيف تعرضه الدالة str_view()؟ هل يمكنك إجراء بحث سريع لتحديد ماهية هذا الرمز الخاص؟

x <- "This\u00a0is\u00a0tricky"

14.3 إنشاء نصوص عديدة من البيانات

بعد أن تعلمت أساسيات إنشاء نص أو اثنين يدوياً، سنغوص في تفاصيل إنشاء النصوص من نصوص أخرى. سيساعدك هذا في حل المشكلة الشائعة حيث تكون لديك بعض النصوص التي كتبتها وتريد دمجها مع نصوص من إطار بيانات. على سبيل المثال، قد تدمج “مرحباً” مع متغير name لإنشاء تحية. سنوضح لك كيفية القيام بذلك باستخدام str_c() و str_glue() وكيفية استخدامهما مع mutate(). هذا يطرح بطبيعة الحال سؤالاً حول دوال stringr التي قد تستخدمها مع summarize()، لذا سنختم هذا القسم بمناقشة str_flatten()، وهي دالة تلخيصية للنصوص.

14.3.1 str_c()

تأخذ str_c() أي عدد من المتجهات كعوامل وترجع متجهاً نصياً:

str_c("x", "y")
#> [1] "xy"
str_c("x", "y", "z")
#> [1] "xyz"
str_c("Hello ", c("John", "Susan"))
#> [1] "Hello John"  "Hello Susan"

تتشابه str_c() جداً مع الدالة الأساسية paste0()، لكنها مصممة للاستخدام مع mutate() من خلال اتباع قواعد tidyverse المعتادة لإعادة التدوير وتمرير القيم المفقودة:

df <- tibble(name = c("Flora", "David", "Terra", NA))
df |> mutate(greeting = str_c("Hi ", name, "!"))
#> # A tibble: 4 × 2
#>   name  greeting 
#>   <chr> <chr>    
#> 1 Flora Hi Flora!
#> 2 David Hi David!
#> 3 Terra Hi Terra!
#> 4 <NA>  <NA>

إذا أردت عرض القيم المفقودة بطريقة أخرى، استخدم coalesce() لاستبدالها. اعتماداً على ما تريد، قد تستخدمها إما داخل أو خارج str_c():

df |> 
  mutate(
    greeting1 = str_c("Hi ", coalesce(name, "you"), "!"),
    greeting2 = coalesce(str_c("Hi ", name, "!"), "Hi!")
  )
#> # A tibble: 4 × 3
#>   name  greeting1 greeting2
#>   <chr> <chr>     <chr>    
#> 1 Flora Hi Flora! Hi Flora!
#> 2 David Hi David! Hi David!
#> 3 Terra Hi Terra! Hi Terra!
#> 4 <NA>  Hi you!   Hi!

14.3.2 str_glue()

إذا كنت تخلط العديد من النصوص الثابتة والمتغيرة باستخدام str_c()، ستلاحظ أنك تكتب الكثير من علامات "، مما يجعل من الصعب رؤية الهدف العام للكود. تتوفر طريقة بديلة تقدمها حزمة glue عبر str_glue()4. أنت تعطيها نصاً مفردًا يتميز بميزة خاصة: أي شيء داخل {} سيتم تقييمه وكأنه خارج علامات التنصيص:

df |> mutate(greeting = str_glue("Hi {name}!"))
#> # A tibble: 4 × 2
#>   name  greeting 
#>   <chr> <glue>   
#> 1 Flora Hi Flora!
#> 2 David Hi David!
#> 3 Terra Hi Terra!
#> 4 <NA>  Hi NA!

كما ترى، تقوم str_glue() حالياً بتحويل القيم المفقودة إلى النص "NA"، مما يجعلها للأسف غير متسقة مع str_c().

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

df |> mutate(greeting = str_glue("{{Hi {name}!}}"))
#> # A tibble: 4 × 2
#>   name  greeting   
#>   <chr> <glue>     
#> 1 Flora {Hi Flora!}
#> 2 David {Hi David!}
#> 3 Terra {Hi Terra!}
#> 4 <NA>  {Hi NA!}

14.3.3 str_flatten()

تعمل str_c() و str_glue() بشكل جيد مع mutate() لأن طول مخرجاتهما يكون نفس طول مدخلاتهما. ماذا لو أردت دالة تعمل بفاعلية مع summarize()، أي شيء يررجع دائماً نصاً مفردًا؟ هذه هي مهمة str_flatten()5: حيث تأخذ متجهاً نصياً وتدمج كل عنصر من عناصر المتجه في نص مفرد:

str_flatten(c("x", "y", "z"))
#> [1] "xyz"
str_flatten(c("x", "y", "z"), ", ")
#> [1] "x, y, z"
str_flatten(c("x", "y", "z"), ", ", last = ", and ")
#> [1] "x, y, and z"

هذا يجعلها تعمل بفاعلية مع summarize():

df <- tribble(
  ~ name, ~ fruit,
  "Carmen", "banana",
  "Carmen", "apple",
  "Marvin", "nectarine",
  "Terence", "cantaloupe",
  "Terence", "papaya",
  "Terence", "mandarin"
)
df |> 
  group_by(name) |> 
  summarize(fruits = str_flatten(fruit, ", "))
#> # A tibble: 3 × 2
#>   name    fruits                      
#>   <chr>   <chr>                       
#> 1 Carmen  banana, apple               
#> 2 Marvin  nectarine                   
#> 3 Terence cantaloupe, papaya, mandarin

14.3.4 تمارين

  1. قارن بين نتائج paste0() و str_c() للمدخلات التالية:

    str_c("hi ", NA)
    str_c(letters[1:2], letters[1:3])
  2. ما الفرق بين paste() و paste0()؟ كيف يمكنك إعادة إنشاء ما يكافئ paste() باستخدام str_c()؟

  3. حول التعبيرات التالية من str_c() إلى str_glue() أو العكس:

    أ. str_c("The price of ", food, " is ", price)

    ب. str_glue("I'm {age} years old and live in {country}")

    جـ. str_c("\\section{", title, "}")

14.4 استخراج البيانات من النصوص السلسلية

من الشائع جداً أن تتزاحم متغيرات متعددة معاً داخل نص سلسلي واحد. في هذا القسم، ستتعلم كيفية استخدام أربع دوال من حزمة tidyr لاستخراجها:

  • df |> separate_longer_delim(col, delim)
  • df |> separate_longer_position(col, width)
  • df |> separate_wider_delim(col, delim, names)
  • df |> separate_wider_position(col, widths)

إذا نظرت عن كثب، ستلاحظ وجود نمط مشترك هنا: separate_، ثم longer أو wider، ثم _، ثم حسب delim أو position. وذلك لأن هذه الدوال الأربع تتكون من دالتين أوليين أكثر بساطة:

  • تماماً كما هو الحال مع pivot_longer() و pivot_wider()، تجعل دوال _longer إطار بيانات المدخلات أطول عن طريق إنشاء صفوف جديدة، بينما تجعل دوال _wider إطار البيانات أعرض عن طريق إنشاء أعمدة جديدة.
  • تقوم delim بتقسيم النص باستخدام فاصل مثل ", " أو " "؛ بينما تقسم position عند عروض محددة، مثل c(3, 5, 2).

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

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

14.4.1 التقسيم إلى صفوف

يكون تقسيم النص إلى صفوف أكثر فائدة عندما يتغير عدد المكونات من صف إلى آخر. الحالة الأكثر شيوعاً هي طلب separate_longer_delim() للتقسيم بناءً على فاصل محدد:

df1 <- tibble(x = c("a,b,c", "d,e", "f"))
df1 |> 
  separate_longer_delim(x, delim = ",")
#> # A tibble: 6 × 1
#>   x    
#>   <chr>
#> 1 a    
#> 2 b    
#> 3 c    
#> 4 d    
#> 5 e    
#> 6 f

من النادر رؤية separate_longer_position() في العمل العملي، ولكن بعض مجموعات البيانات القديمة تستخدم تنسيقاً مدمجاً للغاية حيث يُستخدم كل حرف لتسجيل قيمة:

df2 <- tibble(x = c("1211", "131", "21"))
df2 |> 
  separate_longer_position(x, width = 1)
#> # A tibble: 9 × 1
#>   x    
#>   <chr>
#> 1 1    
#> 2 2    
#> 3 1    
#> 4 1    
#> 5 1    
#> 6 3    
#> # ℹ 3 more rows

14.4.2 التقسيم إلى أعمدة

يكون تقسيم النص إلى أعمدة أكثر فائدة عندما يكون هناك عدد ثابت من المكونات في كل نص، وترغب في توزيعها عبر الأعمدة. وهي أكثر تعقيداً قليلاً من دوال longer الموازية لها لأنك تحتاج إلى تسمية الأعمدة. على سبيل المثال، في مجموعة البيانات التالية، يتكون x من كود، ورقم إصدار، وسنة، مفصولة بـ ".". لاستخدام separate_wider_delim()، نزودها بالفاصل والأرقام/الأسماء في وسيطين:

df3 <- tibble(x = c("a10.1.2022", "b10.2.2011", "e15.1.2015"))
df3 |> 
  separate_wider_delim(
    x,
    delim = ".",
    names = c("code", "edition", "year")
  )
#> # A tibble: 3 × 3
#>   code  edition year 
#>   <chr> <chr>   <chr>
#> 1 a10   1       2022 
#> 2 b10   2       2011 
#> 3 e15   1       2015

إذا كان جزء معين غير مفيد، يمكنك استخدام اسم NA لإهماله وحذفه من النتائج:

df3 |> 
  separate_wider_delim(
    x,
    delim = ".",
    names = c("code", NA, "year")
  )
#> # A tibble: 3 × 2
#>   code  year 
#>   <chr> <chr>
#> 1 a10   2022 
#> 2 b10   2011 
#> 3 e15   2015

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

df4 <- tibble(x = c("202215TX", "202122LA", "202325CA")) 
df4 |> 
  separate_wider_position(
    x,
    widths = c(year = 4, age = 2, state = 2)
  )
#> # A tibble: 3 × 3
#>   year  age   state
#>   <chr> <chr> <chr>
#> 1 2022  15    TX   
#> 2 2021  22    LA   
#> 3 2023  25    CA

14.4.3 تشخيص مشكلات التوسيع للأعمدة

تتطلب separate_wider_delim()6 مجموعة ثابته ومعروفة من الأعمدة. ماذا يحدث إذا لم تحتوي بعض الصفوف على العدد المتوقع من الأجزاء؟ هناك مشكلتان محتملتان: عدد قليل جداً من الأجزاء أو عدد كبير جداً، لذا توفر separate_wider_delim() وسيطين للمساعدة: too_few و too_many. لنلقِ نظرة أولاً على حالة too_few مع مجموعة البيانات العينية التالية:

df <- tibble(a = c("1-1-1", "1-1-2", "1-3", "1-3-2", "1"))

df |> 
  separate_wider_delim(
    a,
    delim = "-",
    names = c("x", "y", "z")
  )
#> Error in `separate_wider_delim()`:
#> ! Expected 3 pieces in each element of `a`.
#> ! 2 values were too short.
#> ℹ Use `too_few = "debug"` to diagnose the problem.
#> ℹ Use `too_few = "align_start"/"align_end"` to silence this message.

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

debug <- df |> 
  separate_wider_delim(
    a,
    delim = "-",
    names = c("x", "y", "z"),
    too_few = "debug"
  )
#> Warning: Debug mode activated: adding variables `a_ok`, `a_pieces`, and
#> `a_remainder`.
debug
#> # A tibble: 5 × 7
#>   x     y     z     a     a_ok  a_pieces a_remainder
#>   <chr> <chr> <chr> <chr> <lgl>    <int> <chr>      
#> 1 1     1     1     1-1-1 TRUE         3 ""         
#> 2 1     1     2     1-1-2 TRUE         3 ""         
#> 3 1     3     <NA>  1-3   FALSE        2 ""         
#> 4 1     3     2     1-3-2 TRUE         3 ""         
#> 5 1     <NA>  <NA>  1     FALSE        1 ""

عند استخدام وضع التصحيح (debug mode)، تحصل على ثلاثة أعمدة إضافية مضافة إلى المخرجات: a_ok و a_pieces و a_remainder (إذا قمت بتقسيم متغير باسم مختلف، فستحصل على بادئة مختلفة). هنا، تتيح لك a_ok العثور بسرعة على المدخلات التي فشلت:

debug |> filter(!a_ok)
#> # A tibble: 2 × 7
#>   x     y     z     a     a_ok  a_pieces a_remainder
#>   <chr> <chr> <chr> <chr> <lgl>    <int> <chr>      
#> 1 1     3     <NA>  1-3   FALSE        2 ""         
#> 2 1     <NA>  <NA>  1     FALSE        1 ""

تخبرنا a_pieces بعدد الأجزاء التي تم العثور عليها، مقارنة بـ 3 المتوقعة (طول names). لا تُعد a_remainder مفيدة عندما يكون هناك عدد قليل جداً من الأجزاء، ولكننا سنراها مرة أخرى قريباً.

في بعض الأحيان، يؤدي النظر في معلومات التصحيح هذه إلى كشف مشكلة في استراتيجية الفواصل الخاصة بك أو اقتراح أنك بحاجة إلى إجراء المزيد من المعالجة الأولية قبل التقسيم. في هذه الحالة، قم بإصلاح المشكلة في مرحلة سابقة وتأكد من إزالة too_few = "debug" لضمان تحول المشكلات الجديدة إلى أخطاء واضحة.

في حالات أخرى، قد ترغب في ملء الأجزاء المفقودة بـ NA والمتابعة. هذه هي وظيفة too_few = "align_start" و too_few = "align_end" اللتين تتيحان لك التحكم في مكان وضع قيم NA:

df |> 
  separate_wider_delim(
    a,
    delim = "-",
    names = c("x", "y", "z"),
    too_few = "align_start"
  )
#> # A tibble: 5 × 3
#>   x     y     z    
#>   <chr> <chr> <chr>
#> 1 1     1     1    
#> 2 1     1     2    
#> 3 1     3     <NA> 
#> 4 1     3     2    
#> 5 1     <NA>  <NA>

تنطبق نفس المبادئ إذا كان لديك عدد كبير جداً من الأجزاء:

df <- tibble(a = c("1-1-1", "1-1-2", "1-3-5-6", "1-3-2", "1-3-5-7-9"))

df |> 
  separate_wider_delim(
    a,
    delim = "-",
    names = c("x", "y", "z")
  )
#> Error in `separate_wider_delim()`:
#> ! Expected 3 pieces in each element of `a`.
#> ! 2 values were too long.
#> ℹ Use `too_many = "debug"` to diagnose the problem.
#> ℹ Use `too_many = "drop"/"merge"` to silence this message.

ولكن الآن، عندما نقوم بتصحيح النتيجة، يمكنك رؤية الغرض من a_remainder:

debug <- df |> 
  separate_wider_delim(
    a,
    delim = "-",
    names = c("x", "y", "z"),
    too_many = "debug"
  )
#> Warning: Debug mode activated: adding variables `a_ok`, `a_pieces`, and
#> `a_remainder`.
debug |> filter(!a_ok)
#> # A tibble: 2 × 7
#>   x     y     z     a         a_ok  a_pieces a_remainder
#>   <chr> <chr> <chr> <chr>     <lgl>    <int> <chr>      
#> 1 1     3     5     1-3-5-6   FALSE        4 -6         
#> 2 1     3     5     1-3-5-7-9 FALSE        5 -7-9

لديك مجموعة مختلفة قليلاً من الخيارات للتعامل مع الأجزاء الزائدة: إما إسقاط أي أجزاء إضافية ضمنياً عبر “drop” أو دمجها جميعاً في العمود الأخير عبر “merge”:

df |> 
  separate_wider_delim(
    a,
    delim = "-",
    names = c("x", "y", "z"),
    too_many = "drop"
  )
#> # A tibble: 5 × 3
#>   x     y     z    
#>   <chr> <chr> <chr>
#> 1 1     1     1    
#> 2 1     1     2    
#> 3 1     3     5    
#> 4 1     3     2    
#> 5 1     3     5

df |> 
  separate_wider_delim(
    a,
    delim = "-",
    names = c("x", "y", "z"),
    too_many = "merge"
  )
#> # A tibble: 5 × 3
#>   x     y     z    
#>   <chr> <chr> <chr>
#> 1 1     1     1    
#> 2 1     1     2    
#> 3 1     3     5-6  
#> 4 1     3     2    
#> 5 1     3     5-7-9

14.5 الأحرف (Letters)

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

14.5.1 الطول (Length)

تخبرك str_length() بعدد الأحرف في النص السلسلي:

str_length(c("a", "R for data science", NA))
#> [1]  1 18 NA

يمكنك استخدام هذه الدالة مع count() للعثور على توزيع أطوال أسماء الأطفال في الولايات المتحدة (babynames)، ثم مع filter() للتحقق من أطول الأسماء، والتي تصادف أن طولها 15 حرفاً7:

babynames |> 
  count(length = str_length(name), wt = n)
#> # A tibble: 14 × 2
#>   length        n
#>    <int>    <int>
#> 1      2   338150
#> 2      3  8589596
#> 3      4 48506739
#> 4      5 87011607
#> 5      6 90749404
#> 6      7 72120767
#> # ℹ 8 more rows

babynames |> 
  filter(str_length(name) == 15) |> 
  count(name, wt = n, sort = TRUE)
#> # A tibble: 34 × 2
#>   name                n
#>   <chr>           <int>
#> 1 Franciscojavier   123
#> 2 Christopherjohn   118
#> 3 Johnchristopher   118
#> 4 Christopherjame   108
#> 5 Christophermich    52
#> 6 Ryanchristopher    45
#> # ℹ 28 more rows

14.5.2 الاستقطاع والاقتطاع (Subsetting)

يمكنك استخراج أجزاء من نص سلسلي باستخدام str_sub(string, start, end)، حيث ترمز start و end إلى المواضع التي يجب أن يبدأ وينتهي عندها النص الفرعي. الوسيطان start و end شاملان، لذا فإن طول النص المرجَع سيكون end - start + 1:

x <- c("Apple", "Banana", "Pear")
str_sub(x, 1, 3)
#> [1] "App" "Ban" "Pea"

يمكنك استخدام قيم سالبة للعد تنازلياً من نهاية النص: -1 هو الحرف الأخير، -2 هو الحرف قبل الأخير، وهكذا.

str_sub(x, -3, -1)
#> [1] "ple" "ana" "ear"

لاحظ أن str_sub() لن تفشل إذا كان النص قصيراً جداً: بل ستعطي أكبر قدر ممكن فقط:

str_sub("a", 1, 5)
#> [1] "a"

يمكننا استخدام str_sub() مع mutate() لتعيين الحرف الأول والأخير من كل اسم:

babynames |> 
  mutate(
    first = str_sub(name, 1, 1),
    last = str_sub(name, -1, -1)
  )
#> # A tibble: 1,924,665 × 7
#>    year sex   name          n   prop first last 
#>   <dbl> <chr> <chr>     <int>  <dbl> <chr> <chr>
#> 1  1880 F     Mary       7065 0.0724 M     y    
#> 2  1880 F     Anna       2604 0.0267 A     a    
#> 3  1880 F     Emma       2003 0.0205 E     a    
#> 4  1880 F     Elizabeth  1939 0.0199 E     h    
#> 5  1880 F     Minnie     1746 0.0179 M     e    
#> 6  1880 F     Margaret   1578 0.0162 M     t    
#> # ℹ 1,924,659 more rows

14.5.3 تمارين

  1. عند حساب توزيع أطوال أسماء الأطفال، لماذا استخدمنا wt = n؟
  2. استخدم str_length() و str_sub() لاستخراج الحرف الأوسط من كل اسم طفل. ماذا ستفعل إذا كان النص يحتوي على عدد زوجي من المحارف؟
  3. هل هناك اتجاهات رئيسية في طول أسماء الأطفال بمرور الوقت؟ ماذا عن شعبية الحروف الأولى والأخيرة؟

14.6 النصوص غير الإنجليزية

حتى الآن، ركزنا على النصوص باللغة الإنجليزية والتي يُعد التعامل معها سهلاً بشكل خاص لسببين. أولاً، الأبجدية الإنجليزية بسيطة نسبياً: هناك 26 حرفاً فقط. ثانياً (ولعله الأكثر أهمية)، البنية التحتية للحوسبة التي نستخدمها اليوم صُممت بشكل أساسي بواسطة متحدثي اللغة الإنجليزية. للأسف، لا تتوفر لدينا المساحة لمعالجة كاملة للغات غير الإنجليزية. ومع ذلك، أردنا لفت انتباهك إلى بعض أكبر التحديات التي قد تواجهها: الترميز (encoding)، وتنوعات الأحرف، والدوال المعتمدة على الإعدادات المحلية (locale).

14.6.1 الترميز (Encoding)

عند العمل مع نصوص غير إنجليزية، غالباً ما يكون التحدي الأول هو الترميز. لفهم ما يحدث، نحتاج إلى الغوص في كيفية تمثيل أجهزة الكمبيوتر للنصوص السلسلية. في R، يمكننا الوصول إلى التمثيل الداخلي للنص باستخدام charToRaw():

charToRaw("Hadley")
#> [1] 48 61 64 6c 65 79

كل رقم من هذه الأرقام الستة عشرية يمثل حرفاً واحداً: 48 هي H، و 61 هي a، وهكذا. يسمى التعيين من الرقم الست عشري إلى الحرف بالترميز، وفي هذه الحالة يسمى الترميز ASCII. يقوم ASCII بعمل ممتاز في تمثيل الأحرف الإنجليزية لأنه الترميز القياسي الأمريكي لتبادل المعلومات (American Standard Code for Information Interchange).

الأمور ليست بهذه السهولة بالنسبة للغات أخرى غير الإنجليزية. في الأيام الأولى للحوسبة، كانت هناك العديد من المعايير المتنافسة لترميز الأحرف غير الإنجليزية. على سبيل المثال، كان هناك ترميزان مختلفان لأوروبا: Latin1 (المعروف أيضاً بـ ISO-8859-1) للغات أوروبا الغربية، و Latin2 (المعروف بـ ISO-8859-2) للغات أوروبا Central. في Latin1، البايت b1 هو “±”، ولكن في Latin2، هو “ą”! لحسن الحظ، يوجد اليوم معيار واحد مدعوم في كل مكان تقريباً: UTF-8. يمكن لـ UTF-8 ترميز كل حرف يستخدمه البشر اليوم تقريباً والكثير من الرموز الإضافية مثل الإيموجي.

تستخدم حزمة readr ترميز UTF-8 في كل مكان. هذا خيار افتراضي جيد ولكنه سيفشل مع البيانات الناتجة عن أنظمة قديمة لا تستخدم UTF-8. إذا حدث هذا، ستنسق النصوص بشكل غريب عند طباعتها. في بعض الأحيان قد يتشوه حرف أو حرفان فقط؛ وفي أحيان أخرى، تحصل على رموز غير مفهومة تماماً. على سبيل المثال، إليك نصين بصيغة CSV تحتويان على ترميزات غير مألوفة8:

x1 <- "text\nEl Ni\xf1o was particularly bad this year"
read_csv(x1)$text
# > [1] "El Ni\xf1o was particularly bad this year"

x2 <- "text\n\x82\xb1\x82\xf1\x82\xc9\x82\xbf\x82\xcd"
read_csv(x2)$text
# > [1] "\x82\xb1\x82\xf1\x82ɂ\xbf\x82\xcd"

لقراءتها بشكل صحيح، حدد الترميز عبر وسيط locale:

read_csv(x1, locale = locale(encoding = "Latin1"))$text
# > [1] "El Niño was particularly bad this year"

read_csv(x2, locale = locale(encoding = "Shift-JIS"))$text
# > [1] "こんにちは"

كيف تجد الترميز الصحيح؟ إذا كنت محظوظاً، فسيتم تضمينه في وثائق البيانات. للأسف، نادراً ما يحدث ذلك، لذا توفر readr دالة guess_encoding() للمساعدة في تخمينه. إنها ليست مضمونة تماماً وتعمل بشكل أفضل عندما يكون لديك الكثير من النصوص، ولكنها نقطة بداية معقولة. توقع تجربة بعض الترميزات المختلفة قبل العثور على الترميز الصحيح.

الترميزات موضوع غني ومعقد؛ لقد خدشنا السطح فقط هنا. إذا كنت ترغب في معرفة المزيد، نوصي بقراءة الشرح المفصل على http://kunststube.net/encoding/.

14.6.2 تنوعات الأحرف (Letter variations)

يشكل العمل بلغات تحتوي على تشكيلات أو علامات نطقية (accents) تحدياً كبيراً عند تحديد مواضع الأحرف (على سبيل المثال، باستخدام str_length() و str_sub()) حيث قد يتم ترميز الأحرف المشكولة كحرف فردي (مثل ü) أو كحرفين عن طريق دمج حرف غير مشكول (مثل u) مع علامة نطقية (مثل ¨). على سبيل المثال، يعرض هذا الكود طريقتين لتمثيل ü تبدوان متطابقتين:

u <- c("\u00fc", "u\u0308")
str_view(u)
#> [1] │ ü
#> [2] │ ü

لكن النصين يختلفان في الطول، وحرفاهما الأولان مختلفان:

str_length(u)
#> [1] 1 2
str_sub(u, 1, 1)
#> [1] "ü" "u"

أخيراً، لاحظ أن مقارنة هذه النصوص باستخدام == تفسرها على أنها مختلفة، بينما تتعرف الدالة المفيدة str_equal() في حزمة stringr على أن كلاهما لهما نفس المظهر:

u[[1]] == u[[2]]
#> [1] FALSE

str_equal(u[[1]], u[[2]])
#> [1] TRUE

14.6.3 الدوال المعتمدة على الإعدادات المحلية (Locale-dependent functions)

أخيراً، هناك حفنة من دوال stringr التي يعتمد سلوكها على الإعدادات المحلية (locale) الخاصة بك. الإعداد المحلي يشبه اللغة ولكنه يتضمن محدد منطقة اختياري للتعامل مع الاختلافات الإقليمية داخل اللغة الواحدة. يتم تحديد الإعداد المحلي باختصار لغة بحروف صغيرة، متبوعاً بحرف _ ومعرف منطقة بحروف كبيرة بشكل اختياري. على سبيل المثال، “en” هي الإنجليزية، و “en_GB” هي الإنجليزية البريطانية، و “en_US” هي الإنجليزية الأمريكية. إذا كنت لا تعرف كود لغتك بالفعل، فإن ويكيبيديا تحتوي على قائمة جيدة، ويمكنك رؤية المقبول منها في stringr عبر الاطلاع على stringi::stri_locale_list().

تستخدم دوال النصوص في R الأساسية تلقائياً الإعداد المحلي المحدد بواسطة نظام التشغيل الخاص بك. هذا يعني أن دوال R الأساسية تفعل ما تتوقعه للغتك، ولكن الكود الخاص بك قد يعمل بشكل مختلف إذا شاركته مع شخص يعيش في بلد آخر. لتجنب هذه المشكلة، تعتمد stringr افتراضياً القواعد الإنجليزية باستخدام الإعداد المحلي “en” وتتطلب منك تحديد وسيط locale لتجاوز ذلك. لحسن الحظ، هناك مجموعتان فقط من الدوال حيث يهم الإعداد المحلي حقاً: تغيير حالة الأحرف (كبيرة/صغيرة) والفرز.

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

str_to_upper(c("i", "ı"))
#> [1] "I" "I"
str_to_upper(c("i", "ı"), locale = "tr")
#> [1] "İ" "I"

يعتمد فرز النصوص على ترتيب الأبجدية، وترتيب الأبجدية ليس هو نفسه في كل لغة9! إليك مثالاً: في اللغة التشيكية، الحرف “ch” هو حرف مركب يظهر بعد h في الأبجدية.

str_sort(c("a", "c", "ch", "h", "z"))
#> [1] "a"  "c"  "ch" "h"  "z"
str_sort(c("a", "c", "ch", "h", "z"), locale = "cs")
#> [1] "a"  "c"  "h"  "ch" "z"

يظهر هذا أيضاً عند فرز النصوص باستخدام dplyr::arrange()، ولهذا السبب تحتوي أيضاً على وسيط locale.

14.7 ملخص

في هذا الفصل، تعلمت بعض إمكانيات حزمة stringr: كيفية إنشاء النصوص السلسلية، ودمجها، واستخراجها، وبعض التحديات التي قد تواجهها مع النصوص غير الإنجليزية. حان الوقت الآن لتعلم واحدة من أهم وأقوى الأدوات للعمل مع النصوص: التعبيرات النمطية (Regular expressions). التعبيرات النمطية هي لغة موجزة للغاية ولكنها تعبيرية جداً لوصف الأنماط داخل النصوص، وهي موضوع الفصل القادم.


  1. أو استخدم دالة R الأساسية writeLines().↩︎

  2. متاح في إصدار R 4.0.0 وما فوق.↩︎

  3. تستخدم str_view() أيضاً الألوان للفت انتباهك إلى علامات التبويب والمسافات والمطابقات وما إلى ذلك. لا تظهر الألوان حالياً في الكتاب، ولكنك ستلاحظها عند تشغيل الكود بشكل تفاعلي.↩︎

  4. إذا كنت لا تستخدم stringr، يمكنك أيضاً الوصول إليها مباشرة عبر glue::glue().↩︎

  5. المكافئ في R الأساسية هو paste() المستخدمة مع عامل collapse.↩︎

  6. تنطبق نفس المبادئ على separate_wider_position() و separate_wider_regex().↩︎

  7. عند النظر في هذه المدخلات، نرجح أن بيانات أسماء الأطفال تحذف المسافات أو الشرطات وتقتطع الأسماء بعد 15 حرفاً.↩︎

  8. هنا أستخدم الرمز الخاص \x لترميز البيانات الثنائية مباشرة في النص.↩︎

  9. الفرز في اللغات التي لا تحتوي على أبجدية، مثل الصينية، أكثر تعقيداً.↩︎