مبادئ وتقنيات علم البيانات

الفصل الثامن: التعامل مع النصوص

فهرس الفصل:


مقدمة

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

دوال النصوص في بايثون

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

تنظيف البيانات النصية

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

state = pd.DataFrame({
    'County': [
        'De Witt County',
        'Lac qui Parle County',
        'Lewis and Clark County',
        'St John the Baptist Parish',
    ],
    'State': [
        'IL',
        'MN',
        'MT',
        'LA',
    ]
})
population = pd.DataFrame({
    'County': [
        'DeWitt  ',
        'Lac Qui Parle',
        'Lewis & Clark',
        'St. John the Baptist',
    ],
    'Population': [
        '16,798',
        '8,067',
        '55,716',
        '43,044',
    ]
})
State County  
IL De Witt County 0
MN Lac qui Parle County 1
MT Lewis and Clark County 2
LA St John the Baptist Parish 3


Population County  
16,798 DeWitt 0
8,067 Lac Qui Parle 1
55,716 Lewis & Clark 2
43,044 St. John the Baptist 3

بالطبع نريد جمع جدولي state و population معاً باستخدام العمود County. للأسف، لا تتطابق أي من أسماء المقاطعات في الجدولين. هذا المثال يوضح عدد من المشاكل في البيانات النصية وهي كالتالي:

  • الكتابة بحروف كبيرة / صغيرة: qui و Qui
  • علامات ترقيم مختلفة: St. و St
  • عدم كتابة بعض الكلمات: مثلاً كلمتي County/Parish لم يتم كتابتها في جدول population.
  • إضافة مسافات: DeWitt و De Witt.
  • استخدام مُختلف للاختصارات: & و and.

دوال النصوص

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

الدالة الوصف
str[x:y]  تقوم الداله بفصل النص str من x (تشمل الحرف) حتى y (لا تشمل الحرف). 
()str.lower  تنشئ نسخه من النص str بعد تحويل جميع الحروف فيه إلى حروف صغيره. 
str.replace(a, b)  تحويل جميع مرات ظهور النص/الحرف a في  str إلى النص/الحرف b
str.split(a)  تقوم الدالة بفصل النص str عند ظهور النص/الحرف a
()str.strip  تحذف الداله المسافات الفارغه في بداية ونهاية النص str 

قمنا باختيار النص St. John the Baptist من جدولي state و population لتطبيق دوال النصوص بحذف الحروف الكبيرة، علامات الترقيم، والتخلص من الكلمتين county و parish:

john1 = state.loc[3, 'County']
john2 = population.loc[3, 'County']

(john1
 .lower()
 .strip()
 .replace(' parish', '')
 .replace(' county', '')
 .replace('&', 'and')
 .replace('.', '')
 .replace(' ', '')
)
'stjohnthebaptist'

تطبيق نفس الدوال على john2 يؤكد لنا أن النصين مُتطابقان:

(john2
 .lower()
 .strip()
 .replace(' parish', '')
 .replace(' county', '')
 .replace('&', 'and')
 .replace('.', '')
 .replace(' ', '')
)
'stjohnthebaptist'

عند الوصول لنتيجة مُقنعه، نقوم بتعريف دالة باسم clean_county والتي ستطبق مهام تنظيف المدخلات (المقاطعات):

def clean_county(county):
    return (county
            .lower()
            .strip()
            .replace(' county', '')
            .replace(' parish', '')
            .replace('&', 'and')
            .replace(' ', '')
            .replace('.', ''))

يمكننا التأكد أن الدالة clean_county تقوم بتوحيد جميع أسماء المقاطعات عبر تطبيقها على الجدولين:

([clean_county(county) for county in state['County']],
 [clean_county(county) for county in population['County']]
)
(['dewitt', 'lacquiparle', 'lewisandclark', 'stjohnthebaptist'],
 ['dewitt', 'lacquiparle', 'lewisandclark', 'stjohnthebaptist'])

بما أن كلا العامودين الآن يحتويان على نفس اسم المقاطعة وبنفس الشكل، يمكننا جمع الجدولين معاً باستخدام اسم المقاطعة.

دوال النصوص في بانداز

في الكود البرمجي السابق، استخدمنا التكرار Loop لتغير أسماء المقاطعات. مصفوفات pandas تقدم طريقة أسهل لتطبيق دوال النصوص لجميع محتوى المصفوفة. أولا، لنرى أسماء المقاطعات في جدول state:

state['County']
0                De Witt County
1          Lac qui Parle County
2        Lewis and Clark County
3    St John the Baptist Parish
Name: County, dtype: object

الدالة .str في مصفوفات بانداز تتعامل مع النص كما في بايثون. استخدام الدوال على .str يطبقها على جميع القيم في المصفوفة:

state['County'].str.lower()
0                de witt county
1          lac qui parle county
2        lewis and clark county
3    st john the baptist parish
Name: County, dtype: object

يُمكنا ذلك من تحويل جميع النصوص في المصفوفة دون الحاجة لاستخدام التكرار:

(state['County']
 .str.lower()
 .str.strip()
 .str.replace(' parish', '')
 .str.replace(' county', '')
 .str.replace('&', 'and')
 .str.replace('.', '')
 .str.replace(' ', '')
)
0              dewitt
1         lacquiparle
2       lewisandclark
3    stjohnthebaptist
Name: County, dtype: object

نعيد حفظ نتيجة تغير شكل عمود المقاطعات على نفس العمود:

state['County'] = (state['County']
 .str.lower()
 .str.strip()
 .str.replace(' parish', '')
 .str.replace(' county', '')
 .str.replace('&', 'and')
 .str.replace('.', '')
 .str.replace(' ', '')
)

population['County'] = (population['County']
 .str.lower()
 .str.strip()
 .str.replace(' parish', '')
 .str.replace(' county', '')
 .str.replace('&', 'and')
 .str.replace('.', '')
 .str.replace(' ', '')
)

والآن بما أن كلا الجدولين لديهما نفس التعبير النصي للمقاطعات:

state
State County  
IL dewitt 0
MN lacquiparle 1
MT lewisandclark 2
LA stjohnthebaptist 3
population
Population County  
16,798 dewitt 0
8,067 lacquiparle 1
55,716 lewisandclark 2
43,044 stjohnthebaptist 3

من السهل جمع الجدولين عندما يتطابق عمود المناطق:

state.merge(population, on='County')
Population State County  
16,798 IL dewitt 0
8,067 MN lacquiparle 1
55,716 MT lewisandclark 2
43,044 LA stjohnthebaptist 3

ملخص دوال النصوص

توفر بايثون دوال سهله وعملية للتعامل وتعديل النصوص. مصفوفات بانداز توفر نفس الدوال وتسهل تطبيقها على جميع القيم داخل المصفوفة.

يمكنك تصفح شرح كامل عن دوال النصوص في بايثون هنا ودوال النصوص في بانداز هنا

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

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

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

"give me a call, my number is 123-456-7890."

رقم الهاتف يحتوي على الأنماط التالية:

  • ثلاث أرقام.
  • متبوعه بخط فاصل.
  • متبوعه بثلاث أرقام.
  • متبوعه بخط فاصل.
  • متبوعه بأربع أرقام.

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

للتحقق إذا كان النص يحتوي على رقم هاتف، قد نعرف دالة كالتالي:

def is_phone_number(string):
    
    digits = '0123456789'
    
    def is_not_digit(token):
        return token not in digits 
    
    # ثلاث ارقام
    for i in range(3):
        if is_not_digit(string[i]):
            return False
    
    # متبوعه بخط فاصل
    if string[3] != '-':
        return False
    
    # متبوعه بثلاث ارقام
    for i in range(4, 7):
        if is_not_digit(string[i]):
            return False
        
    # متبوعه بخط فاصل   
    if string[7] != '-':
        return False
    
    # متبوعه بأربع ارقام
    for i in range(8, 12):
        if is_not_digit(string[i]):
            return False
    
    return True

وللتحقق:

is_phone_number("382-384-3840")
True
is_phone_number("phone number")
False

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

التعابير النمطية Regular expressions (وبشكل مختصر RegEx) تحل هذه المشكلة بجعلنا ننشأ الأنماط للنصوص. باستخدامها، يمكننا إعادة تعريف الدالة السابقة is_phone_number بسطرين في بايثون:

import re

def is_phone_number(string):
    regex = r"[0-9]{3}-[0-9]{3}-[0-9]{4}"
    return re.search(regex, string) is not None

is_phone_number("382-384-3840")
True

في المثال السابق، استخدمنا التعبير النمطي [0-9]{3}-[0-9]{3}-[0-9]{4} لمطابقة أرقام الهواتف. ربما يبدو مبهماً في البداية، ولكن أوامر التعابير النمطية سهلة للتعلم؛ في هذا الجزء شرحنا كل الأوامر.

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

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

سنبدأ أولاً بالتعرف على طريقة الكتابه. التعابير النمطية عادةً ما تُحفظ على شكل نصوص خام Raw String. تعمل تماماً كما تعمل النصوص في بايثون ولكن دون قوانين خاصة للخطوط المائله \.

الخط المائل \ يستخدم لتجاهل ما بعدهEscape. مثلاً، اذا اردنا كتابة It's raining، نحتاج لإضافة \ قبل ' لأن علامة الإقتباس تستخدم لحفظ النص:

print('It\'s raining')
It's raining 

تستخدم ايضاً \ لإضافة سطر جديد باستخدام n\ أو اضافة مسافة باستخدام t\

مثلاً لحفظ النص hello \ world في بايثون، نحتاج لكتابة:

# الخطوط المائلة في بايثون تحتاج للتوضيح ليتم تجاهلها في نصوص بايثون العاديه
some_string = 'hello \\ world'
print(some_string)
hello \ world

استخدام النصوص الخامة يحذف شرط إضافة رمز لتجاهل الخطوط المائلة:

لإخبار بايثون أن النص التالي يعتبر نص خام Raw String, نقوم بإضافة r قبل النص

# لاحظ ال `r` قبل بداية النص
some_raw_string = r'hello \ world'
print(some_raw_string)
hello \ world

بما ان الخطوط المائلة تظهر بشكل كثير في التعابير النمطية، سنستخدم النصوص الخامة لكتابة جميع التعابير النمطية في هذا الجزء.

حرفياً

إيجاد حرف بشكل معين بالتحديد في التعبير النمطي يوجد لنا ذلك الحرف فقط. مثلاً، التعبير "r"a يوجد لنا أي "a" في Say! I like green eggs and ham!. جميع الأرقام والأحرف ورموز الترقيم يمكن البحث عنها بشكل خاص في التعابير النمطية:

def show_regex_match(text, regex):
    """
    تطبع النص مع تغير اللون لتحديد نمط regex
    """
    print(re.sub(f'({regex})', r'\033[1;30;43m\1\033[m', text))
regex = r"green"
show_regex_match("Say! I like green eggs and ham!", regex)
Say! I like green eggs and ham!
show_regex_match("Say! I like green eggs and ham!", r"a")
Say! I like green eggs and ham!

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

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

التعابير النمطية تتحسس لحالة وشكل الحرف. في المثال التالي، التعبير يطابق فقط الحرف الصغير s في eggs، وليس الحرف الكبير S في Say.

show_regex_match("Say! I like green eggs and ham!", r"s")
Say! I like green eggs and ham!

رموز خاصة

بعض الرموز لديها معاني خاصة في RegEx. هذه الرموز تساعدنا على إيجاد أنماط مختلفة من النصوص.

الرمز ` ` (نقطة) تعني أنه تطابق أي حرف عدى أن يكون ببداية سطر جديد:

show_regex_match("Call me at 382-384-3840.", r".all")
Call me at 382-384-3840.

لنطابق النقطة بشكل خاص يجب علينا تجاهلها كرمز بإضافة \:

show_regex_match("Call me at 382-384-3840.", r"\.")
Call me at 382-384-3840.

باستخدام النقطة يمكننا مطابقة أنواع مختلفة من الأنماط، سنقوم بكتابة تعبير نمطي لمطابقة أرقام الهواتف. مثلاً، يمكنا أخذ الرقم 382-384-3840 وتبديل كل رقم بنقطة .، لتبقى لنا فقط الخطوط الفاصلة. النتيجة كالتالي ...-...-....:

show_regex_match("Call me at 382-384-3840.", "...-...-....")
Call me at 382-384-3840.

بما أن النقطة تطابق جميع النصوص، النص التالي سيظهر لنا نتيجة خاطئة، لأننا نريد إيجاد الأرقام فقط وليس الحروف:

show_regex_match("My truck is not-all-blue.", "...-...-....")
My truck is not-all-blue.

المجموعات النصية

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

show_regex_match("I like your gray shirt.", "gr[ae]y")
I like your gray shirt.
show_regex_match("I like your grey shirt.", "gr[ae]y")
I like your grey shirt.
# لا يطابق، المجموعات تطابق حرف واحد فقط من ما تحتويها وليس جميعها
show_regex_match("I like your graey shirt.", "gr[ae]y")
I like your graey shirt.
# في هذا المثال، كتابة المجموعه مره اخرى ستطابقها
show_regex_match("I like your graey shirt.", "gr[ae][ae]y")
I like your graey shirt.

في مجموعات النصوص، الرمز . سيتخدم كحرف وليس كرمز:

show_regex_match("I like your grey shirt.", "irt[.]")
I like your grey shirt.

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

الإختصار المعنى
[0-9] جميع الارقام
[a-z] الأحرف الصغيره
[A-Z] الأحرف الكبيره


show_regex_match("I like your gray shirt.", "y[a-z]y")
I like your gray shirt.

تُمكنا المجموعات النصية من إنشاء أنماط خاصة بأرقام الهواتف:

# استبدلنا النقاط `.` في ...-...-.... ب [0-9] لتحديد النمط على الأرقام
phone_regex = r'[0-9][0-9][0-9]-[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]'
show_regex_match("Call me at 382-384-3840.", phone_regex)
Call me at 382-384-3840.
# والآن لا نطابق هذا النص
show_regex_match("My truck is not-all-blue.", phone_regex)
My truck is not-all-blue.

عكس المجموعات النصية

عكس المجموعات يطابق أي نص غير الموجود في المجموعة. لإنشاء هذا التعبير، نضيف النص داخل الأقواس [^ ]:

show_regex_match("The car parked in the garage.", r"[^c]ar")
The car parked in the garage.

الحدود الرقمية

لإنشاء تعبير نمطي لمطابقة أرقام الهواتف، كتبنا التالي:

[0-9][0-9][0-9]-[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]

هذا النمط يطابق ثلاثة أرقام، متبوعه بخط، ثم ثلاثة أرقام وخط ثم أربعة أرقام أخرى.

الحدود الرقمية تسمح لنا مطابقة التكرار في النمط. نحدد عدد مرات التكرار بوضع الرقم داخل أقواس معقوفة { }:

phone_regex = r'[0-9]{3}-[0-9]{3}-[0-9]{4}'
show_regex_match("Call me at 382-384-3840.", phone_regex)
Call me at 382-384-3840.
# لا تطابق لأن اول جزء مكون من رقمين
phone_regex = r'[0-9]{3}-[0-9]{3}-[0-9]{4}'
show_regex_match("Call me at 12-384-3840.", phone_regex)
Call me at 12-384-3840.

الحدود تطابق جميع النصوص أو الفئات النصية التي كُتبت قبلها. الجدول التالي يظهر شرح لها:

رمز الحدود الرقمية المعنى
{m, n} مطابقة تكرار الحرف من m إلى n مرات.
{m} مطابقة تكرار الحرف m مرات.
{m,} مطابقة تكرار الحرف على الأقل m مرات.
{,n} مطابقة تكرار الحرف بالأكثر n مرات.
اختصارات الحدود الرقمية

الكثير من الحدود الرقمية التي تستخدم كثيراً تم اختصارها:

الرمز رمز الحدود الرقمية المفصل المعنى
* {0,} مطابقة تكرار الحرف 0 أو أكثر.
+ {1,} مطابقة تكرار الحرف مرة واحدة أو أكثر.
? {0,1} مطابقة تكرار الحرف 0 أو مرة واحدة.

نستخدم الرمز * بدلاً من {0,} في الأمثلة التالية:

# تكرار a ثلاث مررات
show_regex_match('He screamed "Aaaah!" as the cart took a plunge.', "Aa*h!")
He screamed "Aaaah!" as the cart took a plunge.
# مطابقة المزيد من a
show_regex_match(
    'He screamed "Aaaaaaaaaaaaaaaaaaaah!" as the cart took a plunge.',
    "Aa*h!"
)
He screamed "Aaaaaaaaaaaaaaaaaaaah!" as the cart took a plunge.
# عدم وجود a بشكل صغير
show_regex_match('He screamed "Ah!" as the cart took a plunge.', "Aa*h!")
He screamed "Ah!" as the cart took a plunge.
الحدود الكمية جشعه!

توجد لنا دائماً الحدود الكمية أكثر القيم تطابقاً. تظهر بعض المرات نتائج غير متوقعة:

# حاولنا إيجاد 311 و 911 ولكن تم ايجاد and ايضاً
# لأن
# <311> and <911>
# هي اعلى قيمه ممكنه للنمط <.+>
show_regex_match("Remember the numbers <311> and <911>", "<.+>")
Remember the numbers <311> and <911>

في هذا المثال نلاحظ أن التعبير النمطي لم يتوقف عند أول ظهور ل > بس أستمر إلى آخر تكرار، يقصد هنا أن الحدود الكمية تبحث دائماً عن المزيد ولا تتوقف عند تكرار واحد إلا إذا حددنا ذلك

في كثير من الحالات، استخدام مجموعات النصوص يسهل علينا تفادي هذه النتائج الخاطئة:

show_regex_match("Remember the numbers <311> and <911>", "<[0-9]+>")
Remember the numbers <311> and <911>

تحديد المكان

في بعض الأحيان نريد أن يتطابق النمط إذا كان في بداية أو نهاية النص. الرمز الخاص ^ يجعل التعبير يطابق النمط عندما يظهر في بداية النص فقط؛ والرمز الآخر الخاص $ يطابق النمط إذا ظهر في نهاية النص. مثلاً التعبير النمطي well$ فقط يطابق ظهور كلمة well في نهاية النص:

show_regex_match('well, well, well', r"well$")
well, well, well

باستخدام كلا الرمزين ^ و $ يجعل التعبير يطابق النمط كاملاً:

phone_regex = r"^[0-9]{3}-[0-9]{3}-[0-9]{4}$"
show_regex_match('382-384-3840', phone_regex)
382-384-3840
# لا يطابق
# لأنه يبدأ بنص وليس ثلاثة أرقام
# وينتهي بنقطة وليس اربع ارقام
show_regex_match('You can call me at 382-384-3840.', phone_regex)
You can call me at 382-384-3840.

تجاهل الرموز الخاصة

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

show_regex_match("Call me at [382-384-3840].", "\[")
Call me at [382-384-3840].
show_regex_match("Call me at [382-384-3840].", "\.")
Call me at [382-384-3840].

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

قمنا بالحديث عن أهم النقاط والرموز في التعابير النمطية. ليكون لدينا مرجع كامل، أضفنا الجدول التالي.

مرجع الرموز الخاصة

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

الرمز الوصف مثال التعبير النمطي يطابق لا يطابق
. يطابق أي حرف عدا سطر جديد abc ab
        abcd
[ ] أي حرف داخل الأقواس a[cb.]ar car jar
      .ar  
[^ ] أي حرف غير ما داخل الأقواس a[^b]ar car bar
      par ar
* صفر أو أكثر تكرار لما يسبق الرمز a[pb]*ark bbark dark
      ark  
+ واحد أو أكثر تكرار لما يسبق الرمز a[pb]+ark bbpark dark
      bark ark
? صفر أو مرة واحد تكرار لما يسبق الرمز s?he she the
      he  
{n} مطابقة عدد مرات التكرار n hello{3} hellooo hello
| مطابقة أي نمط بين خيارين we|[ui]s we e
      us s
      is  
\ تجاهل الرموز الخاصة [\hi]\ [hi] hi
^ بداية السطر a^ark ark two dark
$ نهاية السطر $aark noahs ark noahs arks

مرجع الاختصارات

بعض من الفئات المختصرة:

الرمز المختصر التعبير النمطي الكامل للفئة الوصف
a\w [a-zA-Z0-9] أي حرف صغير/كبير أو رقم
a\W a[^a-zA-Z0-9] غير أي حرف صغير/كبير أو رقم
a\d [0-9] ارقام
a\D a[^0-9] غير الأرقام
a\s a[\t\n\f\r\p{Z}] المسافات
a\S a[^\t\n\f\r\p{z}] غير المسافات

ملخص التعابير النمطية RegEx

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

التعابير النمطية في بايثون وبانداز

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

RE.SEARCH

الدالة re.search(pattern, string) تطابق جميع الأنماط داخل المتغير pattern في النص string. يكون نتيجة الدالة أما النص إذا وجد تطابق أو None إن لم يجد:

phone_re = r"[0-9]{3}-[0-9]{3}-[0-9]{4}"
text  = "Call me at 382-384-3840."
match = re.search(phone_re, text)
match
<_sre.SRE_Match object; span=(11, 23), match='382-384-3840'>

عل الرغم أن الكائن الذي أنشئ نتيجة لعملية البحث يحتوي على الكثير من الخصائص المفيدة، نحن عادةً نستخدم re.search للاختبار والتأكد في حال ظهور النمط في النص:

if re.search(phone_re, text):
    print("Found a match!")
Found a match!
if re.search(phone_re, 'Hello world'):
    print("No match; this won't print")

دالة أخرى كثيره الاستخدام هي re.match(pattern, string) وتعمل بنفس طريقة عمل re.search ولكن تقوم بالبحث عن تطابق فقط في بداية النص string وليس في أي جزء آخر منه.

RE.FINDALL

نستخدم الدالة re.findall(pattern, string) لإيجاد جميع النتائج التي تطابق النمط. هذه الدالة تنتج لنا مصفوفة تحتوي على جميع المطابقات للنمط الذي يكتب في المتغير pattern في النص الذي يكتب في المتغير string:

gmail_re = r'[a-zA-Z0-9]+@gmail\.com'
text = '''
From: email1@gmail.com
To: email2@yahoo.com and email3@gmail.com
'''
re.findall(gmail_re, text)
['email1@gmail.com', 'email3@gmail.com']

مجموعات RegEx

استخدام مجموعات RegEx يسمح لنا بمطابقة أنماط متعددة واستخراجها منفصله عن طريق إضافتها داخل أقواس ( ). عندما يحتوي التعبير النمطي على مجموعات، الدالة re. findall تنتج لنا مصفوفة Tuple تحتوي على نتيجة النمط مفصله.

مثلاً، التعبير النمطي التالي يستخرج أرقام الهواتف من النص:

phone_re = r"[0-9]{3}-[0-9]{3}-[0-9]{4}"
text  = "Sam's number is 382-384-3840 and Mary's is 123-456-7890."
re.findall(phone_re, text)
['382-384-3840', '123-456-7890']

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

# نفس التعبير السابق مع اضافة اقواس بين كل 3/4 ارقام
phone_re = r"([0-9]{3})-([0-9]{3})-([0-9]{4})"
text  = "Sam's number is 382-384-3840 and Mary's is 123-456-7890."
re.findall(phone_re, text)
[('382', '384', '3840'), ('123', '456', '7890')]

كما توقعنا، re.findall تنتج لنا مجموعه تحتوي على جميع ما يطابق نمط أرقام الهواتف.

RE.SUB

الدالة re.sub(pattern, replacement, string) تستبدل جميع تكرارات النمط المكتوب في المتغير pattern بالنص البديل في المتغير replacement في النص الموجود في المتغير string. هذه الدالة تعمل كما تعمل دالة str.sub في بايثون ولكن تستخدم التعبير النمطي.

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

messy_dates = '03/12/2018, 03.13.18, 03/14/2018, 03:15:2018'
regex = r'[/.:]'
re.sub(regex, '-', messy_dates)
'03-12-2018, 03-13-18, 03-14-2018, 03-15-2018'

RE.SPLIT

الدالة re.split(pattern, string) تقوم بفصل النص المدخل في المتغير string في كل مرة يظهر فيها النمط في المتغير pattern. تعمل هذه الدالة بنفس طريقة عمل دالة str.split في بايثون ولكن تستخدم التعبير النمطي لإتمام عملية الفصل.

في الكود البرمجي التالي، استخدمنا re. split لفصل أسماء الفصول من أرقام الصفحات في جدول محتوى الكتاب:

toc = '''
PLAYING PILGRIMS============3
A MERRY CHRISTMAS===========13
THE LAURENCE BOY============31
BURDENS=====================55
BEING NEIGHBORLY============76
'''.strip()

# اولاً فصل كل سطر على حده
lines = re.split('\n', toc)
lines
['PLAYING PILGRIMS============3',
 'A MERRY CHRISTMAS===========13',
 'THE LAURENCE BOY============31',
 'BURDENS=====================55',
 'BEING NEIGHBORLY============76']
# ثم الفصل إلى اسم الفصل ورقم الصفحه
split_re = r'=+' # Matches any sequence of = characters
[re.split(split_re, line) for line in lines]
[['PLAYING PILGRIMS', '3'],
 ['A MERRY CHRISTMAS', '13'],
 ['THE LAURENCE BOY', '31'],
 ['BURDENS', '55'],
 ['BEING NEIGHBORLY', '76']]

التعابير النمطية و بانداز

تذكر أن مصفوفات بانداز تحتوي على دالة .str والتي تدعم دوال التعامل مع النصوص في بايثون. بنفس الطريقة، الدالة .str تدعم أيضا بعض الدوال من مكتبة re. سنشرح بعض الاستخدامات البسيطة للتعابير النمطية في بانداز وللمزيد من الدوال في الصفحة الرئيسية لدوال النصوص في بانداز.

قمنا بحفظ نص بسيط من أول خمس جمل من رواية Little Women في DataFrame. باستخدام دوال النصوص في بانداز يمكننا استخراج نصوص الحوارات المنطوقة في كل جمله:

text = '''
"Christmas won't be Christmas without any presents," grumbled Jo, lying on the rug.
"It's so dreadful to be poor!" sighed Meg, looking down at her old dress.
"I don't think it's fair for some girls to have plenty of pretty things, and other girls nothing at all," added little Amy, with an injured sniff.
"We've got Father and Mother, and each other," said Beth contentedly from her corner.
The four young faces on which the firelight shone brightened at the cheerful words, but darkened again as Jo said sadly, "We haven't got Father, and shall not have him for a long time."
'''.strip()
little = pd.DataFrame({
    'sentences': text.split('\n')
})
little
  sentences
0 a“Christmas won’t be Christmas without any pres…a
1 a“It’s so dreadful to be poor!” sighed Meg, loo…a
2 a“I don’t think it’s fair for some girls to hav…a
3 a“We’ve got Father and Mother, and each other,”…a
4 The four young faces on which the firelight sh…a

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

quote_re = r'"[^"]+"'
little['sentences'].str.findall(quote_re)
0    ["Christmas won't be Christmas without any pre...
1                     ["It's so dreadful to be poor!"]
2    ["I don't think it's fair for some girls to ha...
3     ["We've got Father and Mother, and each other,"]
4    ["We haven't got Father, and shall not have hi...
Name: sentences, dtype: object

بما أن الدالة Series.str.findall توجد لنا جميع التطابقات، بانداز توفر لنا دالتي Series.str.extract و Series.str.extractall لاستخراج التطابقات إلى مصفوفة أو DataFrame. هذه الدوال تشرط علينا أن يحتوي التعبير النمطي على الأقل علي مجموعه واحده:

# ايجاد النصوص بين علامتي اقتباس
quote_re = r'"([^"]+)"'
spoken = little['sentences'].str.extract(quote_re)
spoken
0    Christmas won't be Christmas without any prese...
1                         It's so dreadful to be poor!
2    I don't think it's fair for some girls to have...
3         We've got Father and Mother, and each other,
4    We haven't got Father, and shall not have him ...
Name: sentences, dtype: object

يمكننا إضافة ذلك كعمود في little:

little['dialog'] = spoken
little
  sentences dialog
0 a“Christmas won’t be Christmas without any pres…a Christmas won’t be Christmas without any prese…a
1 a“It’s so dreadful to be poor!” sighed Meg, loo…a It’s so dreadful to be poor!a
2 a“I don’t think it’s fair for some girls to hav…a I don’t think it’s fair for some girls to have…a
3 a“We’ve got Father and Mother, and each other,”…a We’ve got Father and Mother, and each other,a
4 The four young faces on which the firelight sh…a We haven’t got Father, and shall not have him …a

يمكننا أن نؤكد أن تلاعبنا بالنصوص باستخدام التعابير النمطية يعمل بالشكل الذي نرغب به في آخر جمله في ال DataFrame عن طريق طباعة النص الأصلي والنتيجة بعد المطابقة:

print(little.loc[4, 'sentences'])
The four young faces on which the firelight shone brightened at the cheerful words, but darkened again as Jo said sadly, "We haven't got Father, and shall not have him for a long time."
print(little.loc[4, 'dialog'])
We haven't got Father, and shall not have him for a long time.

ملخص التعابير النمطية في بايثون وبانداز

مكتبة re في بايثون توفر لنا الكثير من الدوال المفيدة للتلاعب بالنصوص باستخدام التعابير النمطية. عند العمل على DataFrame، نستخدم الدالة المماثلة للتلاعب بالنصوص باستخدام التعابير النمطية فيه بانداز.

لعرض كامل شرح مكتبة re، اضغط هنا.

لعرض كامل شرح دوال النصوص في بانداز، اضغط هنا.