مرجع التعبيرات النمطية (Regex): الأحرف الخاصة والمجموعات وLookaround
التعبير النمطي (regex) لغة أنماط صغيرة لمطابقة النصوص. \d+ تعني «رقم واحد أو أكثر»، و^Error تعني «سطر يبدأ بكلمة Error». هذه وظيفته. يجمع هذا المرجع بناء الجملة كله في صفحة واحدة: الأحرف الخاصة، المُكمّمات، المراسي، المجموعات، Lookaround، والأعلام (flags)، إضافة إلى أكثر من 15 نمطًا تنسخه وتلصقه في JavaScript أو Python مباشرة.
الدليل موجَّه لمن يعرف أصلًا ما السلسلة النصية ويبحث عن مرجع لا جولة تعليمية. اقفز إلى جدول المرجع السريع إن أردت الرموز وحدها، واقرأ قسمَي Lookaround والمزالق إذا سبق أن جمّد تعبير نمطي خادمك.
1. ما هو Regex ولماذا ما زلت تحتاجه في 2026
التعبير النمطي نمط يُترجَم إلى آلة حالات تمسح السلسلة فإما تطابقها أو تفشل. القواعد قليلة، أما حالات الاستخدام فكثيرة.
يستطيع الذكاء الاصطناعي أن يصوغ نمطًا نيابةً عنك، لكن بعض المهام تبقى من نصيب من يكتب regex بيده:
- تحليل السجلات: لديك عشرة ملايين سطر من سجلات وصول nginx، وتريد كل طلب 5xx صادر من وكيل مستخدم بعينه. تعبير نمطي من 40 حرفًا عبر
grep -Eينتهي خلال ثوانٍ، أما استدعاء نموذج لغوي لكل سطر فلن ينتهي. - التحقق من النماذج والحقول: أرقام الهواتف، الرموز البريدية، الطوابع الزمنية ISO، مفاتيح التراخيص. النمط يعيش بجوار الحقل ويُنفَّذ مع كل ضغطة مفتاح في المتصفح.
- البحث والاستبدال بالجملة: إعادة هيكلة ألف ملف تحتاج فيها إلى التقاط اسم وإعادة حقنه. sed وripgrep وميزة «استبدال في الملفات» في محرّرك تتحدث regex أصلًا.
وللنصف الآخر من العدّة الخاص بـ JSON، راجع مرجع jq السريع: 30 نمطاً عملياً لمعالجة JSON من سطر الأوامر.
1.1 كيف تقرأ نمط regex (دروس regex في خمس ثوانٍ)
تقرأ معظم الأنماط أسهل من اليسار إلى اليمين، رمزًا واحدًا في كل مرّة. خذ ^[A-Z]\w+\d{2,4}$ مثالًا:
^يربط المطابقة ببداية السلسلة.[A-Z]يطابق حرفًا كبيرًا واحدًا بالضبط.\w+يطابق محرف كلمة واحدًا أو أكثر.\d{2,4}يطابق ما بين رقمين وأربعة أرقام.$يربط بنهاية السلسلة.
المهارة في قراءة المراسي أولًا، ثم فئات المحارف، ثم المُكمّمات.
2. Quick Reference Table
هذا القسم هو ما يأتي معظم القرّاء من أجله. انسخ ما يلزمك.
Metacharacters
| Pattern | Matches |
|---|---|
. | Any character except newline (or any character with the s/dotall flag) |
\d | A digit ([0-9], or all Unicode digits with the u flag) |
\D | A non-digit |
\w | A word character ([A-Za-z0-9_]) |
\W | A non-word character |
\s | Any whitespace (space, tab, newline, …) |
\S | Any non-whitespace |
Quantifiers
| Pattern | Matches |
|---|---|
* | 0 or more (greedy) |
+ | 1 or more (greedy) |
? | 0 or 1 (greedy) |
{n} | Exactly n times |
{n,m} | Between n and m times |
{n,} | n or more times |
*?, +?, ??, {n,m}? | Lazy versions of each quantifier |
Anchors
| Pattern | Matches |
|---|---|
^ | Start of string (or start of line with m flag) |
$ | End of string (or end of line with m flag) |
\b | Word boundary |
\B | Non-word boundary |
\A | Absolute start of string (Python) |
\Z | Absolute end of string (Python) |
Character classes
| Pattern | Matches |
|---|---|
[abc] | Any of a, b, c |
[^abc] | Anything except a, b, c |
[a-z] | Any lowercase letter |
[0-9] | Any digit |
\p{L} | Any Unicode letter (u flag in JS, default in Python re) |
Groups
| Pattern | Matches |
|---|---|
(...) | Capture group |
(?:...) | Non-capture group |
(?<name>...) | Named capture (JS ES2018+); Python uses (?P<name>...) |
\1, \2 | Backreference to group 1, 2 |
Lookaround
| Pattern | Matches |
|---|---|
(?=...) | Positive lookahead |
(?!...) | Negative lookahead |
(?<=...) | Positive lookbehind |
(?<!...) | Negative lookbehind |
Flags
| Flag | Effect |
|---|---|
i | Case-insensitive |
m | Multiline: ^ and $ match per-line |
s | Dotall: . matches newlines |
g | Global (JS) — find all matches |
u | Unicode mode |
y | Sticky (JS) — anchor to lastIndex |
3. الأحرف الخاصة وفئات المحارف
3.1 الأحرف الحرفية مقابل الخاصة
معظم المحارف حرفية كما هي. الأحرف الاثنا عشر الخاصة التي يجب تهريبها (escape) إذا أردتها بمعناها الأصلي هي:
. ^ $ * + ? ( ) [ ] { } | \
نسيان تهريب . خطأ شائع في regex. \. يطابق نقطة حرفية. داخل فئة محارف، يطابق [.] نقطة حرفية أيضًا، إذ تفقد معظم الأحرف الخاصة قوتها داخل [...] باستثناء ] و\ و^ (إذا جاءت أولًا) و- (في الوسط).
3.2 اختصارات فئات المحارف
الفئات المختصرة تبدو بسيطة حتى يدخل Unicode على الخط:
// JavaScript — without the u flag, \d is ASCII only
/\d/.test('5'); // true
/\d/.test('٥'); // false (Arabic-Indic digit)
/\d/u.test('٥'); // false — even with u, \d stays ASCII in JS
/\p{N}/u.test('٥'); // true — \p{N} is the Unicode-aware digit class
# Python — re module treats \d as Unicode by default
import re
re.match(r'\d', '٥') # <Match span=(0, 1)>
re.match(r'(?a)\d', '٥') # None — (?a) forces ASCII
إذا كنت تتعامل فقط مع مدخلات ASCII الإنجليزية، فإن \d و[0-9] متبادلتان. أما حين يلصق المستخدم اسمًا فيه علامة تشكيل أو حرف مُحوّر، فستفضّل \p{L} على \w.
3.3 فئات المحارف المخصّصة
// JavaScript
/[A-Za-z][A-Za-z0-9_-]{2,29}/.test('valid_handle-1'); // true
// Negation and ranges combined
/[^aeiou\s]/g // any non-vowel, non-whitespace character
في فئات Unicode، تعني \p{L} «أيّ حرف»، و\p{N} «أيّ رقم»، و\p{Script=Han} «أيّ حرف صيني (هان)». JavaScript يتطلب علم u، بينما Python تدعم \p{...} فقط عبر حزمة regex على PyPI، لا في وحدة re القياسية.
إن كنت تعمل في سطر الأوامر، فقد ترى أيضًا فئات محارف POSIX:
| POSIX class | Matches | ASCII equivalent |
|---|---|---|
[[:alpha:]] | letters | [A-Za-z] |
[[:digit:]] | digits | [0-9] (\d) |
[[:alnum:]] | letters + digits | [A-Za-z0-9] |
[[:space:]] | whitespace | \s |
[[:upper:]] | uppercase | [A-Z] |
[[:lower:]] | lowercase | [a-z] |
فئات POSIX تعمل في grep -E وsed -E. لا تعمل في JavaScript ولا في وحدة re في Python؛ استخدم \d و\s و\w بدلًا منها.
4. المُكمّمات: الجشع مقابل الكسول
4.1 المُكمّمات الأساسية
/a*/.exec('aaab') // ['aaa'] — 0 or more
/a+/.exec('aaab') // ['aaa'] — 1 or more
/a?/.exec('aaab') // ['a'] — 0 or 1
/a{2,3}/.exec('aaaab') // ['aaa'] — 2 to 3
4.2 الجشع (Greedy) مقابل الكسول (Lazy)
افتراضيًا، المُكمّمات جشعة: تلتهم أكبر قدر ممكن ثم تتراجع لتجعل النمط كلّه يتطابق. أضف ? بعدها لتحوّلها كسولة.
const html = '<p>one</p><p>two</p>';
html.match(/<p>.*<\/p>/)[0]; // '<p>one</p><p>two</p>' (greedy eats both)
html.match(/<p>.*?<\/p>/)[0]; // '<p>one</p>' (lazy stops at first)
الكسولة هي ما ستريده غالبًا عند استخراج الوسوم أو السلاسل المُقتبسة. الأفضل من ذلك: تجنّب . تمامًا واستخدم فئة منفية، فـ <p>[^<]*</p> أسرع من <p>.*?</p> لأن المحرّك لا يجد ما يتراجع إليه أصلًا.
4.3 التراجع الكارثي (Catastrophic backtracking)
هكذا يُجمِّد regex الخادم. ضع مُكمِّمًا داخل آخر مع تداخل ملتبس، فيجرّب المحرّك عددًا أُسّيًا من المسارات قبل أن يستسلم.
// Don't do this
/(a+)+b/.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'); // takes seconds
أمام 41 حرف a متبوعًا بـ !، يجرّب المحرّك ما يقارب 2^41 نقطة تقسيم قبل أن يقرّر أن b مفقود. أمامك ثلاثة حلول:
- سطّح النمط:
/a+b/يؤدي نفس المهمة دون تعشيش. - استخدم مجموعة ذرّية (atomic group) (في Python regex وPCRE وJava وRuby):
(?>a+)+b. بمجرد أن يطابقa+، يرفض المحرّك التراجع داخله. - بدّل المحرّك:
regexpفي Go وRE2 وصندوقregexفي Rust تستخدم NFA بزمن خطّي، ولا تستطيع الوقوع في التراجع الكارثي بحكم تصميمها.
JavaScript ووحدة re في Python يعتمدان التراجع، ولا يدعمان المجموعات الذرّية في المكتبة القياسية (تضيفها حزمة regex على PyPI). إذا كنت تتحكم في طول المدخل فالأمر آمن، أما حين يأتي المدخل من المستخدم فتحقّق من الطول أولًا أو ترجم النمط مسبقًا على RE2.
5. المراسي وحدود الكلمات
5.1 ^ و$
افتراضيًا، ^ بداية المدخل كامله و$ نهايته. مع علم m (متعدد الأسطر) يصبحان بداية كل سطر ونهايته:
const log = 'INFO start\nERROR boom\nINFO done';
log.match(/^ERROR.*/); // null — single-line mode, ^ only matches index 0
log.match(/^ERROR.*/m); // ['ERROR boom']
5.2 \b و\B
\b تأكيد بعرض صفر: يطابق الموضع بين حرف كلمة (\w) وحرف ليس من الكلمة. مفيد للبحث عن كلمة بأكملها:
/\bcat\b/.test('the cat sat'); // true
/\bcat\b/.test('concatenate'); // false
حدود الكلمات معرَّفة على \w، وهي ASCII افتراضيًا. النصوص الصينية واليابانية والكورية ليس فيها فراغات بين الكلمات، ولذلك لن يكتشف \b حدود الكلمات هناك. ستحتاج إلى مُجزِّئ مثل jieba أو MeCab قبل regex، لا بديلًا عنه.
5.3 الوضع متعدّد الأسطر
import re
text = "INFO ok\nERROR fail\nINFO done\n"
re.findall(r'^ERROR.*$', text) # []
re.findall(r'^ERROR.*$', text, re.MULTILINE) # ['ERROR fail']
في JavaScript الكتابة نفسها: text.match(/^ERROR.*$/gm). اجمع m مع g لتلتقط كل سطر مطابق.
6. المجموعات والالتقاط والمراجع الخلفية
6.1 مجموعات الالتقاط
تؤدي الأقواس وظيفتين: تجميع الأنماط الفرعية للمُكمّمات، والتقاط ما طابقها لاستخدامه لاحقًا.
'2026-05-13'.match(/(\d{4})-(\d{2})-(\d{2})/);
// ['2026-05-13', '2026', '05', '13', index: 0, ...]
تُرقَّم المجموعات من اليسار إلى اليمين بحسب قوسها المفتوح، ابتداءً من 1.
6.2 المجموعات غير الالتقاطية
إذا احتجت إلى التجميع دون الالتقاط، استخدم (?:...). هي أسرع وتبقي ترقيم المجموعات نظيفًا:
/(?:https?):\/\/(\S+)/.exec('see https://go-tools.org');
// ['https://go-tools.org', 'go-tools.org']
// — the protocol is grouped but not captured; group 1 is the host
6.3 المجموعات المُسمّاة
تسمية المجموعات تجعل الأنماط أوضح للقراءة وأأمن عند إعادة الهيكلة.
// JavaScript (ES2018+)
const m = '2026-05-13'.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
m.groups.year; // '2026'
# Python — note the (?P<...>) syntax
import re
m = re.match(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', '2026-05-13')
m.group('year') # '2026'
6.4 المراجع الخلفية (Backreferences)
تتيح المراجع الخلفية لجزء لاحق من النمط أن يكرّر ما التقطته مجموعة سابقة.
// Find any character that repeats consecutively
'bookkeeper'.match(/(\w)\1/g); // ['oo', 'kk', 'ee']
// Match paired HTML tags by name
const tag = /<(\w+)>(.*?)<\/\1>/;
'<b>bold</b>'.match(tag);
// ['<b>bold</b>', 'b', 'bold']
في Python، تعمل \1 في النمط وفي بديل الاستبدال. أما المراجع المُسمّاة فتُكتب (?P=name) داخل النمط و\g<name> في بدائل re.sub.
7. Lookarounds: Lookahead وLookbehind
Lookarounds تأكيدات بعرض صفر. تتحقق من شرط دون استهلاك محارف، فيمكن تسلسلها.
7.1 Lookahead
// Password: at least 8 chars, one digit, one uppercase, one lowercase
const strong = /^(?=.*\d)(?=.*[A-Z])(?=.*[a-z]).{8,}$/;
strong.test('Hunter2!'); // true
strong.test('hunter2!'); // false — no uppercase
// Negative lookahead — file names that are not .tmp
/^[\w-]+(?!\.tmp$)\.[a-z]+$/.test('report.csv'); // true
7.2 Lookbehind
Lookbehind هي الصورة المعاكسة: تؤكّد ما يأتي قبل الموضع الحالي.
// Extract a price after a currency symbol — keep the number, drop the $
'price: $42.50'.match(/(?<=\$)\d+(\.\d+)?/); // ['42.50', '.50']
// Negative lookbehind — match Bond but not James Bond
'Mr. Bond'.match(/(?<!James )Bond/); // ['Bond']
'James Bond'.match(/(?<!James )Bond/); // null
7.3 Lookbehind في JavaScript مقابل Python
هذا أحد المواضع القليلة التي يتباعد فيها المحرّكان بما يكفي لكسر نمط حين تنقله بين اللغتين.
| Engine | Lookbehind length |
|---|---|
| JavaScript (V8, SpiderMonkey, JSC 16.4+) | Variable-width since ES2018. (?<=\d+) is valid. |
Python stdlib re | Fixed-width only. (?<=\d+) raises error: look-behind requires fixed-width pattern. |
Python regex PyPI package | Variable-width supported. import regex; regex.search(r'(?<=\d+)abc', '12abc'). |
حلول التفافية في Python: أعد كتابة lookbehind بطول معلوم ((?<=\d{3}))، أو التقط البادئة واقطعها بعد المطابقة.
8. الأعلام والمُعدِّلات
8.1 i — تجاهل حالة الأحرف
/error/i.test('FATAL ERROR'); // true
re.search(r'error', 'FATAL ERROR', re.IGNORECASE) # <Match span=(6, 11)>
8.2 m وs
العلم m يحوّل ^ و$ إلى مرسيَين لكل سطر. والعلم s (dotall) يجعل . يطابق فواصل الأسطر. الاثنان مستقلّان، فاجمعهما حين تريد كليهما.
/<script>(.*?)<\/script>/s.exec('<script>\nalert(1)\n</script>')[1];
// '\nalert(1)\n' — without s, the . would refuse the newlines
8.3 g — عام
في JavaScript، يغيّر g الواجهة البرمجية أكثر مما يغيّر المطابقة نفسها. بدون g تُعيد String.match مجموعات الالتقاط، ومع g تُعيد كل سلاسل المطابقة. استخدم matchAll لتحتفظ بمجموعات الالتقاط عبر كل المطابقات.
const text = 'a=1 b=2 c=3';
text.match(/(\w)=(\d)/); // first match with groups
text.match(/(\w)=(\d)/g); // ['a=1', 'b=2', 'c=3'] — no groups
[...text.matchAll(/(\w)=(\d)/g)]; // every match, with groups
لا تستخدم Python العلم g. البدائل الشاملة هي re.findall وre.finditer وre.sub.
8.4 u — Unicode و\p{...}
// Match any Han character (Chinese, Japanese kanji)
/\p{Script=Han}+/gu.test('Hello 世界'); // true
// Match emoji (extended pictographic)
/\p{Extended_Pictographic}/u.test('👋'); // true
في Python يكون Unicode مُفعّلًا افتراضيًا، فـ re.findall(r'[一-鿿]+', text) يكافئ نطاق Han. وإذا أردت هروب خصائص Unicode كاملًا، فاستخدم حزمة regex من PyPI: regex.findall(r'\p{Script=Han}+', text).
9. أنماط شائعة ستستخدمها يوميًا
9.1 التحقق من البريد الإلكتروني
كن صريحًا مع نفسك حول النسخة التي تحتاجها فعلًا.
// The 95% pattern — what most form validators use
const email = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
email.test('a@b.co'); // true
// The "I really want to be RFC 5322-ish" pattern
const rfc = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
الحقيقة: تحقّق RFC 5322 الكامل بـ regex صرف يحتاج إلى نحو 6000 محرف، ويظل يخطئ في حالات حافّة. استخدم نمط 95%، ثم أرسل بريد تحقّق فعلي؛ هذا الاختبار الوحيد الذي يحسم الأمر.
9.2 استخراج عناوين URL
const urlPattern = /https?:\/\/[^\s<>"]+/g;
const found = 'See https://example.com/a?b=1 and http://x.io'.match(urlPattern);
// ['https://example.com/a?b=1', 'http://x.io']
بعد استخراج URL ستحتاج عادةً إلى فحص سلسلة الاستعلام؛ الصقها في ترميز وفك ترميز URL أونلاين لتقرأ المعلمات المُرمَّزة بالنسبة المئوية في لمحة. ولمتى تُرمِّز ومتى تفك الترميز، راجع ترميز وفك ترميز URL: دليل ترميز النسبة المئوية للمطورين.
9.3 أرقام الهواتف
// E.164 — international, optional + and 1-3 digit country code
const e164 = /^\+?[1-9]\d{1,14}$/;
e164.test('+14155551234'); // true
// North American Number Plan with separators
const nanp = /^(\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/;
nanp.test('(415) 555-1234'); // true
إذا أردت ما يتجاوز سؤال «هل هذا الشكل معقول؟»، فاستخدم libphonenumber. لا يستطيع regex أن يتحقق من وجود رمز المنطقة فعلًا.
9.4 IPv4 وIPv6
// IPv4 — strict 0-255 per octet
const ipv4 = /^((25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(25[0-5]|2[0-4]\d|1?\d?\d)$/;
ipv4.test('192.168.1.1'); // true
ipv4.test('999.0.0.1'); // false
// IPv6 — the simplified form. The full RFC 4291 pattern is ~600 chars.
const ipv6simple = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
ipv6simple.test('2001:0db8:85a3:0000:0000:8a2e:0370:7334'); // true
أما لتحقّق IPv6 الحقيقي بصيغة :: المختصرة، وIPv4 المضمَّن، ومُعرِّفات النطاق، فاستخدم isIP() من node:net أو ipaddress.ip_address() في Python. محاولة فعل ذلك بـ regex صرف ستترك لك عبء صيانة لا ينتهي.
9.5 تواريخ وطوابع ISO 8601
// Date only — YYYY-MM-DD
const isoDate = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
isoDate.test('2026-05-13'); // true
// Date + time + optional fractional seconds + Z or offset
const iso = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
iso.test('2026-05-13T09:30:00.123Z'); // true
تبدو ISO 8601 بسيطة، لكنها مليئة بالمزالق: الثواني الكبيسة، وتواريخ الأسبوع (2026-W19)، والتواريخ الترتيبية (2026-133). وللتفريق بين ثوانٍ الحقبة والميلّي ثانية وتحوّلات المنطقة الزمنية، راجع دليل طابع Unix الزمني: الدقة، المنطقة الزمنية ونصائح التوقيت الصيفي.
10. سير عمل البحث/الاستبدال باستخدام Regex
10.1 JavaScript — String.replace مع $1
// Reformat US dates: MM/DD/YYYY -> YYYY-MM-DD
'05/13/2026'.replace(/(\d{2})\/(\d{2})\/(\d{4})/, '$3-$1-$2');
// '2026-05-13'
// Use a callback when the replacement is conditional
'price 42 dollars'.replace(/(\d+) dollars/, (_, n) => `$${n}`);
// 'price $42'
تُشير $1 و$2 وأخواتها إلى المجموعات المرقّمة، و$<name> إلى المجموعات المُسمّاة. $& هي المطابقة الكاملة، و$$ تكتب علامة $ حرفية.
10.2 Python — re.sub مع \1 ودوال الردّ
import re
# Same date reformat as above
re.sub(r'(\d{2})/(\d{2})/(\d{4})', r'\3-\1-\2', '05/13/2026')
# '2026-05-13'
# Callback — uppercase every email address in a string
def upper_email(m):
return m.group(0).upper()
re.sub(r'[\w.-]+@[\w.-]+', upper_email, 'mail me at hi@go-tools.org')
# 'mail me at HI@GO-TOOLS.ORG'
في الاستبدال، تستخدم Python إما \1 أو \g<name>. وبادئة السلسلة الخام r'...' مهمّة، فبدونها يصبح \1 محرفًا حرفيًا.
10.3 سطر الأوامر: sed وgrep وripgrep وjq
عند إعادة الهيكلة الجماعية من سطر الأوامر ينتقل regex من السكربت إلى الصدفة:
# ripgrep — find every TODO with a name attached
rg -n '\bTODO\(([^)]+)\)' --replace 'TODO(\1)'
# grep -E with anchors — failed login lines from auth.log
grep -E '^[A-Z][a-z]{2} +[0-9]+ .*Failed password' /var/log/auth.log
# sed — strip trailing whitespace, in-place, across a tree
find . -name '*.md' -print0 | xargs -0 sed -i -E 's/[[:space:]]+$//'
ripgrep يستخدم صندوق regex في Rust (على غرار RE2، بزمن خطّي، بلا lookbehind). أما grep -E وsed -E فيستخدمان POSIX الموسّع الذي يفتقر إلى \d، فاستعِض عنه بـ [0-9] أو [[:digit:]]. وحين تكون البيانات JSON، استبدل regex بـ jq؛ بطاقتها المرجعية في مرجع jq السريع: 30 نمطاً عملياً لمعالجة JSON من سطر الأوامر.
11. مزالق شائعة
11.1 نسيان تهريب .
علّة حقيقية شحناها في الإنتاج ذات مرة: مُخفي سجلات كان يفترض به أن يُقنّع عناوين IP.
// Wrong — matches '192a168b1c1' too
/(\d+).(\d+).(\d+).(\d+)/.test('192a168b1c1'); // true
// Right
/(\d+)\.(\d+)\.(\d+)\.(\d+)/.test('192a168b1c1'); // false
داخل فئة محارف، النقطة . حرفية أصلًا، ولذلك يعمل كلٌّ من [.] و\.. أما خارج الفئة فهرّبها.
11.2 .* الجشع يلتهم الكثير
'<a href="x"><b>bold</b></a>'.match(/<(.*)>/)[1];
// 'a href="x"><b>bold</b></a' — the whole thing!
.* الجشع يمسح حتى نهاية السلسلة ثم يتراجع حتى يطابق >، أي آخر > في المدخل. حلّ الأمر بالنسخة الكسولة (.*?)، أو بفئة منفية ([^>]*) وهي أسرع وأوضح.
11.3 مراسي الوضع متعدّد الأسطر
التباس شائع: ^ و$ لا يطابقان محارف فواصل الأسطر افتراضيًا، بل يطابقان مواضع بداية ونهاية المدخل بأكمله. علم m هو ما يحوّلهما إلى مراسٍ لكل سطر، وعلم s هو ما يجعل . يعبر فواصل الأسطر. الاثنان متعامدان، وستحتاج إليهما معًا غالبًا حين تحلّل السجلات.
11.4 ReDoS وكيف تنزع فتيله
ReDoS (رفض الخدمة عبر regex) هو النسخة الإنتاجية من التراجع الكارثي. الحلول:
- التحليل الساكن: أدوات مثل
safe-regexوrecheckوقاعدةno-misleading-character-classفي ESLint تلتقط الأنماط الخطرة قبل أن تُشحن. - المجموعات الذرّية (Python regex، PCRE، Ruby، Java):
(?>...)يمنع المحرّك من العودة إلى المجموعة عند التراجع. - المُكمّمات الاستحواذية (
*+،++،?+في PCRE/Java): الفكرة نفسها بصياغة أقصر. - التحوّل إلى محرّك لا يتراجع:
regexpفي Go، وRE2، وصندوقregexفي Rust، وربطre2لـ Python كلها تعمل بزمن خطّي. ripgrep أشهر نشر لـ RE2 في البرّية. - تحقّق من طول المدخل أوّلًا: قنبلة regex بحجم 10 كيلوبايت تجلب الكارثة، أما وضع سقف 10 بايتات للمدخل فسطر واحد من الكود.
ولاستعراض أوسع لأدوات الاستخدام اليومي المرافقة لـ regex (مُنسِّقات، مُفكّكات ترميز، مُحوّلات)، راجع أدوات المطورين الأساسية للترميز والتجزئة (2026).
قبل أن تشحن نمطًا مُعقّدًا، اختبره تفاعليًا. يبدّل regex101.com بين نكهات PCRE وJavaScript وPython وGo، ويشرح كل رمز، ويُظهر التراجع لتلاحظ الأنماط الكارثية.
12. الأسئلة الشائعة
ما الفرق بين * و+ في regex؟
* يطابق صفرًا أو أكثر من التكرارات (ويستطيع مطابقة سلسلة فارغة)، بينما + يطابق واحدًا أو أكثر (ويحتاج إلى تكرار واحد على الأقل). a* يطابق '' و'a' و'aaaa'، أما a+ فيطابق 'a' و'aaaa' ولا يطابق ''.
كيف أطابق عبر أسطر متعدّدة بـ regex؟
شغّل علم الأسطر المتعدّدة (/.../m في JavaScript، وre.MULTILINE في Python)، فيرسو ^ و$ على كل سطر. ولجعل . يعبر فواصل الأسطر أيضًا، أضف علم dotall (s في JavaScript، وre.DOTALL في Python).
هل regex هو نفسه في JavaScript وPython؟
البنية الأساسية (المُكمّمات، المراسي، فئات المحارف، المجموعات الأساسية) متطابقة بنسبة 90٪. الفارقان الحقيقيان: JavaScript (ES2018+) يدعم lookbehind بطول متغيّر ويكتب المجموعات المُسمّاة (?<name>...)، بينما re القياسية في Python تشترط طولًا ثابتًا لـ lookbehind وتستخدم (?P<name>...). وللحصول على lookbehind بطول متغيّر في Python، ثبّت حزمة regex من PyPI.
لماذا يعاني تعبيري النمطي من تراجع كارثي؟
لديك مُكمِّمات مُعشَّشة بمطابقات متداخلة، مثل (a+)+ أو (a|a)*. على مدخل يكاد يطابق ثم يفشل قرب النهاية، يجرّب المحرّك كل تقسيم ممكن للمُكمِّم الداخلي، أي عددًا أُسّيًا من المسارات. أصلح ذلك بمجموعة ذرّية (?>a+)+، أو بمُكمِّم استحواذي a++، أو بالتحوّل إلى محرّك لا يتراجع مثل RE2 أو regexp في Go.
هل يمكنني استخدام lookbehind في JavaScript؟
نعم. lookbehind الإيجابي (?<=...) والسلبي (?<!...) متاحان في V8 (Chrome، Node.js)، وSpiderMonkey (Firefox)، وJavaScriptCore (Safari 16.4+) منذ ES2018، والطول المتغيّر مدعوم. أما الإصدارات الأقدم من Safari فتَرجِم بـ Babel، أو اكشف الميزة بـ try/catch حول new RegExp.
كيف أطابق نقطة . حرفية في regex؟
هرّبها بشرطة مائلة خلفية: \. يطابق نقطة حرفية. داخل فئة محارف، النقطة حرفية أصلًا، فـ [.] و[\.] كلاهما يعمل. خارج الفئة، . غير المهرَّبة حرف خاص يعني «أيّ محرف ما عدا فاصل السطر»، أو أيّ محرف على الإطلاق مع علم dotall.
ماذا يعني \s في regex؟
يطابق \s أيّ محرف فراغ أبيض: مسافة، علامة جدولة، فاصل سطر، إرجاع عربة. وفي وضع Unicode يطابق أيضًا NBSP. أما \S فهو العكس.
هل التعبيرات النمطية حسّاسة لحالة الأحرف؟
نعم افتراضيًا. استخدم العلم i في JavaScript (/cat/i)، أو re.IGNORECASE / (?i) في Python.