المجموعة الملتقطة capturing group هي الجزء الذي يضم محارف بين قوسين (...)
في أي تعبيير نمطي RegEx، ولها تأثيران اثنان:
- تسمح بالحصول على جزء من التطابق مثل عنصر مستقل ضمن مصفوفة النتائج.
- يُطبق المحدد الكمي quantifier على مجموعة الالتقاط كلها إذا وضع بعد القوسين مباشرةً.
أمثلة عن مطابقة عدة مجموعات
لنتعرف كيفية عمل الأقواس من خلال الأمثلة.
مثال gogogo
يفيد النمط +go
دون أقواس في إيجاد المحرف g
يليه المحرف o
مكررًا مرةً أو أكثر، مثل goooo
أو goooooo
، وإذا وضعنا محارف النمط السابق بين قوسين +(go)
، فسيعني ذلك go
أو gogo
أو gogogo
وهكذا.
alert( 'Gogogo now!'.match(/(go)+/ig) ); // "Gogogo"
مثال نطاق موقع ويب
نحتاج إلى تعبير نمطي للبحث عن نطاق موقع ويب، مثل النطاقات التالية:
mail.com users.mail.com smith.users.mail.com
يتألف النطاق من كلمات متتالية تفصل بينها نقاط، ويقابل ذلك التعبير النمطي +w+\.)+\w\)
:
let regexp = /(\w+\.)+\w+/g; alert( "site.com my.site.com".match(regexp) ); // site.com,my.site.com
سيعمل النمط السابق، لكنه سيواجه مشكلةً عندما تحتوي الكلمات على شرطة قصيرة -
، مثل my-site.com
، فلا يعتبر هذا المحرف من محارف الصنف w\
، وسنحل هذه المشكلة باستبدال التعبير w\
بالمجموعة [-w\]
، في كل كلمة عدا الأخيرة، وسيصبح النمط بالشكل +w-]+\.)+\w\])
.
مثال البريد الإلكتروني
يمكن توسيع المثال السابق لإنشاء تعبير نمطي لبريد إلكتروني اعتمادًا على النمط السابق، وما دام للبريد الإلكتروني الشكل name@domain
، فيمكن أن تكون أي كلمة هي الاسم، حيث يُسمح ضمنها بالشرطة القصيرة أو النقاط، وبلغة التعبير النمطي ستكون [.-w\]
، وسيكون التعبير النمطي للبريد الإلكتروني /+[-w.-]+@([\w-]+\.)+[\w\]/
let regexp = /[-.\w]+@([\w-]+\.)+[\w-]+/g; alert("my@mail.com @ his@site.com.uk".match(regexp)); // my@mail.com, his@site.com.uk
ليس هذا التعبير مثاليًا، لكنه سيعمل في معظم الأحيان، وسيساعدك في التخلص من الأخطاء الكتابية، وتأكد أن الطريقة الحقيقية الوحيدة للتحقق من بريد هو استلامه للرسالة التي أرسلتها!
المحتوى الموجود بين قوسين عند البحث عن تطابق
تُرقّم الأقواس من اليسار إلى اليمين، وسيتذكر المحرك المحتوى الذي يتطابق مع كل قوس، ويسمح بالحصول على هذه التطابقات ضمن النتيجة، ويبحث التابع (str.match(regexp
عن التطابق الأول، ويعيد النتيجة في مصفوفة عندما لا تُستخدم الراية g
ضمن التعبير regexp
، حيث ستجد ضمن المصفوفة:
-
التطابق بشكله الكامل في الموقع
0
. -
محتوى القوس الأول في الموقع
1
. -
محتوى القوس الثاني في الموقع
2
. - وهكذا…
فلو أردنا مثلًا البحث عن وسوم HTML من النمط <?*.>
، ثم معالجة النتائج، فمن المناسب أن نحصل على محتوى كل وسم ضمن متغير خاص به، وعندما نغلّف المحتوى الداخلي للوسم ضمن قوسين، بالشكل التالي <(?*.)>
، فسنحصل على الوسم كاملًا، وليكن <h1>
، وعلى محتوى هذا الوسم (أي النص h1
) ضمن النتيجة:
let str = '<h1>Hello, world!</h1>'; let tag = str.match(/<(.*?)>/); alert( tag[0] ); // <h1> alert( tag[1] ); // h1
المجموعات المتداخلة
يمكن أن تتداخل الأقواس، وعندها ستُرقَّم أيضًا من اليسار إلى اليمين، فعندما نبحث عن وسم ضمن الوسم <span class="my">
مثلًا، فلربما نريد الحصول على:
-
محتوى الوسم كاملًا
"span class="my
. -
اسم الوسم:
span
. -
سمات الوسم:
"class="my
.
لنضف الأقواس إلى النمط <(([a-z]+)\s*([^>]*))>
، لاحظ كيف تُرقَّم الأقواس من اليسار إلى اليمين:
let str = '<span class="my">'; let regexp = /<(([a-z]+)\s*([^>]*))>/; let result = str.match(regexp); alert(result[0]); // <span class="my"> alert(result[1]); // span class="my" alert(result[2]); // span alert(result[3]); // class="my"
سنجد دائمًا التطابق الكامل في الموقع صفر من المصفوفة، ثم المجموعات مرقمةً من اليسار إلى اليمين بواسطة القوس المفتوح، حيث تُعاد المجموعة الأولى في الموقع الأول [result[1
، تليها الثانية الناتجة عن القوس المفتوح الثاني (+[a-z])
ضمن [result[2
، ثم نتيجة التطابق مع النمط ([^>]*)
ضمن [result[3
، وستكون نتيجة كل مجموعة بصيغة نص.
المجموعات الاختيارية
حتى لو كانت المجموعة اختياريةً وغير موجودة ضمن التطابق، كأن يكون لها المحدد الكمي ?(...)
، فسيبقى مكانها محجوزًا ضمن المصفوفة، وقيمته هي undefined
، فلو تأملنا مثلًا التعبير ?(a(z)?(c
، فسنجد أنه يبحث عن "a"
متبوعًا -اختياريًا- بالحرف "z"
، ومتبوعًا -اختياريًا أيضًا- بالحرف "c"
، لأن المحدد الكمي ?
يعني محرفًا أو لا شيء، فلو طبّقنا التعبير السابق على نص مكون من الحرف a
فقط، فستكون النتيجة:
let match = 'a'.match(/a(z)?(c)?/); alert( match.length ); // 3 alert( match[0] ); // a (whole match) alert( match[1] ); // undefined alert( match[2] ); // undefined
سيكون طول المصفوفة 3 علمًا أن كل المجموعات فارغة!
لكن لو كان النص هو ac
:
let match = 'ac'.match(/a(z)?(c)?/) alert( match.length ); // 3 alert( match[0] ); // ac (whole match) alert( match[1] ); // undefined, because there's nothing for (z)? alert( match[2] ); // c
سيبقى طول المصفوفة 3، لكنك لن تجد تطابقًا يقابل المجموعة (z)?
، وستكون النتيجة ["ac", undefined, "c"]
.
البحث عن كل التطابقات ضمن المجموعات: التابع matchAll
اقتباس**التابع
matchAll
هو تابع جديد، وقد نحتاج إلى موائمة برمجية polyfill لتعويض نقص دعمه في بعض المتصفحات القديمة التي لا تدعمه، لكن يمكنك استخدام بعض الموائمات الخاصة.
لن يعيد التابع match
محتوى المجموعات إذا استخدم للبحث بوجود الراية g
، والتي تعني إيجاد كل التطابقات، وسنحاول في المثال التالي إيجاد كل الوسوم في النص:
let str = '<h1> <h2>'; let tags = str.match(/<(.*?)>/g); alert( tags ); // <h1>,<h2>
لاحظ أن النتيجة هي مصفوفة تحتوي على التطابقات كاملةً لكن دون تفاصيل، أي دون محتوى كل تطابق، لكننا نحتاج عمليًا إلى ذلك المحتوى، وسيساعدنا البحث باستخدام التابع (str.matchAll(regexp
على استخلاص ذلك المحتوى، فقد أضيف هذا التابع إلى JavaScript بعد فترة طويلة من إضافة match
مثل نسخة جديدة ومحسنة منه.
يشابه matchAll
التابع match
، مع وجود ثلاثة اختلافات، وهي:
- لا يعيد مصفوفةً، بل كائنًا قابلًا للتكرار iterable object.
-
يعيد كل تطابق مثل مصفوفة تحتوي مجموعات عند استخدام الراية
g
. -
عندما لا يجد تطابقات فلن يعيد
null
، بل كائنًا فارغًا قابلًا للتكرار.
إليك مثالًا:
let results = '<h1> <h2>'.matchAll(/<(.*?)>/gi); // النتائج ليست مصفوفة بل كائن قابل للتعداد alert(results); // [object RegExp String Iterator] alert(results[0]); // undefined (*) results = Array.from(results); // تحويل النتيجة إلى مصفوفة عادية alert(results[0]); // <h1>,h1 (1st tag) alert(results[1]); // <h2>,h2 (2nd tag)
إنّ الاختلاف الأول مهم جدًا كما يوضّحه السطر "(*)"، فلا يمكن الحصول على التطابق في الموقع [results[0
، لأن الكائن لا يمثل مصفوفةً زائفةً pseudoarray، ويمكننا تحويلها إلى مصفوفة حقيقية باستخدام Array.from
، وستجد العديد من التفاصيل عن المصفوفات الزائفة والكائنات القابلة للتكرار في المقال Iterables.
لا حاجة لتحويل المصفوفة باستخدام Array.from
إذا كنا سنشكل حلقةً من النتائج:
let results = '<h1> <h2>'.matchAll(/<(.*?)>/gi); for(let result of results) { alert(result); // first alert: <h1>,h1 // second: <h2>,h2 }
أو عند استخدام التفكيك destructuring:
let [tag1, tag2] = '<h1> <h2>'.matchAll(/<(.*?)>/gi);
يشابه تنسيق كل تطابق يعيده التابع matchAll
التنسيق الذي يعيده match
دون الراية g
، وهذا التنسيق هو مصفوفة مع خصائص إضافية index
التي تطابق الفهرس في النص، وinput
الذي يعني النص الأصلي:
let results = '<h1> <h2>'.matchAll(/<(.*?)>/gi); let [tag1, tag2] = results; alert( tag1[0] ); // <h1> alert( tag1[1] ); // h1 alert( tag1.index ); // 0 alert( tag1.input ); // <h1> <h2>
انتبه، لماذا ستكون نتيجة التابع كائنًا قابلًا للتكرار وليس مصفوفةً؟
السبب بسيط وهو التحسين، فلن يُنفِّذ الاستدعاء عملية البحث، بل سيعيد كائنًا قابلًا للتكرار لا يحتوي على النتيجة مبدئيًا، ويُنفَّذ البحث في كل مرة نكرره (ضمن حلقة مثلًا)، وبالتالي سيجد العدد المطلوب من النتائج تمامًا، فإذا كان من المحتمل مثلًا وجود 100 تطابق، لكننا وجدنا في حلقة for..of
خمسةً فقط، وقررنا أن هذا كاف وأوقفنا الحلقة، فلن يستهلك المحرك وقتًا إضافيًا في إيجاد التطابقات 95 الباقية.
المجموعات المسماة
يصعب تذكر المجموعات بأرقامها، على الرغم من بساطته في الأنماط البسيطة، لكن عند البحث عن أنماط أكثر تعقيدًا فلن يكون ترقيم الأقواس أمرًا مناسبًا، وسنجد أن خيار تسمية الأقواس هو الأفضل، ونسمي الأقواس بوضع الاسم بالشكل التالي <name>?
مباشرةً بعد القوس، ولنبحث مثلًا عن تاريخ وفق التنسيق "يوم-شهر-سنة":
let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/; let str = "2019-04-30"; let groups = str.match(dateRegexp).groups; alert(groups.year); // 2019 alert(groups.month); // 04 alert(groups.day); // 30
حيث سنجد المجموعات عبر الخاصية groups.
لكل تطابق، ويمكن إيجاد جميع التواريخ باستخدام الراية g
، كما ينبغي استخدام التابع matchAll
للحصول على التطابق كاملًا بالإضافة إلى المجموعات:
let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g; let str = "2019-10-30 2020-01-01"; let results = str.matchAll(dateRegexp); for(let result of results) { let {year, month, day} = result.groups; alert(`${day}.${month}.${year}`); // first alert: 30.10.2019 // second: 01.01.2020 }
مطابقة مجموعات ثم تنفيذ عملية استبدال
يسمح التابع (str.replace(regexp, replacement
الذي يستبدل محتوى الأقواس ضمن النص replacement
، بكل التطابقات regexp
التي يجدها في النص str
، وينفذ ذلك باستخدام الرمز n$
، حيث n
هو رقم المجموعة، وإليك مثالًا:
let str = "John Bull"; let regexp = /(\w+) (\w+)/; alert( str.replace(regexp, '$2, $1') ); // Bull, John
ستتغير العملية باستخدام <name>$
في الأقواس المسماة، ولتغيير تنسيق التاريخ مثلًا من "يوم -شهر-سنة" إلى "سنة.شهر.يوم":
let regexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g; let str = "2019-10-30, 2020-01-01"; alert( str.replace(regexp, '$<day>.$<month>.$<year>') ); // 30.10.2019, 01.01.2020
استثناء التقاط المجموعات باستخدام :?
نحتاج أحيانًا إلى الأقواس لتطبيق المحددات الكمية بطريقة صحيحة، لكننا لا نحتاج إلى محتواها ضمن النتائج، لذلك يمكن استثناء المجموعات باستخدام :?
في البداية، فإذا أردنا مثلًا إيجاد النمط +(go)
، لكننا لا نحتاج إلى محتوى الأقواس (go
) في عنصر مصفوفة مستقل، فيمكننا صياغة النمط بالشكل `+(go?:)، وإليك مثالًا:
let str = "Gogogo John!"; // ?: exludes 'go' from capturing let regexp = /(?:go)+ (\w+)/i; let result = str.match(regexp); alert( result[0] ); // Gogogo John (تطابق كامل) alert( result[1] ); // John alert( result.length ); // 2 (لا مزيد من العناصر ضمن المصفوفة)
الخلاصة
- تُجمِّع الأقواس أجزاءً من التعبير النمطي ليُطبَّق المحدد الكمي عليها مثل زمرة واحدة.
-
تُرقَّم أقواس المجموعات من اليسار إلى اليمين، كما يمُكن أن تُسمّى اختياريًا باستخدام النمط
(...<name>?)
. - يمكن الحصول على المحتوى الموجود داخل الأقواس -الذي يحقق التطابق- بصورة مستقلة ضمن النتيجة، حيث:
-
يعيد التابع
str.match
المجموعة الملتقطة عند استخدام الراية فقط. -
يعيد التابع
str.matchAll
المجموعات الملتقطة دومًا.
-
إذا لم تُسمَّ الأقواس فسنحصل على محتوياتها ضمن المصفوفة وفقًا لتسلسل ترقيمها، كما يمكن الحصول على محتويات الأقواس المسماة من خلال الخاصية
groups.
. -
يمكن استخدام محتويات الأقواس في النص البديل للتابع، إما عبر أرقامها من خلال
n$
، أو أسمائها من خلال<name>$
. -
يمكن استثناء مجموعة من الترقيم باستخدام النمط
:?
قبلها، وذلك عندما نريد تطبيق مُحصٍ quantifier على كامل المجموعة، لكننا لا نريد أن تظهر محتويات المجموعة -الموجودة بين قوسين- في عنصر مستقل ضمن مصفوفة النتيجة، كما لا يمكن الإشارة إلى هذه الأقواس عند استخدام تابع الاستبدال.
مهام لإنجازها
تحقق من عنوان MAC
يتكون عنوان MAC لواجهة اتصال مع الشبكات من 6 أرقام ست عشرية ذات خانتين تفصل بينها نقطتان ":"، مثل العنوان التالي '01:32:54:67:89:AB'
، اكتب تعبيرًا نظاميًا يتحقق من أن النص هو عنوان MAC.
let regexp = /your regexp/; alert( regexp.test('01:32:54:67:89:AB') ); // true alert( regexp.test('0132546789AB') ); // false (no colons) alert( regexp.test('01:32:54:67:89') ); // false (5 numbers, must be 6) alert( regexp.test('01:32:54:67:89:ZZ') ) // false (ZZ at the end)
الحل: يُعطى التعبير النمطي الذي يبحث عن عدد ست عشري من خانتين بالشكل: {2}[0-9a-f]
مفترضين استخدام الراية i
. سنحتاج الآن إلى هذا النمط وخمسة أنماط أخرى مشابهة له، وبالتالي سيكون التعبير النمطي على الشكل:
[0-9a-f]{2}(:[0-9a-f]{2}){5}
ولكي نجبر التعبير على التقاط كامل النص الموافق، لابد من وضع محرفي ارتكاز البداية والنهاية $...^
إليك شيفرة الحل بشكلها الكامل:
let regexp = /^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/i; alert( regexp.test('01:32:54:67:89:AB') ); // true alert( regexp.test('0132546789AB') ); // false (no colons) alert( regexp.test('01:32:54:67:89') ); // false (5 numbers, need 6) alert( regexp.test('01:32:54:67:89:ZZ') ) // false (ZZ in the end)
أوجد الألوان التي تنسّق بالشكل abc# أو abcdef
اكتب تعبيرًا نمطيًا يبحث عن الألوان المكتوبة وفق أحد التنسيقين abc#
أو abcdef#
، أي المحرف #
يليه ثلاث أو ست أرقام ست عشرية.
let regexp = /your regexp/g; let str = "color: #3f3; background-color: #AA00ef; and: #abcd"; alert( str.match(regexp) ); // #3f3 #AA00ef
لاحظ أنه لا ينبغي الحصول على تطابقات تحوي أربع أو خمس أرقام ست عشرية، بل 3 أو 6 فقط.
الحل:
سيكون التعبير النمطي المناسب للبحث عن شيفرة لون ثلاثية الأرقام كالتالي:
/#[a-f0-9]{3}/i
يمكننا أيضًا إضافة ثلاث أرقام ست عشرية أخرى بالضبط فلن نحتاج أكثر أو أقل، لكون الشيفرة اللونية مزيج من ثلاث أو ستة أرقام.
لنستخدم إذًا المكمم {1,2} بعد وضع الصيغة السابقة للتعبير النمطي بين قوسين:
/#([a-f0-9]{3}){1,2}/i
لاحظ الشيفرة:
let regexp = /#([a-f0-9]{3}){1,2}/gi; let str = "color: #3f3; background-color: #AA00ef; and: #abcd"; alert( str.match(regexp) ); // #3f3 #AA00ef #abc
تواجهنا مشكلة صغيرة هنا، فسيجد التعبير النمطي المحارف abc# ضمن abcd#. يمكن وضع النمط b\
في النهاية لحل المشكلة:
let regexp = /#([a-f0-9]{3}){1,2}\b/gi; let str = "color: #3f3; background-color: #AA00ef; and: #abcd"; alert( str.match(regexp) ); // #3f3 #AA00ef
أوجد كل الأرقام
اكتب تعبيرًا نمطيًا يبحث عن كل الأعداد العشرية، بما فيها الصحيحة والعشرية ذات الفاصلة العائمة أو السالبة.
let regexp = /your regexp/g; let str = "-1.5 0 2 -123.4."; alert( str.match(regexp) ); // -1.5, 0, 2, -123.4
الحل:
يُعطى نمط العدد العشري الموجب بوجود القسم العشري منه على الشكل:
\d+(\.\d+)?
لنضع الإشارة السالبة -
لتكون اختيارية في بداية النمط:
let regexp = /-?\d+(\.\d+)?/g; let str = "-1.5 0 2 -123.4."; alert( str.match(regexp) ); // -1.5, 0, 2, -123.4
فسر العبارات الرياضية
تتكون العملية الحسابية من عددين بينهما إشارة عمليات، مثل:
-
1 + 2
-
1.2 * 3.4
-
-3 / -6
-
-2 - 2
قد تكون العملية "+"
أو "-"
أو "*"
أو "/"
، وقد توجد مساحات فارغة قبل أو بعد أو بين الأجزاء.
أنشئ تابعًا (parse(expr
يقبل العبارة معاملًا، ويعيد مصفوفةً من ثلاثة عناصر، وهي:
- العدد الأول.
- العامل الرياضي (إشارة العملية).
- العدد الثاني.
let [a, op, b] = parse("1.2 * 3.4"); alert(a); // 1.2 alert(op); // * alert(b); // 3.4
الحل: يُعطى التعبير النمطي لإيجاد عدد كما رأينا في المهمة السابقة كالتالي:
-?\d+(\.\d+)?
تُعطى العملية الرياضية وفق النمط التالي [/*+-] ولابد من وضع المحرف -
في بداية الأقواس المربعة لأنها ستعنى مجالًا من المحارف إن وضعت في المنتصف ونحن نريد فقط المحرف -
بحد ذاته. لايد أيضًا من تجاوز المحرف /
في التعبير النمطي كالتالي /.../
وهذا ما سنفعله لاحقًا.
نحتاج إلى عدد ثم عملية ثم عدد آخر وقد تكون هناك مساحات فارغة اختيارية بينهم وبالتالي سيكون التعبير النمطي كاملًا:
-?\d+(\.\d+)?\s*[-+*/]\s*-?\d+(\.\d+)?
يتكون التعبير النمطي من ثلاثة أقسام يفصل بينها s*\
:
/-?\d+(\.\d+)?/ // العدد الأول /[-+*/],/ // رمز العملية /-?\d+(\.\d+)?/ // العدد الثاني
يمثل القسم الأول العدد الأول والقسم الثاني العملية الحسابية والثالث العدد الثاني، ولكي يظهر كل قسم كنتيجة مستقلة ضمن مصفوفة النتيجة، سنضع كل قسم ضمن قوسين:
/(-?\d+(\.\d+)?)\s*([-+*/])\s*(-?\d+(\.\d+)?)/
إليك الشيفرة:
let regexp = /(-?\d+(\.\d+)?)\s*([-+*\/])\s*(-?\d+(\.\d+)?)/; alert( "1.2 + 12".match(regexp) );
تتضمن النتائج:
- التطابق كاملًا : "result[0] == "1.2 + 12
-
المجموعة الأولى
(-?\d+(\.\d+)?)
وتمثل العدد الأول مع أجزائه العشرية: "1.2" -
المجموعة الثانية
(\.\d+)?
وتمثل الجزء العشري الأول: ".2" -
المجموعة الثالثة
([-+*\/])
وتمثل العملية الحسابية: "+" -
المجموعة الرابعة
(-?\d+(\.\d+)?)
وتمثل العدد الثاني: "12" -
المجموعة الخامسة
(\.\d+)?
والتي تمثل الجزء العشري من العدد الثاني وهو في الواقع غير موجود undefined
نحتاج في الواقع إلى العددين والعملية الحسابية دون الأجزاء العشرية لذلك سنجعل التعبير أكثر وضوحًا ليلائم ما نريده. سنزيل العنصر الأول من المصفوفة والذي يمثل التطابق الكامل بإجراء انزياح لمصفوفة النتيجة array.shift
.
يمكن التخلص من الأجزاء العشرية (.\d+)
في المجموعة الثانية والرابعة (أي النقط 3 و 4) من المصفوفة بوضع المحرف ?
في بداية كل مجموعة.
إليك الحل النهائي:
function parse(expr) { let regexp = /(-?\d+(?:\.\d+)?)\s*([-+*\/])\s*(-?\d+(?:\.\d+)?)/; let result = expr.match(regexp); if (!result) return []; result.shift(); return result; } alert( parse("-1.23 * 3.45") ); // -1.23, *, 3.45
ترجمة -وبتصرف- للفصل Capturing Groups من سلسلة The Modern JavaScript Tutorial.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.