تعرَّف التعابير النمطية بأنها مجموعات من المحارف التي تصف مجموعةً أخرى من المحارف أكبر منها، وهي تصف نمط المحارف الذي نستطيع البحث عنه في متن نص ما، وهي تشبه مفهوم المحارف البديلة wildcards المستخدمة في تسمية الملفات على أغلب نظم التشغيل، حيث يمكن استخدام محرف النجمة *
لتمثيل أي تسلسل من المحارف في اسم ملف، ولهذا فإن *.py
تعني أي ملف ينتهي بالامتداد .py
، بل إن المحارف البديلة ما هي إلا مجموعة فرعية صغيرة من التعابير النمطية.
وتدعم أغلب لغات البرمجة الحديثة التعابير النمطية ضمنيًا، أو لديها مكتبات أو وحدات متاحة للاستخدام في البحث عن النصوص واستبدالها وفقًا لتعابير نمطية، وذلك بسبب الإمكانيات الكبيرة لهذه التعابير، لكن شرحها المفصل هو خارج نطاق حديثنا، وستجد مصادر أخرى تتحدث عنها بتوسع شديد، وننصحك هنا بمراجعة كتب مثل كتاب أورايلي في التعابير النمطية وهو باللغة الإنجليزية، إضافةً إلى المقالات الموجودة في أكاديمية حسوب.
ولعل إحدى الخصائص المميزة للتعابير النمطية هي أنها تُظهر أوجه التشابه مع البرامج في البنية، فهي أنماط مبنية من وحدات أصغر منها، وتلك الوحدات هي:
- محارف منفردة.
- محارف بديلة.
- نطاقات أو مجموعات من المحارف، أو مجموعات محاطة بأقواس.
وبما أن المجموعة نفسها ما هي إلا وحدة، فيمكن أن تكون لدينا مجموعات من المجموعات إلى أن نصل إلى مستوى تعقيد كبير، ونستطيع جمع تلك الوحدات بطرق تشبه استخدام التسلسلات أو التكرارات أو العوامل الشرطية في لغات البرمجة، وسننظر في كل منها في حينه، فإضافةً إلى شرح مفهوم التعابير النمطية، سنعرف كيف نستخدمها في برامج بايثون، وننظر كيف تدعمها لغتا VBScript وجافاسكربت، ولنستطيع تجربة الأمثلة هنا يجب أن نستورد وحدة re
ونستخدم التوابع الخاصة بها، وسنفترض أنك استوردتها تلقائيًا دون ذكر ذلك في كل مرة.
التسلسلات
تسلسلات المحارف
لا شك أن أبسط بنية برمجية يمكن تصورها هي تسلسل من المحارف، كما أن أبسط تعبير نمطي ما هو إلا تسلسل من المحارف كذلك:
red
وهذا سيطابق أو يبحث في سلسلة نصية عن أي حدوث لهذه الأحرف الثلاثة التي تتكون منها كلمة red على الترتيب، وبناءً على ذلك سيجد كلمات مثل red وlettered وcredible، لأنها تحتوي على كلمة red ضمنها. ولنتحكم أكثر في خرج المطابقات، فإننا نوفر بعض المحارف الخاصة التي تُعرف باسم المحارف الوصفية metacharacters للحد من نطاق البحث:
التعبير | المعنى | مثال |
---|---|---|
^red
|
في بداية السطر فقط | red ribbons are good |
red$
|
في نهاية السطر فقط | I love red |
\Wred
|
في بداية الكلمة فقط | it's redirected by post |
red\W
|
في نهاية الكلمة فقط | you covered it already |
يُطلق على هذه المحارف اسم المرابط anchors لأنها تثبت موضع التعبير النمطي في جملة أو كلمة ما، وهناك عدة مرابط أخرى معرَّفة في توثيق وحدة re
يمكنك الاطلاع عليها.
المحارف البديلة
قد تحتوي التسلسلات على محارف بديلة Wildcard Characters تحل محل أي محرف، والمحرف البديل هو نقطة .
جرِّب الشيفرة التالية مثلًا:
>>> import re >>> re.match('be.t', 'best') <_sre.SRE_Match object at 0x01365AA0> >>> re.match('be.t', 'bess')
تخبرنا الرسالة التي في الأقواس السهمية أن التعبير النمطي 'be.t'
-الممرَّر وسيطًا أول- يطابق السلسلة 'best'
الممرَّرة وسيطًا ثانيًا، كما يطابق 'beat' و'bent' و'belt' وغيرها، لكن المثال الثاني لا يطابق لأن 'bess'
لا تنتهي بحرف t
، لذا لا يُنشئ MatchObject
. يمكنك تجريب عدة مطابقات أخرى لتفهم كيفية عملها، ولاحظ أن match()
لا تطابق إلا في بداية السلسلة النصية، أما لمنتصفها فنستخدم search()
كما سنرى لاحقًا.
المجالات أو الفئات
يتكون المجال range (أو الفئة set) من تجميعة من المحارف المغلَّفة في أقواس مربعة، ويبحث التعبير النمطي عن أي محرف يكون داخل هذه الأقواس:
>>> re.match('s[pwl]am', 'spam') <_sre.SRE_Match object at 0x01365AD8>
فهذا سيطابق swam
أو slam
، لكن لن يطابق sham
لأن h
غير موجودة في فئة التعبير النمطي.
أما إذا وضعنا محرف الإقحام ^
أول عنصر في المجموعة، فكأننا نقول أننا نريد البحث عن أي محرف عدا المحارف الموجودة في المجموعة:
>>> re.match('[^f]ool', 'cool') <_sre.SRE_Match object at 0x01365AA0> >>> re.match('[^f]ool','fool')
وبناءً على ذلك نستطيع مطابقة cool
وpool
، لكننا لن نطابق fool
لأننا نبحث عن أي محرف عدا f
في بداية النمط.
المجموعات
نستطيع جمع تسلسلات من المحارف أو الوحدات الأخرى معًا من خلال تغليفها بأقواس، وهي لا تقوم بدور العزل هنا، بل تفيدنا عند دمجها مع خصائص التكرار والشرطيات التي سنشرحها لاحقًا.
التكرار
يمكن إنشاء تعابير نمطية تطابق تسلسلات مكررةً من المحارف باستخدام بعض المحارف الوصفية الخاصة، ونستطيع البحث بها عن تكرار محرف واحد أو مجموعة محارف:
التعبير | المعنى | مثال |
---|---|---|
'?' | محرف واحد على الأكثر من المحارف السابقة -أي عدد صفر أو واحد من المحارف-، انتبه إلى الجزء الصفري هنا لأنه قد يربكك. | pythonl?y يطابق: pythony وpythonly |
'*' | يبحث عن صفر محرف سابق أو أكثر. |
pythonl*y يطابق ما ذكر في السطر السابق بالإضافة إلى: pythonlly وpythonllly ... إلخ |
'+' | يبحث عن محرف واحد أو أكثر من المحارف السابقة. | pythonl+y يطابق: pythonly وpythonlly وpythonllly ...إلخ |
{n,m} | يبحث عن نطاق من التكرارات من n إلى m من المحارف السابقة. | { fo{1,2 يطابق : fo أو foo |
يمكن تطبيق جميع محارف التكرار هذه على مجموعات أخرى من المحارف، وبناءً عليه:
>>> re.match('(.an){1,2}s', 'cans') <_sre.SRE_Match object at 0x013667E0>
يكون النمط هنا (.an){1,2}s
الذي يقول إن لدينا مجموعةً تتكون من أي محرف متبوع بالحرفين an
، ونريد أن نجد مجموعةً أو اثنتين متبوعتين بالحرف s
، وسيطابق هذا النمط: cancans وpans وcanpans، لكن لن يطابق bananas لانعدام وجود حرف قبل مجموعة an الثانية فيها. جرب تعديل البحث ليطابق bananas.
اقتباسإرشاد: انظر في محددات التكرار الأخرى، ولا تنس a الإضافية التي في آخر bananas.
هناك مشكلة واحدة مع نمط التكرار {m,n}
، وهو أنه لا يحد المطابقة لعدد n
من الوحدات، وعلى ذلك سيطابق مثال {fo{1,2
الذي في الجدول السابق fooo
لأنه يطابق foo
في بداية fooo
، وعليه فإذا أردنا الحد من عدد المحارف المطابَقة، فسنحتاج إلى أن نُتبع تعبير الضرب بمرساة أو نطاق نفي negated range.
وفي حالتنا، فإن [fo{1,2}[^o
سيمنع fooo
من المطابقة بما أنه يقول طابق حرف o
واحد أو حرفين متبوعين بأي شيء غير o
، لكن يجب أن يكون متبوعًا بشيء ما، لذا فإن foo
لم تَعُد مطابقةً الآن. يوضح هذا الطبيعة المتقلبة للتعابير النمطية، فقد يصعب ضبطها للحصول على الخرج الذي نريده، ويجب أن نضعها تحت اختبارات فاحصة لتجنب الأخطاء.
أما النمط الفعلي المطلوب للسماح بمطابقة foo
وfoobar
مع استثناء fooo
، فهو 'fo{1,2}[^o]*$'
، وهذا يعني fo
أو foo
المتبوعين بعدد صفر أو أكثر من حروف o
ونهاية السطر، وفي الواقع حتى هذا النمط ليس تامًا وخاليًا من الأخطاء -جرب fooboo
مثلًا-، لكن نحتاج أن نشرح بعض العناصر الأخرى قبل أن نحسّنه ونعدل فيه.
التعابير الجشعة
يقال إن التعابير النمطية جشعة، أي أن دوال البحث والمطابقة ستطابق كل ما تستطيعه من السلسلة النصية، بدلًا من التوقف عند أول مطابقة تامة، وهذا لا يهم غالبًا، لكننا سنحصل على مطابقات أكثر من المطلوب عند جمع المحارف البديلة مع عوامل التكرار. لننظر في المثال التالي: إذا كان لدينا تعبير نمطي مثل a.*b
الذي يقول إننا نريد إيجاد a
متبوعًا بأي عدد من المحارف إلى أن نصل إلى حرف b
، فستبحث دالة المطابقة من أول a
إلى آخر b
، مما يعني أنه إذا احتوت سلسلة البحث على أكثر من b
فستُضمَّن جميعًا في الجزء *.
من التعبير عدا آخر واحدة، وعليه:
re.match('a.*b','abracadabra')
فقد طابق MatchObject في هذا المثال كل abracadab
وليس أول ab
فقط، وسلوك المطابقة الجشع هذا هو أكثر الأخطاء التي يرتكبها المبرمج في بداية استخدامه للتعابير النمطية، ولمنع هذا السلوك الجشع نضيف '?'
بعد محرف التكرار، كما يلي:
re.match('a.*?b','abracadabra')
مما سيطابق ab
فقط.
الشرطيات
يتبقى لدينا الآن أن نجعل التعبير النمطي يبحث في عناصر اختيارية أو يختار نمطًا من بين عدة أنماط، وسننظر في كل منها على حدة.
العناصر الاختيارية
يمكن تحديد محرف ما ليكون اختياريًا باستخدام عدد صفر أو أكثر من محارف التكرار الوصفية:
>>> re.match('computer?d?', 'computer') <re.MatchObject instance at 864890>
هذا سيطابق compute
وcomputer
وcomputed
، كما سيطابق computerd
، لكننا لا نريد هذه الأخيرة، لذا سنضيق النطاق الذي نريده كما يلي:
>>> re.match('compute[rd]$','computer') <re.MatchObject instance at 874390>
وهذا سيختار computer
وcomputed
فقط، ويرفض computerd
، وإذا أضفنا ?
بعد هذا النطاق فسنسمح باختيار compute
مع تجنب computerd
أيضًا.
التعابير الاختيارية
بالإضافة إلى خيارات المطابقة من قائمة محارف السابقة الذكر، يمكن المطابقة بناءً على اختيار من تعابير فرعية، فقد ذكرنا سابقًا أننا نستطيع جمع تسلسلات من المحارف في أقواس، لكن الواقع أننا نستطيع جمع أي تعبير نمطي عشوائي بين أقواس ومعاملته مثل وحدة، وسنستخدم الصيغة (RE)
أثناء شرح التركيب اللغوي هنا للإشارة إلى أي تجميع لتعابير نمطية، والحالة التي نريد دراستها هنا هي مطابقة تعبير نمطي يحتوي على (RE)xxxx
أو (RE)yyyy
حيث تكون xxxx
وyyyy
أنماطًا مختلفةً، وبناءً عليه فإذا أردنا مطابقة premature
وpreventative
فسنفعل هذا بواسطة محرف الاختيار الوصفي |
:
>>> regexp = 'pre(mature|ventative)' >>> re.match(regexp,'premature') <re.MatchObject instance at 864890> >>> re.match(regexp,'preventative') <re.MatchObject instance at 864890> >>> re.match(regexp,'prelude')
نلاحظ أنه عند تعريف التعبير النمطي تعين علينا أن ندرج النص الكامل لكلا الخيارين داخل أقواس بدلًا من (e|v)
، ولو لم نفعل لاقتصر الخيار على prematureentative
وprematurventative
فقط، أي كان الحرفان فقط هما اللذان سيمثلان الخيارات المتاحة، وليس المجموعات كلها.
نستطيع الآن باستخدام هذه التقنية أن نعود إلى المثال أعلاه الذي أردنا فيه التقاط fo
أو foo
وتجنب التقاط fooo
إضافةً إلى أي شيء يأتي بعدها، وقد تركناها مع تعبير نمطي يتكون من fo{1,2}[^o]*$
، والمشكلة هنا أن التطابق سيفشل إذا احتوت السلسلة النصية التالية لـ fo
أو foo
على o
، لكن يمكن الالتفاف على ذلك باستخدام عدة خيارات من التعبيرات، ونريد أن ينجح التطابق سواء كان النمط في نهاية السطر أو متبوعًا بأي حرف سوى o
، وسيبدو ذلك كما يلي:
fo{1,2}($|[^o])
والذي سيعطينا أخيرًا ما نريده، تجدر الإشارة إلى أنه يجب تنفيذ اختبارات كافية عند استخدام التعابير النمطية، لضمان عدم التقاط أي شيء غير مرغوب فيه، وأننا نلتقط كل ما نريد التقاطه.
المزيد من الملاحظات حول التعابير النمطية
تحتوي وحدة re
على مزايا كثيرة لم نذكرها هنا، يجدر النظر فيها ودراستها من توثيق الوحدة نفسها، لكننا نريد تسليط الضوء على مجموعة من الرايات flags التي يمكن استخدامها عند تصريف التعبيرات مع دالة re.compile()
، والتي تتحكم في أمور مثل مطابقة النمط في الأسطر المختلفة، أو تجاهله لحالة الأحرف، أو غير ذلك.
ومن المهم استخدام أداة تختبر التعابير النمطية للتحقق من نتائجها، وتوجد أدوات عديدة منها على الويب، لكن نخص بالذكر منها أداة regex101، حيث نكتب فيها تعبيرًا نمطيًا وسلسلة اختبار، ثم نرى الأجزاء التي طابقها التعبير من السلسلة النصية، وهذه الأداة تحديدًا تعطينا وصفًا مفيدًا عما يفعله التعبير النمطي، وتسمح لنا باختيار أصناف فرعية للمطابَقات الناتجة، وغيرها من المزايا المفيدة.
استخدام التعابير النمطية في بايثون
رأينا في السطور السابقة شيئًا يسيرًا من التعابير النمطية، ونريد أن نطبق ذلك في بايثون، حيث نستطيع استخدامها أداة بحث قويةً جدًا في النصوص، إذ يمكن البحث عن صور كثيرة مختلفة لسلاسل نصية في عملية واحدة، بل يمكن البحث عن المحارف التي لا تُطبع مثل الأسطر الفارغة باستخدام بعض المحارف الوصفية المتاحة، كما يمكن استبدال هذه الأنماط باستخدام التوابع والدوال الخاصة بوحدة re
، كما رأينا في دالة match()
أعلاه، وفيما يلي بعض الدوال الأخرى المتاحة:
الدالة - التابع | التأثير |
---|---|
(match(RE,string | يعيد كائن مطابقة إذا طابق التعبير النمطي بداية السلسلة. |
(search(RE,string | يعيد كائن مطابقة إذا وُجد تعبير نمطي في أي مكان في السلسلة النصية. |
(split(RE, string |
مثل string.split() لكن يستخدم تعبيرًا نمطيًا مثل فاصل.
|
(sub(RE, replace, string | تعيد سلسلةً نصيةً أُنتجت عن طريق استبدال لـ re في أول مطابقة للتعبير النمطي، وهذه الدالة لها مزايا أخرى إضافية، انظر توثيقها للمزيد. |
(findall(RE, string | تبحث عن جميع مرات حدوث التعبير النمطي في سلسلة نصية، وتعيد سلسلةً من كائنات المطابقة. |
(compile(RE | تنتج كائن تعبير نمطي يمكن إعادة استخدامه لعدة عمليات مع نفس التعبير النمطي، ويكون للكائن جميع التوابع أعلاه لكن مع re مضمَّنة، ويكون أكثر كفاءةً من النسخ الخاصة بالدالة. |
لا شك أن هذه القائمة لا تحتوي جميع توابع re ودوالها، كما أن التوابع التي ذكرناها في الجدول لها معامِلات اختيارية لتوسيع استخدامها، واخترناها لأنها أكثر العمليات استخدامًا، ومناسبةً لاحتياجاتنا.
مثال عملي على التعابير النمطية
لننشئ برنامجًا يبحث في ملف HTML عن وسم IMG ليس له قسم ALT، فإذا وجدنا واحدًا فسنضيف رسالةً إلى المالك ليكتب ملفات HTML أفضل في المستقبل.
import re # لنسمح بصفر مسافة أو أكثر بين img أو IMG التقط # < و I img = '< *[iI][mM][gG] ' # alt أو ALT السماح بأي عدد من المحارف حتى before > alt = img + '.*[aA][lL][tT].*>' # افتح الملف واقرأه في قائمة filename = input('Enter a filename to search ') inf = open(filename,'r') lines = inf.readlines() # ALT بدون IMG إذا احتوى السطر على وسم # HTML فأضف رسالتنا كتعليق for index,line in enumerate(lines): if ( re.search(img,line) and not re.search(alt,line) ): lines[index] += '<!-- PROVIDE ALT TAGS ON IMAGES! -->\n' # والآن اكتب الملف المعدَّل. inf.close() outf = open(filename,'w') outf.writelines(lines) outf.close()
لدينا ملاحظتان على الشيفرة أعلاه نريد الإشارة إليهما، الأولى أننا استخدمنا re.search
بدلًا من re.match
، لأن الأولى تبحث عن الأنماط في أي مكان داخل السلسلة النصية، بينما تبحث الثانية في بداية السلسلة فقط، أما الملاحظة الثانية فهي أننا وضعنا زوجًا خارجيًا من الأقواس حول الاختبارين، وهو أمر غير ضروري لكنه يسمح لنا بتقسيم الاختبار إلى سطرين، مما يجعله أسهل في القراءة خاصةً إذا كنا سندمج تعبيرات كثيرةً.
وهذه الشيفرة ليست مثاليةً لأنها لا تأخذ في الحسبان الحالة التي يكون وسم img
فيها مقسمًا على عدة أسطر، لكنها تكفي لشرح التقنية عمومًا، على أنه يُفضل تجنب هذا "التخريب" الذي قمنا به في ملف HTML، لكن الذي ينسى وسوم alt
يستحق جزاءه.
نأتي لأمر أخير، وهو حدود كفاءة التعابير النمطية، فلهياكل البيانات المعرَّفة بوضوح -مثل HTML- أدوات أخرى غير التعابير النمطية، تُعرف باسم المحلِّلات تكون أكثر كفاءةً وأسهل في الاستخدام دون أخطاء، وسنستخدم محلل HTML في جزئية لاحقة من هذه السلسلة، وتتجلى فائدة التعابير النمطية في عمليات البحث المعقدة في النصوص الحرة إذ تحل لنا مشاكل كثيرة، مع التأكيد مرةً أخرى على الاختبار المفصل لها، كما لا يجب استخدامها إلا عند الحاجة الضرورية إليها، أما إذا كنا نريد البحث عن سلسلة بسيطة فنستخدم التابع find
، لتجنب مشاكل التعابير النمطية.
وسنعود مرةً أخرى إلى التعابير النمطية في دراسة الحالة لعدّاد القواعد النحوية، لذا جرب استخدامها حتى ذلك الحين، وتفقد التوابع الأخرى الموجودة في وحدة re، فلم نشرح حتى الآن إلا قشور هذه الأدوات بالغة القوة في معالجة النصوص.
التعابير النمطية في جافاسكربت
تدعم جافاسكربت التعابير النمطية ضمنيًا وبقوة، بل إن عمليات البحث في السلاسل النصية التي استخدمناها من قبل ما هي إلا بحوث تعابير نمطية، فقد استخدمنا أبسط صورة لها "تسلسل بسيط من المحارف"، وتنطبق جميع القواعد التي ذكرناها في بايثون على جافاسكربت، عدا أن التعابير النمطية هنا تكون محاطةً بشرطة مائلة /
بدلًا من علامات الاقتباس:
<script type="text/javascript"> var str = "A lovely bunch of bananas"; document.write(str + "<BR>"); if (str.match(/^A/)) { document.write("Found string beginning with A<BR>"); } if (str.match(/b[au]/)) { document.write("Found substring with either ba or bu<BR>"); } if (!str.match(/zzz/)) { document.write("Didn't find substring zzz!<BR>"); } </script>
ينجح التعبيران الأولان ويفشل الثالث، لذا حصلنا على الاختبار السلبي، لاحظ علامة التعجب في البداية.
التعابير النمطية في VBScript
لا تحتوي VBScript على دعم مضمّن للتعابير النمطية كما في جافاسكربت، لكن فيها كائن تعبير نمطي يمكن بدؤه واستخدامه للبحث وعمليات الاستبدال وغيرها، كما يمكن التحكم فيه لتجاهل حالة الأحرف وللبحث في جميع النسخ أو نسخة واحدة فقط:
<script type="text/vbscript"> Dim regex, matches Set regex = New RegExp regex.Global = True regex.Pattern = "b[au]" Set matches = regex.Execute("A lovely bunch of bananas") If matches.Count > 0 Then MsgBox "Found " & matches.Count & " substrings" End If </script>
نكون بهذا قد وصلنا إلى نهاية هذا المقال مكتفين بما ذكرناه فيه، لكن نعيد التأكيد على أن التعابير النمطية غنية بالتعقيدات الدقيقة التي لا يمكن تغطيتها في هذا المقال القصير، ويمكن الرجوع إلى المصادر الموجودة في الويب لمزيد من المعلومات عن استخدامها، إضافةً إلى كتاب أورايلي الذي أوردناه في بداية المقال.
خاتمة
بنهاية هذا المقال نود أن تكون قد تعلمت:
- التعابير النمطية هي أنماط نصية تستطيع تطوير قوة وكفاءة عمليات البحث النصية.
- يصعب التحكم بالتعابير النمطية، وقد تتسبب في زلات برمجية غريبة، لذا يجب التعامل معها بحرص.
- التعابير النمطية ليست الحل السهل لكل مشكلة، بل قد يكون الحل في منظور أكثر تعقيدًا، فإذا لم ينجح استخدام التعابير النمطية في حل مشكلة لثلاث محاولات متتالية، فيجب البحث عن حل آخر.
ترجمة -بتصرف- للفصل السادس عشر: Regular Expressions من كتاب Learn To Program لصاحبه Alan Gauld.
اقرأ أيضًا
- المقال التالي: البرمجة كائنية التوجه
- المقال السابق: فضاءات الأسماء في البرمجة
- ما هي البرمجة ومتطلبات تعلمها؟
- تعلم البرمجة
- التعابير النمطية (regexp/PCRE) في PHP
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.