14 النصوص السلسلية
14.1 مقدمة
حتى الآن، استخدمت العديد من النصوص السلسلية (Strings) دون التعمق في تفاصيلها. حان الوقت الآن للغوص فيها، والتعرف على كيفية عملها، وإتقان بعض أدوات المعالجة النصية القوية المتاحة تحت تصرفك.
سنبدأ بتفاصيل إنشاء النصوص السلسلية ومتجهات المحارف. ثم ستنتقل إلى إنشاء النصوص من البيانات، ثم العكس: استخراج النصوص من البيانات. بعد ذلك سنناقش الأدوات التي تتعامل مع الأحرف الفردية. ويختتم الفصل بالدوال التي تتعامل مع الأحرف الفردية ومناقشة قصيرة حول الحالات التي قد تقودك فيها توقعاتك من اللغة الإنجليزية إلى نتائج غير دقيقة عند التعامل مع لغات أخرى.
سنواصل العمل مع النصوص السلسلية في الفصل القادم، حيث ستتعلم المزيد عن قوة التعبيرات النمطية (Regular expressions).
14.1.1 المتطلبات المسبقة
في هذا الفصل، سنستخدم دوالاً من حزمة stringr، والتي تُعد جزءاً من منظومة tidyverse الأساسية. سنستخدم أيضاً بيانات babynames لأنها توفر بعض النصوص الممتعة للتعامل معها ومعالجتها.
يمكنك معرفة ما إذا كنت تستخدم دالة من حزمة stringr بسهولة لأن جميع دوالها تبدأ بـ str_. هذا مفيد بشكل خاص إذا كنت تستخدم RStudio لأن كتابة str_ ستفعل الإكمال التلقائي، مما يساعدك على تذكر الدوال المتاحة.

14.2 إنشاء نص سلسلي
لقد أنشأنا نصوصاً سلسلية بشكل عابر في أجزاء سابقة من هذا الكتاب ولكننا لم نناقش التفاصيل. أولاً، يمكنك إنشاء نص سلسلي باستخدام علامات التنصيص المفردة (') أو المزدوجة ("). لا يوجد فرق في السلوك بين الاثنين، ولكن حرصاً على الاتساق، يوصي دليل تنسيق tidyverse باستخدام "، إلا إذا كان النص يحتوي على عدة علامات ".
string1 <- "هذا نص سلسلي"
string2 <- 'إذا أردت تضمين "اقتباس" داخل النص، أستخدم علامات تنصيص مفردة'إذا نسيت إغلاق علامة التنصيص، فسترى رمز + وهو موجه الاستمرار:
> "هذا نص سلسلي بدون علامة تنصيص ختامية
+
+
+ نجدة! أنا عالق داخل نص سلسلي
إذا حدث هذا لك ولم تتمكن من تحديد علامة التنصيص التي يجب إغلاقها، اضغط على مفتاح Escape للإلغاء والمحاولة مرة أخرى.
14.2.1 رمُوز الهروب (Escapes)
لتضمين علامة تنصيص مفردة أو مزدوجة حقيقية داخل النص، يمكنك استخدام \ لـ “هروبها”:
double_quote <- "\"" # أو '"'
single_quote <- '\'' # أو "'"لذا إذا أردت تضمين مائل عكسي حقيقي في النص الخاص بك، فستحتاج إلى تهريبه: "\\":
backslash <- "\\"احذر من أن التمثيل المطبوع للنص ليس هو النص نفسه لأن التمثيل المطبوع يظهر رمُوز الهروب (بمعنى آخر، عند طباعة نص، يمكنك نسخ المخرجات ولصقها لإعادة إنشاء ذلك النص). لرؤية المحتويات الخام للنص، استخدم str_view()1:
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.
لاحظ أن str_view() تستخدم أقواس جعدة لعلامات التبويب لجعل كشفها أسهل3. أحد تحديات التعامل مع النصوص هو وجود طرق متنوعة يمكن أن تنتهي بها المسافات البيضاء في النص، لذا تساعدك هذه الخلفية على إدراك حدوث شيء غريب.
14.2.4 تمارين
-
أنشئ نصوصاً سلسلية تحتوي على القيم التالية:
He said "That's amazing!"\a\b\c\d\\\\\\
أنشئ النص في جلسة 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() جداً مع الدالة الأساسية paste0()، لكنها مصممة للاستخدام مع mutate() من خلال اتباع قواعد tidyverse المعتادة لإعادة التدوير وتمرير القيم المفقودة:
إذا أردت عرض القيم المفقودة بطريقة أخرى، استخدم 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. أنت تعطيها نصاً مفردًا يتميز بميزة خاصة: أي شيء داخل {} سيتم تقييمه وكأنه خارج علامات التنصيص:
كما ترى، تقوم str_glue() حالياً بتحويل القيم المفقودة إلى النص "NA"، مما يجعلها للأسف غير متسقة مع str_c().
قد تتساءل أيضاً عما يحدث إذا كنت بحاجة إلى تضمين { أو } عادية في النص الخاص بك. أنت على المسار الصحيح إذا حزرت أنك ستنحتاج إلى تهريبها بطريقة ما. الحيلة هي أن glue تستخدم تقنية تهريب مختلفة قليلاً: بدلاً من وضع رمز خاص كبادئة مثل \، تقوم بمضاعفة الرموز الخاصة:
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, mandarin14.3.4 تمارين
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 rows14.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 CA14.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-914.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 rows14.5.2 الاستقطاع والاقتطاع (Subsetting)
يمكنك استخراج أجزاء من نص سلسلي باستخدام str_sub(string, start, end)، حيث ترمز start و end إلى المواضع التي يجب أن يبدأ وينتهي عندها النص الفرعي. الوسيطان start و end شاملان، لذا فإن طول النص المرجَع سيكون end - start + 1:
يمكنك استخدام قيم سالبة للعد تنازلياً من نهاية النص: -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 rows14.5.3 تمارين
- عند حساب توزيع أطوال أسماء الأطفال، لماذا استخدمنا
wt = n؟ - استخدم
str_length()وstr_sub()لاستخراج الحرف الأوسط من كل اسم طفل. ماذا ستفعل إذا كان النص يحتوي على عدد زوجي من المحارف؟ - هل هناك اتجاهات رئيسية في طول أسماء الأطفال بمرور الوقت؟ ماذا عن شعبية الحروف الأولى والأخيرة؟
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:
لقراءتها بشكل صحيح، حدد الترميز عبر وسيط locale:
كيف تجد الترميز الصحيح؟ إذا كنت محظوظاً، فسيتم تضمينه في وثائق البيانات. للأسف، نادراً ما يحدث ذلك، لذا توفر readr دالة guess_encoding() للمساعدة في تخمينه. إنها ليست مضمونة تماماً وتعمل بشكل أفضل عندما يكون لديك الكثير من النصوص، ولكنها نقطة بداية معقولة. توقع تجربة بعض الترميزات المختلفة قبل العثور على الترميز الصحيح.
الترميزات موضوع غني ومعقد؛ لقد خدشنا السطح فقط هنا. إذا كنت ترغب في معرفة المزيد، نوصي بقراءة الشرح المفصل على http://kunststube.net/encoding/.
14.6.2 تنوعات الأحرف (Letter variations)
يشكل العمل بلغات تحتوي على تشكيلات أو علامات نطقية (accents) تحدياً كبيراً عند تحديد مواضع الأحرف (على سبيل المثال، باستخدام str_length() و str_sub()) حيث قد يتم ترميز الأحرف المشكولة كحرف فردي (مثل ü) أو كحرفين عن طريق دمج حرف غير مشكول (مثل u) مع علامة نطقية (مثل ¨). على سبيل المثال، يعرض هذا الكود طريقتين لتمثيل ü تبدوان متطابقتين:
لكن النصين يختلفان في الطول، وحرفاهما الأولان مختلفان:
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] TRUE14.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 في الأبجدية.
يظهر هذا أيضاً عند فرز النصوص باستخدام dplyr::arrange()، ولهذا السبب تحتوي أيضاً على وسيط locale.
14.7 ملخص
في هذا الفصل، تعلمت بعض إمكانيات حزمة stringr: كيفية إنشاء النصوص السلسلية، ودمجها، واستخراجها، وبعض التحديات التي قد تواجهها مع النصوص غير الإنجليزية. حان الوقت الآن لتعلم واحدة من أهم وأقوى الأدوات للعمل مع النصوص: التعبيرات النمطية (Regular expressions). التعبيرات النمطية هي لغة موجزة للغاية ولكنها تعبيرية جداً لوصف الأنماط داخل النصوص، وهي موضوع الفصل القادم.
أو استخدم دالة R الأساسية
writeLines().↩︎متاح في إصدار R 4.0.0 وما فوق.↩︎
تستخدم
str_view()أيضاً الألوان للفت انتباهك إلى علامات التبويب والمسافات والمطابقات وما إلى ذلك. لا تظهر الألوان حالياً في الكتاب، ولكنك ستلاحظها عند تشغيل الكود بشكل تفاعلي.↩︎إذا كنت لا تستخدم stringr، يمكنك أيضاً الوصول إليها مباشرة عبر
glue::glue().↩︎المكافئ في R الأساسية هو
paste()المستخدمة مع عاملcollapse.↩︎تنطبق نفس المبادئ على
separate_wider_position()وseparate_wider_regex().↩︎عند النظر في هذه المدخلات، نرجح أن بيانات أسماء الأطفال تحذف المسافات أو الشرطات وتقتطع الأسماء بعد 15 حرفاً.↩︎
هنا أستخدم الرمز الخاص
\xلترميز البيانات الثنائية مباشرة في النص.↩︎الفرز في اللغات التي لا تحتوي على أبجدية، مثل الصينية، أكثر تعقيداً.↩︎