التعابير النمطية: نمط للبحث داخل النصوص
التعابير النمطية (Regex) في جافا سكريبت هي طريقة لوصف "شكل" النص الذي تبحث عنه: مثل "أربعة أرقام متتالية"، أو "كلمة متبوعة بفاصلة"، أو "أي نص يشبه البريد الإلكتروني". وتوفر لك جافا سكريبت طريقتين لإنشاء تعبير نمطي:
الصيغة الحرفية — أي وضع النمط بين شرطتين مائلتين مع كتابة الـ flags بعد الشرطة الختامية — هي ما ستستخدمه في معظم الأحيان. أمّا new RegExp(...) فاحتفظ بها للحالات التي يكون فيها النمط نفسه ديناميكياً، كأن تبنيه من إدخال المستخدم أو من متغيّر.
حرف الـ i في النهاية هو flag (راية). معناه أن المطابقة لا تفرّق بين الأحرف الكبيرة والصغيرة. سنتحدّث عن الـ flags بعد قليل بمزيد من التفصيل.
test: هل يوجد تطابق؟
أبسط سؤال يمكنك طرحه على تعبير نمطي هو: "هل تحتوي هذه السلسلة على تطابق؟" والإجابة تأتي عبر test:
\d تعني "أي رقم". أما test فترجع true أو false فقط لا غير. فإذا كان كل ما تحتاجه هو إجابة بنعم أو لا — كالتحقق من صحة حقل إدخال أو فلترة مصفوفة — فإن test هي الأداة المناسبة.
match: استخراج النص المطابق
عندما تريد الحصول على النص المطابق نفسه، استخدم الدالة match الخاصة بالسلسلة النصية:
بدون الفلاق g، ترجع دالة match مصفوفة فيها أول تطابق فقط مع بيانات إضافية (مثل index وinput). أما مع g، فترجع كل التطابقات على شكل مصفوفة نصوص عادية. وفي حال ما لقيت أي تطابق، الناتج يكون null وليس مصفوفة فارغة، فخُذ حذرك وتحقّق من ذلك قبل الاستخدام:
الـ Flags تغيّر طريقة عمل الـ regex في جافا سكريبت
الـ flags تأتي بعد الشرطة المائلة الأخيرة، وتتحكّم في أسلوب المطابقة. أكثرها استخدامًا في التعابير النمطية في جافا سكريبت:
g— global، يجد كل التطابقات بدل الأول فقط.i— يتجاهل الفرق بين الحروف الكبيرة والصغيرة.m— multiline، يجعل^و$يطابقان بداية ونهاية كل سطر، لا بداية ونهاية النص كاملًا.s— dotall، يجعل.يطابق حتى أسطر جديدة.u— يدعم يونيكود، ولا غنى عنه مع كثير من الإيموجي والأنماط خارج نطاق ASCII.
بدون العلم m، فإن ^ يرتبط ببداية النص كاملاً فقط. أما مع m، فيصبح مرتبطاً ببداية كل سطر، ولهذا تم التقاط كلٍّ من الورود والبنفسج.
فئات المحارف والمكمّمات في regex
هذه هي اللبنات الأساسية لأغلب الأنماط التي ستكتبها:
\dرقم،\wمحرف كلمة (حرف أو رقم أو شرطة سفلية)،\sمسافة بيضاء.[abc]أحد الأحرف a أو b أو c.[^abc]أي شيء عدا هذه الأحرف.[a-z]نطاق من الأحرف..أي محرف باستثناء سطر جديد.*صفر أو أكثر،+واحد أو أكثر،?صفر أو واحد.{3}ثلاث مرات تماماً،{2,5}بين مرتين وخمس مرات،{2,}مرتان أو أكثر.^البداية،$النهاية.
وعند تجميع هذه العناصر معاً:
إنّ \b هي حدود الكلمة — ذلك الفاصل الخفي بين حرفٍ يُعدّ جزءًا من كلمة وحرفٍ لا يُعدّ كذلك. مفيدة حين تريد مطابقة "كلمة كاملة" فقط.
مجموعات الالتقاط: تذكّر أجزاء من المطابقة
الأقواس تُنشئ مجموعة تلتقط ما طابقته. الدالتان exec وmatch تُعيدان هذه الالتقاطات إلى جانب المطابقة الكاملة:
الفهرس 0 يحتوي على التطابق الكامل، ثم تأتي كل مجموعة في خانة مستقلة بعده. لكن تتبّع المجموعات بالأرقام يصبح مربكًا عندما يزيد عددها عن اثنتين، لذا من الأفضل تسميتها:
المجموعات المُسمّاة (Named groups) أوضح عند استخدامها، وتبقى شغّالة حتى لو أعدت ترتيب النمط.
replace: إعادة كتابة النص المُطابق
تأخذ دالة replace نمطًا ونصًا بديلًا. ويمكن أن يكون البديل إمّا نصًا أو دالة:
بدون الفلاغ g، لن يُستبدَل سوى أول تطابق فقط. ومن الأخطاء الشائعة أن ينسى المطور هذا الفلاغ ثم يتساءل لماذا ما زال البريد الإلكتروني الثاني يظهر بشكل خاطئ.
سلاسل الاستبدال تدعم المراجع الخلفية (back-references). فالرموز $1 و$2 وهكذا تشير إلى مجموعات الالتقاط (capture groups)، بينما $<name> يشير إلى المجموعات المُسمّاة:
لأي حالة أعقد من مجرد استبدال مباشر، مرّر دالة بدل النص الجديد. هذه الدالة تستقبل النص المطابق ومجموعات الالتقاط كوسائط:
الشرطة السفلية تمثّل المطابقة الكاملة (وهي غير مهمّة لنا هنا)، بينما n هو أوّل مجموعة التقاط. هذا النمط — تعبير نمطي مع دالة استبدال — يغطّي معظم حالات معالجة النصوص الواقعية.
matchAll: كل المطابقات مع مجموعات الالتقاط
تُعيد String.prototype.matchAll مُكرِّرًا (iterator) يمرّ على كل مطابقة مع مجموعات الالتقاط الخاصة بها — وهذا شيء لا تقدر عليه match العادية حتى مع الراية g:
matchAll يحتاج إلى الراية g، وإلا سترمي لك جافا سكريبت خطأ TypeError. لو كنت بحاجة للوصول العشوائي بدل التكرار، انشر النتيجة داخل مصفوفة هكذا: [...text.matchAll(email)].
تهريب المحارف الخاصة في regex
بعض المحارف مثل . * + ? ( ) [ ] { } | \ ^ $ لها معنى خاص داخل التعابير النمطية في جافا سكريبت. ولمطابقتها كنصٍّ حرفي، لا بدّ من تهريبها بشرطة مائلة عكسية:
النسخة غير المهرّبة تطابق examplexcom لأن . تعني "أي حرف". هذا النوع من الأخطاء شائع جدًا — ويمر بصمت. إذا لاحظت أن التعبير النمطي يطابق أكثر مما ينبغي، فأول ما عليك التحقق منه هو وجود . غير مهرّبة.
وعند بناء النمط انطلاقًا من مدخلات المستخدم، يجب عليك تهريبها، وإلا تمكّن المستخدم من حقن صيغة regex خاصة به:
$& في نص الاستبدال هو اختصار يعني "النص المطابق بالكامل".
النظر الأمامي والنظر الخلفي (Lookahead و Lookbehind)
أحياناً تحتاج أن تطابق نصاً فقط عندما يأتي بعده (أو قبله) نص معين، دون أن يدخل هذا النص ضمن نتيجة المطابقة. هذا بالضبط ما تقوم به أدوات النظر (lookarounds):
(?= ...)الـ positive lookahead: بمعنى "يأتي بعده كذا".(?<= ...)الـ positive lookbehind: بمعنى "يسبقه كذا".(?! ...)و(?<! ...)هما النسختان السالبتان من الاثنين السابقين.
ميزة الـ lookarounds إنها ما "تستهلك" الأحرف، يعني الجزء اللي تفحصه يبقى متاحاً للجزء التالي من النمط.
كلمة عن التحقق من البريد الإلكتروني
هذا سؤال يتكرر كثيراً: "أعطني regex يتحقق من صحة الإيميل". والإجابة الصادقة: لا تفعل. قواعد البريد الإلكتروني الحقيقية معقدة جداً، وأي تعبير نمطي قصير بما يكفي لتقرأه سيكون خاطئاً في اتجاه ما. أما للتحقق في نماذج الإدخال (form validation)، فيكفيك نمط عملي بسيط:
اقرأها هكذا: "أحرف ليست فراغات ولا @، ثم @، ثم نفس النوع من الأحرف، ثم نقطة، ثم المزيد منها." هذا النمط كفيل بالتقاط الأخطاء الإملائية الواضحة دون ادعاء الالتزام بمعيار RFC 5322. وإن أردت تحققًا حقيقيًا، فأرسل بريد تأكيد.
أخطاء شائعة في regex جافا سكريبت
إليك بعض الفخاخ التي يجدر بك استيعابها جيدًا:
- نسيان الراية
gمعreplaceأوmatchAll. ستحصل على أول تطابق فقط، أو يُرمىTypeError. - حالة
lastIndexفي التعابير النمطية العامة. أي تعبير نمطي يحمل الرايةgأوyيتذكر أين توقف بين استدعاءاتtestوexec. لا تعيد استخدام نفس التعبير على نصوص غير مترابطة — أنشئ واحدًا جديدًا، أو استعملmatchAll. - نقاط وشرطات مائلة غير مهرّبة في الأنماط الديناميكية. هرّب دائمًا مدخلات المستخدم قبل حشرها داخل
new RegExp(...). - التراجع الكارثي (Catastrophic backtracking). المكممات المتداخلة مثل
(a+)+مع مدخلات خبيثة قادرة على تجميد تبويب المتصفح بالكامل. إن شعرت أن التعبير النمطي بطيء، فبسّطه.
التالي: التواريخ والأوقات
التعابير النمطية تتعامل مع شكل النص، أما البيانات الحقيقية فتأتي معها أختام زمنية تحتاج إلى تحليل وتنسيق وعمليات حسابية. الصفحة التالية تغطي Date وIntl.DateTimeFormat، إضافة إلى النموذج الذهني الذي يبعدك عن أخطاء المناطق الزمنية.
الأسئلة الشائعة
كيف ننشئ Regex في JavaScript؟
هناك طريقتان. الأولى الصياغة الحرفية باستخدام الشرطتين المائلتين: /hello/i. والثانية عبر الباني RegExp الذي يأخذ نصاً: new RegExp('hello', 'i'). استخدم الصياغة الحرفية عندما يكون النمط ثابتاً، واستخدم الباني عندما تحتاج إلى بناء النمط من متغير أثناء التشغيل.
ما الفرق بين test و match و exec في Regex؟
الدالة regex.test(str) تُرجع قيمة منطقية (true أو false)، وهي الأسرع إذا كان يهمك فقط وجود تطابق من عدمه. أما str.match(regex) فتُرجع مصفوفة بالنتائج أو null إن لم يوجد تطابق. بينما regex.exec(str) تُرجع تطابقاً واحداً في كل استدعاء مع مجموعات الالتقاط، ومع الراية g تتتبع الموقع بين الاستدعاءات عبر الخاصية lastIndex.
كيف أستبدل جميع التطابقات باستخدام Regex في JavaScript؟
استخدم الراية g هكذا: str.replace(/foo/g, 'bar'). بدون g سيُستبدل أول تطابق فقط. يمكنك أيضاً استدعاء str.replaceAll(/foo/g, 'bar')، لكن انتبه: عند تمرير Regex إلى replaceAll فالراية g إلزامية وإلا سترمي استثناءً.
ما هي مجموعات الالتقاط (Capture Groups) في Regex؟
الأقواس داخل النمط تُنشئ مجموعات التقاط تحفظ ما تمّ مطابقته. مثلاً /(\d{4})-(\d{2})/.exec('2024-11') يُرجع مصفوفة العنصر رقم 1 فيها هو '2024' والعنصر رقم 2 هو '11'. ويمكنك تسميتها هكذا (?<year>\d{4}) والوصول إليها عبر match.groups.year.