اختبار التعابير النمطية
اختبر النمط على نص حقيقي، مع تظليل المطابقات وإيقاف صارم للتراجع المتفجّر.
لماذا يحتاج مختبِر التعابير النمطية إلى عامل (Worker)
معظم آلات هذه الصفحة موجودة من أجل وضع فشل واحد. تأمّل /(a+)+b/ مقابل ثلاثين حرف a بلا b. على المحرك أن يقرر كيف يقسم تلك المحارف الثلاثين بين المكمّم الداخلي والخارجي، والطرق لذلك 2³⁰. وكلها تُجرَّب قبل أن يمكن إعلان استحالة المطابقة.
والمهم أن التعبير النمطي قيد التنفيذ لا يمكن مقاطعته. فجافاسكربت أحادية الخيط، فلا يستطيع حارس setTimeout الانطلاق حتى يعود التعبير — وذلك مع هذا المدخل يقع في مكان ما بعد الموت الحراري للكون. وقد ضاع التبويب.
لذا يعمل النمط داخل Web Worker على خيطه الخاص، وتمسك الصفحة الرئيسية مؤقتاً. فحين يفوز المؤقت، يقتل worker.terminate() الخيط قتلاً مباشراً. وهذه هي الآلية الوحيدة التي تعمل فعلاً، ولهذا يخبرك هذا المختبِر أن نمطك خطر بدل أن يتحول إلى تبويب معلّق.
ويستحق الأمر أن يُستوعَب، لأن النمط نفسه في شيفرة الإنتاج هو حجب للخدمة: مدخلات يقدّمها المستخدم تُطابَق بتعبير هشّ، فيقيّد طلب واحد نواة معالج إلى أجل غير مسمّى. ويُسمّى هذا الصنف من العلل ReDoS.
الصيغ التي ينبغي تجنّبها
| خطرة | لماذا | أأمن |
|---|---|---|
(a+)+ | مكمّمات متداخلة — انقسامات أُسّية | a+ |
(\w|\s)* | تبديل داخل مكمّم، وفروع متداخلة | [\w\s]* |
(\d+)*$ | مجموعة مكمَّمة مع مرساة تفرض استكشافاً كاملاً | \d*$ |
.*.*= | محرفان بدل غير محدودين يتنازعان النص نفسه | [^=]*= |
والعامل المشترك هو الالتباس: أي وجود أكثر من طريقة يستهلك بها النمط المحارف نفسها. وصنف المحارف يؤدي عمل التبديل بلا التباس، ولهذا فإن عمود «الأأمن» ليس في جوهره إلا استبدال | بـ [].
الجشِع والكسول، والعلّة التي يكتبها الجميع مرة
المكمّمات جشعة افتراضياً: فـ .+ يأخذ كل ما يستطيع ولا يعيد المحارف إلا متذمّراً. فمع <b>bold</b>:
| النمط | يطابق |
|---|---|
<.+> | <b>bold</b> — الشيء كله |
<.+?> | <b> — كسول، يقف عند أول > |
<[^>]+> | <b> — وأسرع، لأن ليس ثمة ما يُتراجَع عنه |
والصيغة الثالثة هي التي ينبغي أن تمدّ يدك إليها. فصنف المحارف المنفيّ لا يستطيع تجاوز الهدف، فلا تراجع يُلغى — وهو أوضح في التعبير عن المقصد وأرخص في التنفيذ من النسخة الكسولة.
lastIndex، ولماذا تختفي مطابقة من كل اثنتين
كائن RegExp الحامل للراية g يحمل حالة. فهو يحتفظ بـ lastIndex يتقدّم بعد كل exec أو test، فتُستأنَف إعادة استعمال الكائن نفسه عبر استدعاءات منفصلة من حيث توقّف:
const re = /\d+/g;
re.test('123'); // true, lastIndex is now 3
re.test('123'); // false! it resumed from index 3أنشئ التعبير داخل الحلقة، أو صفّر lastIndex = 0، أو استعمل matchAll التي تدير ذلك عنك. والمختبِر أعلاه ينشئ كائناً جديداً في كل تشغيل، فما تراه هنا هو سلوك الاستدعاء الأول.
أين تختلف تعابير جافاسكربت النمطية
النتائج هنا تطابق Node وشيفرة المتصفح تماماً، لأنه المحرك نفسه. أما مقابل اللغات الأخرى فالفروق التي توقع الناس هي:
- لا مجموعات ذرّية ولا مكمّمات تملّكية. ففي PCRE
(?>...)وa++لمنع التراجع منعاً باتاً؛ ولا شيء منهما في جافاسكربت، ولهذا كتابة ReDoS هنا أسهل. - لا استدعاء ذاتي. لا تستطيع مطابقة أقواس متوازنة بتعبير نمطي في جافاسكربت. فإن كان ذلك هو المطلوب فأنت تحتاج محلّلاً.
- النظر إلى الخلف وصل متأخراً. فـ
(?<=...)مدعوم في المتصفحات الحالية دون نسخ سفاري القديمة، فتحقّق من منصاتك المستهدفة. - RE2 في Go آلة أخرى. زمن خطّي مضمون، ولا مراجع خلفية، ولا نظر حول. فإن تساءلت يوماً لماذا أسقطت Go خصائص، فهذه الصفحة هي السبب.
ذات صلة
إن كان النص الذي تطابقه ملف JSON، فإن تنسيقه أولاً يجعل النمط بلا حاجة عادةً — فالمحلّل يغلب التعبير النمطي على البيانات المهيكلة في كل مرة. وإن كان مرادك الجدولة لا المطابقة، فإن محلّل cron هو الأداة المجاورة.
أسئلة حول التعابير النمطية
لماذا أُوقف نمطي بعد ثانيتين؟
لأنه كان يتراجع تراجعاً كارثياً. فبعض الصيغ — كالمكمّمات المتداخلة مثل (a+)+ أو (\w|\s)* — تدفع المحرك إلى تجريب عدد أُسّي من طرق المطابقة، حتى إن سلسلة من أربعين محرفاً قد تستغرق زمناً أطول من عمر الكون. ولا سبيل في جافاسكربت لمقاطعة تعبير نمطي قيد التنفيذ، ولهذا يعمل النمط هنا داخل Web Worker: إذ يمكن إنهاء العامل فتبقى الصفحة مستجيبة. أما مهلة على الخيط الرئيسي فلن تنطلق إلا بعد انتهاء التعبير، وهذا في مثل تلك الأنماط لا يحدث أبداً.
هل يستخدم هذا محرك التعابير النمطية نفسه الموجود في شيفرتي؟
يستخدم محرك متصفحك، أي محرك جافاسكربت — فتطابق النتائج ما تحصل عليه في Node وفي شيفرة المتصفح تماماً. لكنها لن تطابق دائماً PCRE أو بايثون أو Go أو جافا. فجافاسكربت لا تملك مجموعات ذرّية ولا مكمّمات تملّكية ولا استدعاءً ذاتياً؛ والنظر إلى الخلف مدعوم في المتصفحات الحديثة لكنه وصل متأخراً. أما RE2 في Go فتصميم مختلف كلياً ويخلو عمداً من المراجع الخلفية، وهذا بالضبط ما يجنّبه مشكلة التراجع أعلاه.
لماذا لا يجد تعبيري الشامل إلا مطابقة من كل اثنتين؟
لأن كائن RegExp الحامل للراية g يحتفظ بـ lastIndex يتقدّم مع كل استدعاء، فإعادة استعمال الكائن نفسه عبر استدعاءات exec أو test منفصلة تستأنف من حيث توقّف. وهذا من أكثر أجزاء الواجهة إرباكاً على نحو ثابت. فإما أن تنشئ التعبير من جديد في كل مرة، أو تصفّر lastIndex قبل الاستعمال، أو تستخدم matchAll التي تتولى الأمر عنك.
ما الفرق بين المكمّم الجَشِع والمكمّم الكسول؟
المكمّم الجَشِع يأخذ أكبر قدر ممكن ولا يعيد المحارف إلا مُكرَهاً؛ أما الكسول (ويُكتب بإضافة ? في آخره) فيأخذ أقل قدر ممكن ولا يتوسّع إلا مضطراً. فمع <b>bold</b> يطابق النمط <.+> السلسلة كلها، لأن .+ يبتلع كل شيء ثم يتراجع بالقدر الكافي فقط ليجد > ختامياً. أما كتابة <.+?> فتطابق <b> وحدها. وهذا هو السبب الأشيع على الإطلاق في أن يلتقط النمط أكثر بكثير مما قصدت.
هل من الآمن لصق بيانات حقيقية هنا؟
نعم. يُسلَّم النمط والنص إلى عامل داخل هذه الصفحة ويُقيَّمان هناك. ولا يُرسَل شيء إلى أي خادم — وهذا نفسه ما يجعل إلغاء نمط منفلت ممكناً، إذ لا طلب هناك بانتظار جواب.
آخر مراجعة . وجدت شيئاً لم يعد محدَّثاً؟ أخبِرنا.
