ما هي التعبيرات المنتظمة؟
قبل بضع سنوات ، كنت أفعل بعض التحقق من صحة المدخلات في مربع إدخال على نموذج ويب Web Form. بحيث يمكن للمستخدمين إدخال رقم هاتف في النموذج، ثم يتم طباعة الرقم في الملف الشخصي للمستخدم.
فيمكن إدخال رقم الهاتف بعدة طرق:
- 5555-555 (555)
- 555-555-5555
ولكن 555-5555 غير مقبول. هنا قد نتساءل لماذا لم نستبعد جميع الرموز الغير رقمية ونحصيها لمعرفة ما إذا كان ما مجموعها 10 أرقام.
ربما يكون هذا التفكير سليماً، لكنها لن تمنع المستخدمين من إدخال شيء من هذا القبيل ! 555؟ 333-3333. ومن وجهة نظر مطور الويب ،كان بإمكاني كتابة إجراءات لفحص جميع الأشكال المتوقعة للمدخلات، ولكني أردت حلًا يكون مرناً أكثر.
هنا يأتي دور الـ (regexes)، فقديما كنت أبحث عن التعبير المناسب للبحث عن نص معين ثم أنسخه وأستخدمه دون فهم لكيفية عمله، ولكن اتضح أنها مثل الكثير من التعبيرات الرياضية. فعندما تنظر إلى تعبيرمثل 2 × 2 = 4 ، فأنت تفكر عادة “اثنان ضرب اثنان يساوي أربعة.” التعبيرات المنتظمة مشابه جدا لذلك. وفي نهاية دراسة التعبيرات المنتظمة، سترى نمطاً مثل $b^ وتقول لنفسك ، “بداية السطر متبوعة بـ b ، ثم نهاية السطر.” ليس ذلك فحسب ولكن سوف تتعلم الكثير من المهارات التي تجعل من استخدام التعبيرات المنتظمة أكثر متعة.
متى نستخدم التعبيرات المنتظمة؟
تستخدم التعبيرات المنتظمة عندم يكون هناك قواعد يجب أن تنطبق على شيء ما ولا يوجد دالة مدمجة في لغة البرمجة تقوم بتطبيق هذه القواعد. فلايوجد دالة مدمجة في PHP مثلاً تقوم بالتحقق من أن يكون شكل رقم الهاتف 2588-22-566.
ولكن هناك هناك دوال تقوم بأمور أخرى، فمثلاً أنت تريد أن تتأكد من أن ما تم إدخاله هو بريد إلكتروني صحيح، فيمكن أن نفحص ذلك عن طريق التعبير المنتظم التالي ^\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}$ ولكن لماذا تعتمد على ذلك، مادامت هناك الدالة filter_var أو filter_input
filter_var ( mixed $variable [, int $filter = FILTER_DEFAULT [, mixed $options ]] ) : mixed
حيث يمكنك تمرير الإيميل للدالة وتطبيق الفلتر FILTER_VALIDATE_EMAIL
<?php $email = "john.doe@example.com"; if (filter_var($email, FILTER_VALIDATE_EMAIL)) { echo("$email is a valid email address"); } else { echo("$email is not a valid email address"); } ?>
ولكن إذا كان النص الذي تريد البحث عنه، له قواعد مخصصة تختلف عن القواعد العامة( التي ربما تجد لها دوال مدمجة لتنفيذها مثل البريد الإلكتروني)، عليك باللجوء للتعابير النمطية.
وبالرغم من أن التعبيرات المنتظمة قوية، فلديها بعض السلبيات. أحدها هو مجموعة المهارات المطلوب لقراءة وكتابة التعبيرات. لذا إذا قررت دمج التعبيرات المنتظمة في التطبيق الخاص بك ، يجب عليك إضافة التعليقات عليها بالكامل وشرحها؛ بحيث، إذا احتاج شخص آخر إلى تغيير التعبير ، يمكنهم القيام بذلك دون كسر للوظائف. بالإضافة إلى ذلك ، إذا كنت حديث العهد بالتعبيرات المنتظمة ، فقد تجد أنه يصعب تقفي وتتبع أخطاءها. لتجنب هذه الصعوبات ، لا تستخدم regexes في الوقت الذي تتوفر فيه دالة/وظيفة مضمنة تقوم بعمل ذلك.
صياغة التعبير المنتظم
لكي يفهم المحرك النمط المكتوب، فلابد أن يكتب بالقواعد التي يفهمها المحرك، وهي:
- النمط يجب أن يكتب بين محددات مثل / ، مثل /a\d+/ وهنا النمط هو a\d+ والمحدد هو // ، وهناك محددات أخرى يمكن استخدامها بدلاً عن / وهي # و % وغيرها يمكنك مراجعتها من هنا ولكن سنتحدث عن أهميتها لاحقاً.
- يكتب داخل المحدد الرموز والأحرف التي تحدد عمل محرك التعبير المنتظم، سنفصلها لاحقاً
- إذا كان المُحدِد سيستخدم بنفسه داخل التعبير بيجب عمل له تخطي escape عن طريق استخدام \ .
- إذا كنت تريد البحث أن أي من هذه الرموز كجزء من النص وليس كرمز خاص للنمط عليك بتخطيها عن طريق استخدام \ .
مكونات التعبير المنتظم
أول شيء يجب التعرف عليه عند استخدام التعبيرات المنتظمة هو أن كل شيء هو في الأساس حرف ، ونقوم بكتابة الأنماط لتتطابق مع تسلسل محدد من الحروف. وبما أن الرموز في الحاسب الألي كل منها يتبع أسلوب ترميز معين Character encoding، فإن محرك التعبير المنتظم يفهم الأنماط التي تتبع الترميز ASCII، والتي تتضمن الأحرف والأرقام وعلامات الترقيم والرموز الأخرى على لوحة المفاتيح مثل٪ # $ @! ، ولكن يمكن أيضًا يمكن استخدام الرموز التي تتبع الترميز unicode لمطابقة أي نوع من النصوص الدولية ولها طرق معينة.
كما ذكرنا أن التعبير المنتظم عبارة عن مجموعة من الرموز التي لها معنى لدى محرك تنفيذ النمط، هذه الرموز تم تقسيمها حسب وظيفتها إلى:
- Special Characters
- word boundaries and assertions
- Line anchors
- Quantifiers
- Groups and Ranges
- character classes
- Escape and Escape Sequences
- Pattern Modifiers
وأمور أخرى نتحدث عنها باستفاضة في مواضيع منفصلة.
فكرة عمل التعبير المنتظم
التعبير المنتظم يقوم بالبحث داخل نص أو مجموعة من النصوص عن نص معين بتطبيق النمط ما، ولكي نستطيع تصور فكرة العمل سنأخذ المثال التالي.
تخيل أنك مطلوب منك حصر الأشجار في طريق ما، هذا الطريق يوجد به الأشجار بشكل متتالي في صف واحد ولكن يتخلل هذا الصف أعمدة إنارة، فبالتالي يكون السيناريو المتوقع لحصر الأشجار كالآتي:
- تبدأ من أول الطريق قبل أول شجرة
- تسأل نفسك هل أرى شجرة؟ (نفترض أنها شجرة) تكون الإجابة نعم وتقوم بوضع رقم 1 على أول شجرة
- ثم تتخطى الشجرة الأولى لتقف بعدها لفحص ما يليها، (لنفترض أن ما يليها هو عامود). هل أرى شجرة؟ تكون الإجابة لا وتضع علامة x (عملياً لايحدث أنك تضع x لكن للتوضيح أنك أثناء وقوفك في هذا المكان لم ترى شجرة)
- ثم تتخطى العامود ، لتقف بعده فتجد أن ما بعده شجرة فتقوم بترقيم الشجرة.
- وهكذا في كل مرة ترى شجرة تقوم بترقيمها، أو تضع علامة x في مكان وقوفك لأنك لم ترى شجرة.
وهذا هو المنطق الذي يتعامل به محرك التعبير المنتظم، وهو أنه يفحص النقطة التالية (لاحظ أنا اقول النقطة التالية وليس التي يقف عندها، ففي مثال الشجرة كنا نفحص الشجرة بدون الوصول إليها وإنما عندما نراها بأعيننا أمامنا).
ويمكننا تطبيق هذا التصور على محرك التعبير المنتظم، بحيث عندما نبدأ مثلاً في البحث عن شيء ما داخل النص “bbba”، يجب أن تتخيل أن هناك مؤشر مثل مؤشر الكتابة داخل محررات النصوص (هذا العامود | الذي يختفي ويظهر أثناء الكتابة)، وهذا المؤشر موجود قبل أول حرف b في الكلمة. عند هذه النقطة يبدأ المحرك في النظر للنقطة التالية لفحص ما إذا كان هناك مطابقة أما لا.
إذا لاحظنا في مثال الشجرة، نجد أن النقطة التي تقف عندها دائما لايوجد مايتم فحصه وإنما تقف قبل أو بعد الشجرة/العامود، هذه المنطقة تسمى منطقة اللاشيء، وبالتالي محرك التعبيرات المنتظمة (المؤشر الذي تخيلناه) يقف في منطقة اللاشيء دائما لفحص النقطة التالية ، وعندما يبدأ في تطبيق النمط يأخذه خطوة خطوة.
إذن دعونا نتخيل محاولة البحث عن الحرف a داخل النص bedaaca، والتي تسير بالتسلسل الآتي:
- المؤشر الآن يقف قبل حرف b ويسأل نفسه هل أرى الحرف a؟ وكما نرى فإنه لا
- ينتقل إلى النقطة قبل الحرف e ويسأل نفسه هل أرى الحرف a؟ وكما نرى فإنه لا
- ينتقل إلى النقطة قبل الحرف d ويسأل نفسه هل أرى الحرف a؟ وكما نرى فإنه لا
- ينتقل إلى النقطة قبل الحرف a ويسأل نفسه هل أرى الحرف a؟ وكما نرى فإنه نعم؟ إذن سيقوم بتسجيلها كعملية مطابقة
- ينتقل إلى النقطة قبل الحرف a الثاني ويسأل نفسه هل أرى الحرف a؟ وكما نرى فإنه نعم؟ إذن سيقوم بتسجيلها كعملية مطابقة
- ينتقل إلى النقطة قبل الحرف c ويسأل نفسه هل أرى الحرف a؟ وكما نرى فإنه لا
- ينتقل إلى النقطة قبل الحرف a الثالث ويسأل نفسه هل أرى الحرف a؟ وكما نرى فإنه نعم؟ إذن سيقوم بتسجيلها كعملية مطابقة
- ثم ينتقل لإنهاء الجملة بعد الحرف a الأخير ويسأل نفسه هل أرى الحرف a؟ وكما نرى فإنه لا
يمكننا اختبار هذه الخطوات بشكل واضح من خلال أحد المواقع المخصصة لمعالجة للتعابير النمطية، وأحد هذه المواقع regex101 والذي من خلاله يمكنك مشاهدة هذه الخطوات كما هو موضح في الفيديو التالي
[youtube video=”A7t35VGLdW8″ width=”700″ height=”420″]
في المثال السابق تعاملنا مع نمط بسيط وهو مجرد البحث عن الحرف a ، ولكن دائما النمط يتكون من مجموعة رموز تمثل عملية البحث عن شيء أكثر تعقيداً مثل النمط ^\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}$ . هذه الرموز كما لاحظت في النمط السابق ربما تكون حرف لغوي (وهو كل الأحرف الكبيرة والصغيرة والأرقام) أو رمز من الرموز الخاصة Special characters التي تمثل قواعد التعبيرات المنتظمة.
مثال بسيط آخر عن كيفية البحث:
عند محاولة البحث عن الكلمة cat في الجملة He captured a catfish for his cat، فإن محرك البحث سيقوم بالآتي:
- يقف المحرك قبل H، ثم يفحص وجود c ، فلايجد
- ينتقل المحرك قبل e، ثم يفحص وجود c ، فلايجد
- ينتقل المحرك قبل المسافة البيضاء، ثم يفحص وجود c ، فلايجد
- ينتقل المحرك بعد المسافة البيضاء، ثم يفحص وجود c ، فيجد مطابقة
- ينتقل المحرك إلى الرمز a في النمط ليبدأ عملية البحث عنه، وفي نفس الوقت ينتقل مؤشر المحرك داخل النص للنقطة بعد c، ثم يفحص وجود a ، فيجد
- ينتقل المحرك إلى الرمز t في النمط ليبدأ عملية البحث عنه، وفي نفس الوقت ينتقل مؤشر المحرك داخل النص للنقطة بعد a، ثم يفحص وجود t ،فلايجد
- عند هذه النقطة يعرف المحرك أنه مستحيل حدوث تطابق مع النمط إذا بدأ المطابقة من النقطة قبل c في النص. أي يبدأ من [ien]aptured a catfish for his cat[/ien]
- يرجع المحرك لبداية النمط مرة أخرى ليبدأ بحث جديد من بعد النقطة c.
- الآن المحرك يقف قبل a وسيبدأ مرة أخرى في مطابقة النمط من جديد.
- يبدأ البحث مرة أخرى عن c فلايجد حتى يصل قبل c في catfish، فيجد مطابقة
- ينتقل المحرك c في النمط ليبحث عن a، فيجد مطابقة
- ينتقل المحرك a في النمط ليبحث عن t، فيجد مطابقة
- الآن حصل المحرك على مراده، لكن النص لم ينتهي
- يبدأ المحرك من جديد في تطبيق النمط ولكن يقف الآن قبل [ien]fish for his cat[/ien]
- يقوم بتكرار ماسبق حتى يصل للمطابقة الثانية مع cat في آخر الجملة.
لاحظ سلوك المحرك في الفيديو التالي
[youtube video=”8rSD0JEmXEM” width=”700″ height=”420″]
الآن سننتقل إلى شرح أكثر تفصيلاً، ولكن لكي نستطيع تطبيق التعابير النظامية فعلينا إما استخدام أي من محررات النصوص التي توفر البحث باستخدام التعابير النظامية أو أحد المواقع المخصصة في معالجة التعبيرات المنتظمة مثل(regex101 or regexr)، أو أن نعتمد على لغة برمجة، وهذا الأخير يعتبر الخيار الأنسب خاصة وأن الهدف من تعلم التعابير النظامية هو تطبيقها في البرمجة. وبما أن المدونة اهتمامها الأول بلغة PHP سنعتمد على بعض الدوال المدمجة في اللغة، ولكن هذا لايعني أنك لا تستطيع متابعة الدروس إذا كنت مهتم بلغة أخرى، فكل ما نتحدث عنه هو أشياء مشتركة في أغلب لغات البرمجة ولكن بعضها يختلف قلييييل جدا وسنشير إلى الإختلاف كلما أمكن.
تخطى الجزء التالي إن لم تكن مهتماً بلغة PHP، واختر مايناسبك حسب لغتك البرمجية أو اتعتمد على أحد مواقع معالجة التعبيرات المنتظمة.
من الدوال الخاصة بالتعبيرات المنتظمة في PHP الدالة preg_replace عند محاولة استبدال نص باستخدام التعبيرات المنتظمة أو الدالة preg_match أو preg_match_all عند المطابقة فقط بدون استبدال.
ولكن يجب أن تنتبه لكيفية عمل كل الدالة preg_match و preg_match_all، حيث أنه بما أن كل منهما يتعامل مع التعبيرات المنتظمة فهما يخضعان للتحكم في النتائج عن طريق تغيير إعدادات محرك التعبير المنتظم Regex modifiers والإعداد المحوري هنا هو الإعداد global ويرمز له بالحرف g في التعبير المنتظم. ومهمة هذه الإعداد هي التحكم فيما إذا كان المحرك يتوقف بعد أو مطابقة أو يستمر في البحث لجلب جميع المطابقات.
في الحقيقة الأصل أن محرك البحث متحمس/متعطش لجلب المطابقات فبمجرد الوصول لأول مطابقة يقف مباشرة ويسترجع النتيجة، ولكن لايكون هذا مطلوب دائماً فنتحكم في ذلك عن طريق global وبالتالي:
- الدالة preg_match تقوم بتطبيق النمط ولكن بدون استخدام g فيقوم المحرك بالتوقف بعد أول مطابقة
- الدالة preg_match_all تقوم بتطبيق النمط مع استخدام g فيقوم المحرك بالاستمرار في البحث لجلب جميع المطابقات في النص.
ولاكتشاف عمل التعبيرات المنتظمة سنقوم بتحويل المثال السابق إلى كود للبحث عن الكلمة cat داخل الجملة He captured a catfish for his cat
<?php $string = 'He captured a catfish for his cat'; preg_match_all("/cat/",$string ,$matchings); echo '<pre>'; var_dump($matchings); echo '</pre>'; ?>
تكون النتيجة:
array(1) { [0]=> array(2) { [0]=> string(3) "cat" [1]=> string(3) "cat" } }
لاحظ قامت preg_match_all بالبحث في النص كله لجلب كل cat، جرب استبدالها بـ preg_match ولاحظ النتيجة.
[highlight background=”” color=””]يجب أن لايختلط عليك الأمر بين طريقة عمل preg_match_all ومحددات الكميات Regex quantifiers فالأولى تقوم بتكرار النمط بالكامل أكثر من مرة في النص بالكامل لجلب جميع المطابقات أما الثانية تقوم بتكرار جزء من النمط في عملية المطابقة الواحدة[/highlight]
في المثال السابق تمت مطابقت كل ما هو cat بما فيها التي توجد في catfish، أما إذا أردت البحث عن كلمة cat ككلمة منفصلة وليس جزء من كلمة فهذا يحتاج منك للتعرف على قواعد حدود الكلمات Word boundaries وغيرها من القواعد التي سنبدأ في التعرف عليها، لعمل أمور أكثر تعقيدا واحترافية.
كما ذكرنا أن محرك البحث يقووم بفحص الرمز/الحرف التالي في النص لمطابقته مع الرمز في النمط، وإذا حدث تطابق جزئي للنمط ثم حدث فشل في مطابقة باقي النمط يبدأ المحرك من جديد من بعد أخر نقطة حدث عندها تطابق، هذا الأسلوب يسمى بالبحث الخطي، بحيث يقوم المحرك بالفحص المباشر في اتجاه النص. لكن هناك حالات يحتاج فيها المحرك للبحث بشكل مختلف في الحالات التي يتم استخدام محددات الكميات Quantifiers (الإختيارية منها) أو البدائل Alternations. بحيث يقوم المحرك بالرجوع لمكان ما يستطيع عنده تجربة خيار آخر (الخيارات تظهر في حالة استخدام محددات الكميات و البدائل)، وهذه الحالة تسمى التتبع الخلفي Regex Backtracking ونتحدث عنها في موضوع خاص.
التخطي باستخدام الشرطة المائلة للخلف backslash
إذا كانت التعبيرات المنتظمة تستخدم للبحث داخل النصوص عن نصوص أو أرقام أو غيرها من الرموز، فإن رموز التعبيرات المنتظمة نفسها عبارة عن رموز نصية، وأحياناً هذه الرموز تكون جزء من النص الذي تريد البحث عنه،لذلك وجب إضافة شيء للتفرقة بين ما يجب تنفيذه كقاعدة للتعابير النمطية وان يتم معاملته كنص. ويتم التخطي باستخدام الشرطة المائلة للخلف \ .
يمكننا أن نوجز التخطي باستخدام \ بأنه عملية تخطي للمعنى الطبيعي للرمز. بمعنى أنه إذا كان النص الذي تريد أن تبحث عنه هو a*b يكون التعبير المنتظم عبارة عن a\*b حيث نتخطى المعنى الحقيق للرمز * ليتم التعامل معه محرف عادي وليس رمز نمطي Atom.
ولايتم استخدام التعطي مع الأحرف العادية لأن بعض الأحرف إذا سبقها \ يكون لها معنى لدى المحرك. فمثلا النمط /b/ يعني أننا نبحث عن الحرف b داخل النص أما عند وضع \ قبلها فهذا اصبح له معنى آخر وهو محدد الكلمة word boundary.
الملخص
- محرك التعبير المنتظم Regex Engine يبدأ المطابقة من أقصى اليسار
- محرك التعبير المنتظم متحمس دائما لاسترجاع المطابقات ويتوقف عن البحث مع أول مطابقة مع كامل النمط.
- يمكن أن نمنع حماس المحرك من استرجاع أول مطابقة فقط من خلال الإعداد global. (نتحدث عنه بالتفصيل في موضوع إعدادات محرك التعبير المنتظم Regex modifiers)
- التعبير المنتظم يتكون من مجموعة رموز مخصصة Special characters أو تسمى Metacharacters
- بما أن الرموز الخاصة يمكن أن توجد في النص بشكل حرفي وليست حزء من نمط، يحتاج ذلك عمل تخطي Regex escaping characters
- يبدأ المحرك بأول رمز في التعبير المنتظم ثم يبحث في كامل النص عنه، بحيث إذا وجد مطابقة مع أول رمز في النمط يبدأ في استكمال النمط حتى إذا نجح في مطابقة النمط، ثم يبدأ عملية البحث من جديد بداية من آخر نقطة حدث عنده مطابقة (إذا سمحت له بذلك).أما إذا فشل وكان هناك محددات كميات في النمط أو بدائل، سيقوم باستخدام التتبع الخلفي Backtracking.