Majid Al-RaimiRegular expressions in practiceالتعابير النمطية عمليا

ICS 582Lecture 02المحاضرة 02Part 09الجزء 09

Regular expressions in practiceالتعابير النمطية عمليا

Escaping, precedence and groups, substitutions with backreferences, lookarounds, the cheat sheets, the GPT-2 pre-tokenizer regex, and the engineering habits that keep patterns correct and fast.الهروب (escaping)، والأسبقية والمجموعات، والاستبدال مع المراجع الخلفية (backreferences)، والنظر حول النمط (lookarounds)، وأوراق المراجعة، وتعبير المُقسِّم المسبق في GPT-2، والعادات الهندسية التي تبقي الأنماط صحيحة وسريعة.

Conceptsالمفاهيم
6
Slidesالشرائح
66-78
Readingمدة القراءة
36 min36 دقيقة
Understoodما فهمته
0/6 conceptsمفاهيم

Why this part mattersلماذا يهمّ هذا الجزء

Part 08 gave you the alphabet of regular expressions. This part is about using it under real conditions: patterns that must match a literal period, patterns that must reorder a date, patterns that must enforce three rules at once, and one pattern that quietly decides how every GPT-2 style tokenizer sees text.

Most BPE tokenizers you will train or inspect, GPT-2's included, start with a regex pre-tokenizer, every cleaning script for an Arabic corpus is a chain of substitutions, and the exam asks you to work precedence, substitution and lookahead by hand. The cheat sheets on slides 73 to 75 are here as reference tables you can come back to, and the workbench lets you test every claim in the browser.

أعطاك الجزء 08 أبجدية التعابير النمطية (regular expressions). أما هذا الجزء فيتناول استخدامها في ظروف حقيقية: أنماط يجب أن تطابق نقطة حرفية، وأنماط يجب أن تعيد ترتيب تاريخ، وأنماط يجب أن تفرض ثلاث قواعد في آن واحد، ونمط واحد يحدّد بصمت كيف يرى كل tokenizer على طريقة GPT-2 النص.

معظم الـ tokenizers من نوع BPE التي ستدرّبها أو تفحصها، ومنها tokenizer الخاص بـ GPT-2، تبدأ بـ pre-tokenizer مبني على regex، وكل سكربت لتنظيف مدوّنة عربية هو سلسلة من عمليات الاستبدال، والامتحان يطلب منك أن تحلّ مسائل الأولوية والاستبدال والـ lookahead يدويًا. أوراق الملخّص (cheat sheets) في الشرائح 73 إلى 75 موجودة هنا كجداول مرجعية يمكنك الرجوع إليها، ومنصة التجربة (workbench) تتيح لك اختبار كل ادعاء في المتصفح.

By the end you canفي نهاية هذا الجزء ستستطيع

  1. Escape metacharacters and predict what (cat|dog)s versus cat|dogs matches using the precedence ladder.أن تُهرّب (escape) المحارف الخاصة (metacharacters) وتتوقع ما يطابقه النمط (cat|dog)s مقابل النمط cat|dogs باستخدام سُلّم الأولوية (precedence).
  2. Write a capture-group substitution that reorders a date and a backreference that finds a doubled word.أن تكتب استبدالًا بمجموعات الالتقاط (capture groups) يعيد ترتيب تاريخ، ومرجعًا خلفيًا (backreference) يجد كلمة مكرّرة.
  3. Write a lookahead-based password check and explain why lookahead consumes nothing.أن تكتب فحصًا لكلمة المرور مبنيًا على الـ lookahead، وتشرح لماذا لا يستهلك الـ lookahead أي نص.
  4. Explain each alternative of the GPT-2 pre-tokenizer on a given string and why it needs \p{L}.أن تشرح كل بديل في الـ pre-tokenizer الخاص بـ GPT-2 على نص معطى، ولماذا يحتاج إلى \p{L}.
  5. Recognize catastrophic backtracking, give an example, and name two ways to make a pattern Unicode-aware.أن تتعرّف على التراجع الكارثي (catastrophic backtracking) وتعطي مثالًا عليه، وتسمّي طريقتين لجعل النمط واعيًا بـ Unicode.

Suppose you want to find every mention of Dr. in a corpus. The obvious pattern Dr. also matches Drs and Dry, because a period in a pattern means any character. Written as Dr\., it matches only the period. That is the whole idea of escaping: fourteen characters, . ^ $ * + ? ( ) [ ] { } | \, mean something to the engine, and a backslash returns them to their plain selves.

لنفترض أنك تريد إيجاد كل ذكر لـ Dr. في مدوّنة نصية. الـ pattern البديهي Dr. يطابق أيضًا Drs وDry، لأن النقطة داخل النمط تعني أي محرف. وعندما يُكتب بالشكل Dr\. فإنه يطابق النقطة فقط. هذه هي فكرة الـ escaping (التهريب) كاملة: أربعة عشر محرفًا، . ^ $ * + ? ( ) [ ] { } | \، لها معنى خاص عند المحرّك، والـ backslash يعيدها إلى طبيعتها العادية.

Escapes from SLP3 Figure 2.14, plus the two that run the other wayمحارف التهريب من الشكل 2.14 في SLP3، مضافًا إليها الاثنان اللذان يعملان في الاتجاه المعاكس

\*
an asterisk, as in K*A*P*L*A*Nنجمة، كما في K*A*P*L*A*N
\.
a period, as in Dr. Livingston, I presumeنقطة، كما في Dr. Livingston, I presume
\?
a question mark, as in Why don't they come and lend a hand?علامة استفهام، كما في Why don't they come and lend a hand?
\n
a newline (the backslash gives a plain letter a special meaning)سطر جديد (newline)، وهنا يعطي الـ backslash حرفًا عاديًا معنى خاصًا
\t
a tab (same direction: plain letter, special meaning)مسافة جدولة (tab)، في الاتجاه نفسه: حرف عادي يأخذ معنى خاصًا

Notice that the backslash works in both directions. \. turns a special character into a literal one, while \n and \t turn plain letters into a newline and a tab. When a pattern needs a literal backslash it doubles: \\. In Python, write patterns as raw strings, r'...', so the string literal does not eat the backslashes before the regex engine sees them (Python re documentation).

لاحظ أن الـ backslash يعمل في الاتجاهين. \. يحوّل محرفًا خاصًا إلى محرف حرفي، بينما \n و\t يحوّلان حرفين عاديين إلى سطر جديد (newline) ومسافة جدولة (tab). وعندما يحتاج النمط إلى backslash حرفي فإنه يُضاعَف: \\. في Python اكتب الأنماط كسلاسل خام (raw strings)، r'...'، حتى لا تلتهم السلسلة الحرفية الـ backslashes قبل أن يراها محرّك الـ regex (توثيق Python re).

The precedence ladderسُلّم الأولوية (precedence)

Escaping decides what a character means. Precedence decides how the operators bind, and it is where most wrong answers come from. Take the pattern cat|dogs and run it on the string cats dogs cat dog. Students expect cats and dogs. The engine returns cat (inside cats), dogs, and cat again, because the disjunction operator | has the lowest precedence of all: it splits the entire expression into cat on one side and dogs on the other. Parentheses fence the alternation in, so (cat|dog)s reads as cat or dog, then s, and returns cats and dogs.

الـ escaping يحدّد ما يعنيه المحرف. أما الـ precedence (الأولوية) فتحدّد كيف ترتبط العوامل (operators)، ومنها تأتي معظم الإجابات الخاطئة. خذ النمط cat|dogs وشغّله على النص cats dogs cat dog. يتوقع الطلاب cats وdogs. لكن المحرّك يُرجع cat (داخل cats)، وdogs، ثم cat مرة أخرى، لأن عامل الاختيار (disjunction) | له أدنى أولوية على الإطلاق: فهو يقسم التعبير كله إلى cat في جهة وdogs في الجهة الأخرى. الأقواس تحصر الاختيار داخلها، فيُقرأ (cat|dog)s على أنه cat أو dog، ثم s، ويُرجع cats وdogs.

Where the vertical bar cuts depends on the parentheses. Same letters, two different sets of matches.موضع قطع الخط العمودي يعتمد على الأقواس. الحروف نفسها، ومجموعتان مختلفتان من المطابقات.
PatternالنمطReads asيُقرأ على أنهMatchesالمطابقات
cat|dogscat|dogscat, or dogscat، أو dogscat, dogs, catcat, dogs, cat
(cat|dog)s(cat|dog)scat or dog, then scat أو dog، ثم scats, dogscats, dogs
Two patterns on the string cats dogs cat dogنمطان على النص cats dogs cat dog

SLP3 (section 2.6.4) gives the full ladder. Parentheses bind tightest, then the counters (the quantifiers), then plain sequences and anchors, and last of all the disjunction. This is why the* matches theeeee and not thethe: the star applies to the single letter e, not to the whole sequence. It is why the|any matches the or any but never thany. And it is why guppy|ies fails to match guppies: it means guppy or ies, so the fix is gupp(y|ies).

يعطي SLP3 (القسم 2.6.4) السُّلّم كاملًا. الأقواس هي الأشد ارتباطًا، ثم العدّادات (أي الـ quantifiers)، ثم التتابعات العادية والـ anchors (المراسي)، وفي الأخير الاختيار (disjunction). لهذا يطابق the* النص theeeee وليس thethe: فالنجمة تنطبق على الحرف e وحده، لا على التتابع كله. ولهذا يطابق the|any إما the أو any لكنه لا يطابق thany أبدًا. ولهذا أيضًا يفشل guppy|ies في مطابقة guppies: فهو يعني guppy أو ies، والحل هو gupp(y|ies).

Operator precedence, highest first (SLP3, section 2.6.4)أولوية العوامل، من الأعلى إلى الأدنى (SLP3، القسم 2.6.4)

1 Parenthesis1 الأقواس
( )
2 Counters2 العدّادات (quantifiers)
* + ? {}
3 Sequences and anchors3 التتابعات والمراسي (anchors)
the, ^my end$
4 Disjunction4 الاختيار (disjunction)
|

The same ladder explains a subtler example from SLP3. Column [0-9]+ * matches one column label such as Column 1 followed by any number of spaces, because the star applies only to the final space. To repeat over a whole row of labels, Column 1 Column 2 Column 3, the sequence has to be fenced first. SLP3 writes (Column [0-9]+ +)*, which needs a space after every label, so on this row it stops before Column 3; (Column [0-9]+ *)* allows zero trailing spaces and takes the whole row.

السُّلّم نفسه يفسّر مثالًا أدقّ من SLP3. النمط Column [0-9]+ * يطابق عنوان عمود واحدًا مثل Column 1 متبوعًا بأي عدد من المسافات، لأن النجمة تنطبق على المسافة الأخيرة فقط. ولتكرار المطابقة على صف كامل من العناوين، Column 1 Column 2 Column 3، يجب أن يُحصر التتابع بين أقواس أولًا. يكتب SLP3 النمط (Column [0-9]+ +)*، الذي يتطلّب مسافة بعد كل عنوان، لذلك يتوقف على هذا الصف قبل Column 3؛ أما (Column [0-9]+ *)* فيسمح بصفر مسافات في النهاية ويأخذ الصف كله.

Grouping without rememberingالتجميع دون تذكّر

Parentheses do two jobs at once. They set the scope of an operator, and they create a capture group that stores whatever the group matched. Sometimes you want only the first job. Thenon-capturing form (?:...) groups without allocating a register, which keeps the numbering of the groups you do care about stable. SLP3 (2.17) uses it to skip fourteen dates and capture only the fifteenth: (?:\d\d/\d\d/\d\d\d\d\s+){14}(\d\d/\d\d/\d\d\d\d). The repeated dates are grouped so that the counter {14} applies to the whole date, and only the last date lands in group 1.

تؤدي الأقواس وظيفتين في الوقت نفسه. فهي تحدّد نطاق العامل، وتُنشئ capture group (مجموعة التقاط) تخزّن ما طابقته المجموعة. وأحيانًا تريد الوظيفة الأولى فقط. الصيغة غير الملتقِطة (non-capturing) (?:...) تجمّع دون حجز سجلّ (register)، وهذا يُبقي ترقيم المجموعات التي تهمّك ثابتًا. يستخدمها SLP3 (2.17) لتخطّي أربعة عشر تاريخًا والتقاط الخامس عشر فقط: (?:\d\d/\d\d/\d\d\d\d\s+){14}(\d\d/\d\d/\d\d\d\d). التواريخ المتكررة مجمّعة حتى ينطبق العدّاد {14} على التاريخ كاملًا، ولا يقع في المجموعة 1 إلا التاريخ الأخير.

Recall

Why does guppy|ies fail to match guppies, and what is the fix?لماذا يفشل النمط guppy|ies في مطابقة guppies، وما الحل؟

Sequences bind tighter than |, so the pattern means the whole word guppy or the whole string ies. Fence the alternation: gupp(y|ies).
التتابعات ترتبط أقوى من |، لذلك يعني النمط الكلمة كاملة guppy أو السلسلة كاملة ies. احصر الاختيار بين أقواس: gupp(y|ies).

Quick checkاختبار سريع

Which whole word does the pattern cat|dogs match but (cat|dog)s never matches?ما الكلمة الكاملة التي يطابقها النمط cat|dogs ولا يطابقها النمط (cat|dog)s أبدًا؟

Real patterns are assembled from small pieces, tested, and grown. Start with an email address. It has a local part, an at sign and a domain, so the very simplified pattern on slide 69 is three pieces glued together: [A-Za-z0-9._]+@[A-Za-z0-9._]+. A character class of letters, digits, dots and underscores, repeated at least once, on both sides of the @. The same compose-then-test loop gives a course-code ID pattern in one line: two to four uppercase letters, an optional space, three digits, [A-Z]{2,4}\s?\d{3}, which accepts ICS 582 and COE558 and rejects ICS58 (only two digits) and ICSSS 582 (five letters). The habit is the same in both: build the smallest piece that covers the positives, then run it against the nearest negatives.

الأنماط الحقيقية تُركَّب من قطع صغيرة، وتُختبَر، ثم تُوسَّع. ابدأ بعنوان بريد إلكتروني. له جزء محلي (local part)، وعلامة @، ونطاق (domain)، لذلك فالنمط المبسّط جدًا في الشريحة 69 هو ثلاث قطع ملصقة معًا: [A-Za-z0-9._]+@[A-Za-z0-9._]+. أي character class (فئة محارف) من الحروف والأرقام والنقاط والشرطات السفلية (underscores)، مكرّرة مرة واحدة على الأقل، على جانبي الـ @. وحلقة التركيب ثم الاختبار نفسها تعطي نمطًا لرمز المقرر في سطر واحد: من حرفين إلى أربعة أحرف كبيرة، ثم مسافة اختيارية، ثم ثلاثة أرقام، [A-Z]{2,4}\s?\d{3}، وهو يقبل ICS 582 وCOE558 ويرفض ICS58 (رقمان فقط) وICSSS 582 (خمسة أحرف). والعادة واحدة في الحالتين: ابنِ أصغر قطعة تغطي الأمثلة الموجبة، ثم شغّلها على أقرب الأمثلة السالبة.

AddressالعنوانWhat matchedما طوبِقVerdictالحكم
m.alraimi@kfupm.edu.sam.alraimi@kfupm.edu.sam.alraimi@kfupm.edu.sam.alraimi@kfupm.edu.sawhole address, fineالعنوان كاملًا، سليم
first-last@x.comfirst-last@x.comlast@x.comlast@x.comhyphen is not in the class, the local part is truncatedالشرطة ليست ضمن الفئة، فيُبتر الجزء المحلي
a+b@y.orga+b@y.orgb@y.orgb@y.orgplus is not in the class eitherعلامة الجمع ليست ضمن الفئة أيضًا
me@localhostme@localhostme@localhostme@localhostaccepted although there is no top-level domainمقبول رغم عدم وجود نطاق علوي (top-level domain)
The simplified email pattern on four addressesنمط البريد الإلكتروني المبسّط على أربعة عناوين

The table is an error analysis, and it exposes two failure modes that pull in opposite directions. The class is too narrow: hyphens and plus signs are legal in local parts, so the pattern silently truncates them and matches the wrong substring. That is a recall problem: real addresses the pattern should catch, it catches only in part. The domain side is too loose: it accepts me@localhost, which has no top-level domain. That is a precision problem: strings the pattern should reject, it accepts. Widening the class to [A-Za-z0-9._+-]+ raises recall; requiring a dot followed by at least two letters at the end, \.[A-Za-z]{2,}$, raises precision. Each fix is one small piece, tested on representative samples, and the point of slide 69 is to iterate this way rather than to write a pattern that fits the one address in front of you.

هذا الجدول تحليل للأخطاء (error analysis)، ويكشف نمطَي فشل يسحبان في اتجاهين متعاكسين. الفئة ضيّقة أكثر من اللازم: الشرطات (-) وعلامات الجمع (+) مسموحة في الجزء المحلي، فيبترها النمط بصمت ويطابق سلسلة جزئية خاطئة. هذه مشكلة recall (الاستدعاء): عناوين حقيقية كان يجب أن يلتقطها النمط، لكنه لا يلتقطها إلا جزئيًا. وجانب النطاق متساهل أكثر من اللازم: فهو يقبل me@localhost، الذي لا يحتوي على نطاق علوي (top-level domain). هذه مشكلة precision (الدقة): سلاسل كان يجب أن يرفضها النمط، لكنه يقبلها. توسيع الفئة إلى [A-Za-z0-9._+-]+ يرفع الـ recall؛ واشتراط نقطة يتبعها حرفان على الأقل في النهاية، \.[A-Za-z]{2,}$، يرفع الـ precision. كل إصلاح قطعة صغيرة واحدة، تُختبر على عيّنات ممثِّلة، والمغزى من الشريحة 69 هو أن تكرّر العمل بهذه الطريقة بدلًا من كتابة نمط يناسب العنوان الوحيد الذي أمامك.

Capture, then reuseالتقط، ثم أعد الاستخدام

Once a pattern matches, each capture group has stored a substring, and a substitution can put them back in any order. Slide 70 shows the classic sed notation s/(\d{4})-(\d{2})-(\d{2})/$2$3$1/. Work it on an ISO date.

بمجرد أن يطابق النمط، تكون كل capture group قد خزّنت سلسلة جزئية، ويمكن لعملية الاستبدال أن تعيدها بأي ترتيب. تعرض الشريحة 70 صيغة sed الكلاسيكية s/(\d{4})-(\d{2})-(\d{2})/$2$3$1/. طبّقها على تاريخ بصيغة ISO.

Worked exampleمثال محلول

Reordering 2026-01-25إعادة ترتيب 2026-01-25

  1. Name the groupsسمِّ المجموعات

    (\d{4})-(\d{2})-(\d{2}) matches four digits, a hyphen, two digits, a hyphen, two digits. Group 1 is 2026, group 2 is 01, group 3 is 25. The hyphens are matched but not captured.
    (\d{4})-(\d{2})-(\d{2}) يطابق أربعة أرقام، ثم شرطة، ثم رقمين، ثم شرطة، ثم رقمين. المجموعة 1 هي 2026، والمجموعة 2 هي 01، والمجموعة 3 هي 25. الشرطات تُطابَق لكنها لا تُلتقط.
  2. Apply the slide's replacementطبّق استبدال الشريحة

    $2$3$1 writes group 2, then 3, then 1 with nothing between them: 01252026. The separators vanished because a replacement only contains what you write into it.
    $2$3$1 يكتب المجموعة 2، ثم 3، ثم 1 بلا أي شيء بينها: 01252026. اختفت الفواصل لأن الاستبدال لا يحتوي إلا على ما تكتبه فيه.
  3. Write the separators yourselfاكتب الفواصل بنفسك

    Python syntax: re.sub(r'(\d{4})-(\d{2})-(\d{2})', r'\2/\3/\1', s) gives 01/25/2026. Swapping the first two references, \3/\2/\1, gives 25/01/2026.
    بصيغة Python: re.sub(r'(\d{4})-(\d{2})-(\d{2})', r'\2/\3/\1', s) يعطي 01/25/2026. وبتبديل أول مرجعين، \3/\2/\1، تحصل على 25/01/2026.
  4. Three replacements, three outputsثلاثة استبدالات، ثلاثة مخرجات

    The pattern never changes. Only the replacement string decides the order and the separators.
    النمط لا يتغيّر أبدًا. سلسلة الاستبدال وحدها هي التي تحدّد الترتيب والفواصل.
ReplacementالاستبدالOutputالمخرجReadingالقراءة
$2$3$1$2$3$10125202601252026slide 70: month, day, year, separators droppedالشريحة 70: الشهر، ثم اليوم، ثم السنة، مع حذف الفواصل
\2/\3/\1\2/\3/\101/25/202601/25/2026US order with slashes written by the replacementالترتيب الأمريكي، والشرطات المائلة يكتبها الاستبدال
\3/\2/\1\3/\2/\125/01/202625/01/2026day, month, year: the slide 72 targetاليوم، ثم الشهر، ثم السنة: الهدف المطلوب في الشريحة 72
One pattern, three replacement strings, on 2026-01-25نمط واحد، وثلاث سلاسل استبدال، على 2026-01-25

The syntax of the reference differs by tool. sed and Perl write $1 or \1 inside s/.../.../; JavaScript's replace uses $1 and $& for the whole match; Python's re.sub uses \1 in a raw string and \g<0> for the whole match (Python re documentation). SLP3 (section 2.6.7) uses the same trick to move US dates to European order: re.sub(r"(\d{2})/(\d{2})/(\d{4})", r"\2-\1-\3", s) turns 10/15/2011 into 15-10-2011.

صيغة المرجع تختلف من أداة لأخرى. sed وPerl يكتبان $1 أو \1 داخل s/.../.../؛ والدالة replace في JavaScript تستخدم $1، و$& للمطابقة كاملة؛ والدالة re.sub في Python تستخدم \1 داخل سلسلة خام، و\g<0> للمطابقة كاملة (توثيق Python re). ويستخدم SLP3 (القسم 2.6.7) الحيلة نفسها لنقل التواريخ الأمريكية إلى الترتيب الأوروبي: re.sub(r"(\d{2})/(\d{2})/(\d{4})", r"\2-\1-\3", s) يحوّل 10/15/2011 إلى 15-10-2011.

A backreference is not only for replacements. Inside the pattern itself, \1 means "the same text that group 1 just captured", so (\w+) \1 finds a doubled word. SLP3's careful version, \b([A-Za-z]+)\s+\1\b, restricts the group to letters, adds word boundaries and allows any whitespace; on the the cat sat on on the mat it returns the and on. This is the mechanism behind the faster-they-ran exercise in the next concept.

الـ backreference (المرجع الخلفي) ليس للاستبدالات فقط. داخل النمط نفسه، \1 تعني "النص نفسه الذي التقطته المجموعة 1 للتو"، لذلك يجد (\w+) \1 كلمة مكرّرة. والنسخة المتأنّية في SLP3، \b([A-Za-z]+)\s+\1\b، تقصر المجموعة على الحروف، وتضيف word boundaries (حدود الكلمات)، وتسمح بأي مسافة بيضاء؛ وعلى النص the the cat sat on on the mat تُرجع the وon. وهذه هي الآلية التي يقوم عليها تمرين faster-they-ran في المفهوم التالي.

Looking without movingالنظر دون تحرّك

The email and date patterns consume text left to right. Some rules cannot be written that way. A password must be at least eight characters, contain a capital letter, and contain a digit, and the three rules overlap: the same characters count for all of them. Lookahead solves this. (?=...) requires that a pattern could match starting here, (?!...) forbids it, and neither consumes anything. SLP3 (section 2.6.8) puts it precisely: the match pointer does not advance, just as with anchors.

أنماط البريد والتاريخ تستهلك النص من اليسار إلى اليمين. لكن بعض القواعد لا يمكن كتابتها بهذه الطريقة. يجب أن تكون كلمة المرور ثمانية محارف على الأقل، وأن تحتوي على حرف كبير، وأن تحتوي على رقم، والقواعد الثلاث متداخلة: المحارف نفسها تُحسب لها جميعًا. الـ lookahead (النظر إلى الأمام) يحلّ هذه المشكلة. (?=...) يشترط أن يكون نمطٌ ما قابلًا للمطابقة بدءًا من هنا، و(?!...) يمنع ذلك، ولا يستهلك أيٌّ منهما شيئًا. ويقولها SLP3 (القسم 2.6.8) بدقة: مؤشر المطابقة لا يتقدّم، تمامًا كما في الـ anchors.

Both lookaheads probe from position 0 and leave the pointer there. Only the final .{8,} consumes the string.كلا الـ lookaheads يستكشفان من الموضع 0 ويتركان المؤشر هناك. وحده .{8,} في النهاية يستهلك النص.

So ^(?=.*[A-Z])(?=.*\d).{8,}$ reads: at the start of the line, check that somewhere ahead there is a capital; still at the start, check that somewhere ahead there is a digit; now, still at the start, consume at least eight characters to the end. Because the two checks both begin at position 0 they can look at the same characters, which a consuming pattern could never do.

إذن يُقرأ ^(?=.*[A-Z])(?=.*\d).{8,}$ هكذا: عند بداية السطر، تحقّق من وجود حرف كبير في مكان ما أمامك؛ وما زلت عند البداية، تحقّق من وجود رقم في مكان ما أمامك؛ والآن، وما زلت عند البداية، استهلك ثمانية محارف على الأقل حتى النهاية. ولأن الفحصين يبدآن كلاهما من الموضع 0 فيمكنهما النظر إلى المحارف نفسها، وهذا ما لا يستطيعه نمط مستهلِك أبدًا.

CandidateالمرشّحةLengthالطولCapitalsالأحرف الكبيرةDigitsالأرقامResultالنتيجة
Passw0rdXPassw0rdX99P, XP, X00acceptedمقبولة
passwordpassword88noneلا يوجدnoneلا يوجدrejectedمرفوضة
PASSWORD1PASSWORD199allكلها11acceptedمقبولة
Pw1shortPw1short88PP11accepted, exactly 8مقبولة، 8 محارف بالضبط
The password pattern on four candidatesنمط كلمة المرور على أربع كلمات مرشّحة

Negative lookahead is just as useful. SLP3's equation 2.18, ^(?![tT])(\w+)\b, captures a word at the start of a line only if it does not begin with t or T. Without the lookahead you would need a class that lists every other letter.

والـ lookahead السالب مفيد بالقدر نفسه. المعادلة 2.18 في SLP3، ^(?![tT])(\w+)\b، تلتقط كلمة في بداية السطر فقط إذا لم تبدأ بـ t أو T. ومن دون الـ lookahead ستحتاج إلى فئة تسرد كل الحروف الأخرى.

Recall

What does re.sub(r'(\d{4})-(\d{2})-(\d{2})', r'\2\3\1', '2026-01-25') return, and why is there no separator?ماذا يُرجع re.sub(r'(\d{4})-(\d{2})-(\d{2})', r'\2\3\1', '2026-01-25')، ولماذا لا يوجد فاصل؟

01252026. The hyphens were matched but never captured, and the replacement writes only the three groups, so nothing puts a separator back.
01252026. الشرطات طوبِقت لكنها لم تُلتقط أبدًا، والاستبدال لا يكتب إلا المجموعات الثلاث، فلا شيء يعيد الفاصل.

Quick checkاختبار سريع

After the lookahead ^(?=.*\d) succeeds on Passw0rdX, where does the match pointer stand?بعد أن ينجح الـ lookahead ^(?=.*\d) على Passw0rdX، أين يقف مؤشر المطابقة؟

Slide 72 is a set of six problems with no answers. Each of them uses one tool from the previous two concepts, and together they cover everything the exam can ask about escaping, each capture group and its backreference, and lookahead. Try each one in the workbench before revealing the solution. The check button compares your matches against the expected list and tells you what is missing or extra.

الشريحة 72 مجموعة من ست مسائل بلا حلول. كل واحدة منها تستخدم أداة واحدة من المفهومين السابقين، وهي معًا تغطي كل ما يمكن أن يسأل عنه الامتحان في الـ escaping، وفي كل capture group والـ backreference الخاص بها، وفي الـ lookahead. جرّب كل مسألة في منصة التجربة قبل كشف الحل. زر الفحص يقارن مطابقاتك بالقائمة المتوقعة ويخبرك بما هو ناقص أو زائد.

InteractiveتفاعليRegex workbench: match, substitute and pre-tokenize with live feedbackمنضدة Regex: طابِق واستبدل وقسّم مبدئيًا مع نتائج فورية

Match the two tags <Begin> and <\End> and nothing else.طابِق الوسمين <Begin> و<\End> ولا شيء غيرهما.

Literalالصيغة الحرفية/…/g
26 / 300
Resultالنتيجة

Type a pattern to see live results.اكتب نمطًا لترى النتائج مباشرةً.

Patterns run in JavaScript syntax inside a worker with a 600 ms limit. Python differs in three places: backreferences in replacements are \1 not $1, named groups are (?P<name>...), and \p{L} needs the regex module.تعمل الأنماط بصياغة JavaScript داخل worker بحد زمني 600 ms. وتختلف Python في ثلاثة مواضع: المراجع الخلفية في نص الاستبدال تُكتب \1 لا $1، والمجموعات المسماة تُكتب (?P<name>...)، و\p{L} تحتاج إلى وحدة regex.

The six problems map one to one onto the tools of the previous two concepts. The tags problem rewards a negated class or an escaped backslash: <[^>]+> reads as "any run of non-closing-brackets inside brackets". The Apple problem is the one where a global i flag backfires, because it would also lowercase Juice, so the case-insensitivity has to live per letter inside a class like [Aa], or inside a scoped group like (?i:apple). The password problem is the argument for lookahead: three overlapping rules, all checked from the same position, none consuming input. The faster-they-ran sentence needs backreferences, capturing the repeated words once and reusing them as \1 and \2. The date swap is the classic capture-group substitution. The boxes problem shows that the whole-match reference, $& or \g<0>, lets you skip a group entirely when you do not need to rearrange anything.

المسائل الست تقابل أدوات المفهومين السابقين واحدة بواحدة. مسألة الوسوم (tags) تكافئ استخدام فئة منفية أو backslash مُهرَّب: يُقرأ <[^>]+> على أنه "أي سلسلة من محارف ليست أقواس إغلاق، داخل أقواس". ومسألة Apple هي التي ينقلب فيها العلم i العام ضدك، لأنه سيجعل Juice أيضًا غير حسّاسة لحالة الأحرف فيقبل حرفها الأول صغيرًا، لذلك يجب أن يكون عدم التمييز بين الحروف الكبيرة والصغيرة لكل حرف على حدة داخل فئة مثل [Aa]، أو داخل مجموعة محدودة النطاق مثل (?i:apple). ومسألة كلمة المرور هي الحجة على الـ lookahead: ثلاث قواعد متداخلة، تُفحص كلها من الموضع نفسه، ولا يستهلك أيٌّ منها المدخلات. وجملة faster-they-ran تحتاج إلى backreferences، بالتقاط الكلمات المكررة مرة واحدة وإعادة استخدامها بوصفها \1 و\2. وتبديل التاريخ هو الاستبدال الكلاسيكي بمجموعات الالتقاط. ومسألة الصناديق (boxes) تبيّن أن مرجع المطابقة الكاملة، $& أو \g<0>، يتيح لك الاستغناء عن المجموعة كليًا عندما لا تحتاج إلى إعادة ترتيب أي شيء.

Recall

Match the tags <Begin> and <\End> in the string <Begin> Hello World <\End>, and nothing else.طابِق الوسمين <Begin> و<\End> في النص <Begin> Hello World <\End>، ولا شيء غيرهما.

<\\?\w+>: an opening bracket, an optional escaped backslash (the closing tag uses a backslash where HTML would use a slash), one or more word characters, a closing bracket. <[^>]+> also works: anything except a closing bracket, inside brackets. Both return <Begin> and <\End>.
<\\?\w+>: قوس فتح، ثم backslash مُهرَّب اختياري (وسم الإغلاق يستخدم backslash حيث يستخدم HTML الشرطة المائلة)، ثم محرف كلمة واحد أو أكثر، ثم قوس إغلاق. و<[^>]+> يعمل أيضًا: أي شيء عدا قوس الإغلاق، داخل أقواس. وكلاهما يُرجع <Begin> و<\End>.

Recall

Match Apple Juice, apPLe Juice and APPLE Juice but not Apple juice.طابِق Apple Juice وapPLe Juice وAPPLE Juice، لكن ليس Apple juice.

The first word is case-insensitive, the second is not, so a global i flag is wrong. Spell the classes: [Aa][Pp][Pp][Ll][Ee] Juice. Engines with scoped flags allow (?i:apple) Juice (Python re documentation).
الكلمة الأولى غير حسّاسة لحالة الأحرف، والثانية حسّاسة، لذلك فالعلم i العام خاطئ. اكتب الفئات حرفًا حرفًا: [Aa][Pp][Pp][Ll][Ee] Juice. والمحرّكات التي تدعم الأعلام محدودة النطاق تسمح بـ (?i:apple) Juice (توثيق Python re).

Recall

Write a password check: at least 8 characters, at least one capital letter, at least one digit.اكتب فحصًا لكلمة المرور: 8 محارف على الأقل، وحرف كبير واحد على الأقل، ورقم واحد على الأقل.

^(?=.*[A-Z])(?=.*\d).{8,}$. Two lookaheads from the start, then a consuming .{8,} to the end. On the four test lines it accepts Passw0rdX, PASSWORD1 and Pw1short and rejects password.
^(?=.*[A-Z])(?=.*\d).{8,}$. اثنان من الـ lookaheads من البداية، ثم .{8,} مستهلِك حتى النهاية. وعلى أسطر الاختبار الأربعة يقبل Passw0rdX وPASSWORD1 وPw1short ويرفض password.

Recall

Match both 'The faster they ran, the faster we ran' and 'The faster they read, the faster we read' with one pattern.طابِق الجملتين 'The faster they ran, the faster we ran' و'The faster they read, the faster we read' بنمط واحد.

Capture the two words that repeat and refer back to them: [Tt]he (\w+) they (\w+), the \1 we \2. Group 1 captures faster, group 2 captures ran or read, and \1 and \2 must reproduce them. A mixed sentence, ran then read, does not match.
التقط الكلمتين اللتين تتكرران وأشِر إليهما مرة أخرى: [Tt]he (\w+) they (\w+), the \1 we \2. المجموعة 1 تلتقط faster، والمجموعة 2 تلتقط ran أو read، ويجب أن يعيد \1 و\2 إنتاجهما. والجملة المختلطة، ran ثم read، لا تُطابَق.

Recall

Convert 2026-01-25 to 25/01/2026 with capture groups.حوِّل 2026-01-25 إلى 25/01/2026 باستخدام مجموعات الالتقاط.

Pattern (\d{4})-(\d{2})-(\d{2}), replacement \3/\2/\1 in Python or $3/$2/$1 in JavaScript. The slashes are written by the replacement, not captured. For the slide's literal input 2026-25-01, the same pattern with \2/\3/\1 gives 25/01/2026; see the errata.
النمط (\d{4})-(\d{2})-(\d{2})، والاستبدال \3/\2/\1 في Python أو $3/$2/$1 في JavaScript. الشرطات المائلة تكتبها سلسلة الاستبدال، ولا تُلتقط. وبالنسبة إلى المدخل الحرفي في الشريحة، 2026-25-01، فإن النمط نفسه مع \2/\3/\1 يعطي 25/01/2026؛ راجع تصحيح الشريحة (errata).

Recall

Convert 'the 35 boxes' to 'the <35> boxes'.حوِّل 'the 35 boxes' إلى 'the <35> boxes'.

No group is needed if the tool can name the whole match: re.sub(r'\d+', r'<\g<0>>', s) in Python, or "the 35 boxes".replace(/\d+/g, "<$&>") in JavaScript. With a group, (\d+) and the replacement <\1>.
لا حاجة إلى مجموعة إذا كانت الأداة قادرة على تسمية المطابقة كاملة: re.sub(r'\d+', r'<\g<0>>', s) في Python، أو "the 35 boxes".replace(/\d+/g, "<$&>") في JavaScript. ومع مجموعة: (\d+) والاستبدال <\1>.

Slides 73 to 75 are the reference card for the whole regex block: every quantifier, character class, capture group and anchor in one place. They are reproduced here as tables so you can scan them during revision, with the two engine differences that the card leaves implicit: how line anchors behave under multiline mode, and which engines allow a variable-length lookbehind.

الشرائح 73 إلى 75 هي البطاقة المرجعية لكتلة الـ regex كلها: كل quantifier، وcharacter class، وcapture group، وanchor في مكان واحد. وهي معروضة هنا كجداول لتتصفّحها أثناء المراجعة، مع فرقين بين المحرّكات تتركهما البطاقة ضمنيين: كيف تتصرف مراسي الأسطر في وضع multiline، وأيّ المحرّكات تسمح بـ lookbehind متغيّر الطول.

Alternation and quantifiersالاختيار والـ quantifiers

a|b
alternation: a or bالاختيار (alternation): a أو b
?
zero or one of the preceding elementصفر أو مرة واحدة من العنصر السابق
+
one or moreمرة واحدة أو أكثر
*
zero or moreصفر مرة أو أكثر
*?
zero or more, lazy: stop as early as the rest of the pattern allowsصفر مرة أو أكثر، بصيغة كسولة (lazy): توقّف في أبكر موضع يسمح به باقي النمط
{N}
exactly NN مرة بالضبط
{N,M}
between N and Mبين N وM مرة

The lazy form matters more than its size suggests. Greedy .* grabs as much as it can and backs off only if the rest of the pattern fails; .*? takes as little as possible and grows only when it must. Part 08 showed the difference on HTML tags.

الصيغة الكسولة (lazy) أهم مما يوحي به حجمها. الـ greedy (الجشع) .* يلتهم أكبر قدر ممكن ولا يتراجع إلا إذا فشل باقي النمط؛ أما .*? فيأخذ أقل قدر ممكن ولا يكبر إلا عند الضرورة. وقد أظهر الجزء 08 الفرق على وسوم HTML.

Pattern collectionsمجموعات المحارف في الأنماط

[A-Z]
one uppercase ASCII letterحرف ASCII كبير واحد
[a-z]
one lowercase ASCII letterحرف ASCII صغير واحد
[0-9]
one ASCII digitرقم ASCII واحد
[asdf]
one of a, s, d, fمحرف واحد من a, s, d, f
[^asdf]
any one character except a, s, d, fأي محرف واحد عدا a, s, d, f

Groupsالمجموعات

(...)
capturing group: match and remember, referenced as \1 or $1مجموعة التقاط (capturing group): تطابق وتتذكّر، ويُشار إليها بـ \1 أو $1
(?:...)
non-capturing group: scope for | or a quantifier, nothing rememberedمجموعة غير ملتقِطة (non-capturing group): نطاق لـ | أو لـ quantifier، ولا يُتذكَّر شيء
(?<name>...)
named group; Python spells it (?P<name>...)مجموعة مسمّاة (named group)؛ وتكتبها Python بالشكل (?P<name>...)

General tokensرموز عامة

.
any character except newlineأي محرف عدا السطر الجديد (newline)
\n
newlineسطر جديد (newline)
\t
tabمسافة جدولة (tab)
\s
one whitespace characterمحرف مسافة بيضاء واحد (whitespace)
\S
one non-whitespace characterمحرف واحد ليس مسافة بيضاء
\w
one word character: letter, digit or underscoreمحرف كلمة واحد (word character): حرف أو رقم أو شرطة سفلية (underscore)
\W
one non-word characterمحرف واحد ليس من محارف الكلمة
\b
word boundary, zero widthحدّ كلمة (word boundary)، عديم العرض
\B
not a word boundary, zero widthليس حدّ كلمة، عديم العرض
^
start of string, or start of line under mبداية النص، أو بداية السطر مع العلم m
$
end of string, or end of line under mنهاية النص، أو نهاية السطر مع العلم m
\\
a literal backslashbackslash حرفي

Take one input and walk the flags. On the two-line string cat\ncar the pattern ^ca.$ shows the three flags in action. With no flags it matches nothing: ^ wants the very first position, and the dot cannot be a newline, so $ after a 3-letter match can never reach the end. Turning on m gives cat and car, one per line. Adding g reports both; without g a JavaScript exec returns only the first. The i flag is independent: it would let the pattern tolerate Cat too. Each flag flips exactly one assumption, anchor meaning, case, or how many results are returned.

خذ مدخلًا واحدًا ومرّ على الأعلام. على النص ذي السطرين cat\ncar يُظهر النمط ^ca.$ الأعلام الثلاثة وهي تعمل. من دون أعلام لا يطابق شيئًا: ^ يريد الموضع الأول تمامًا، والنقطة لا يمكن أن تكون سطرًا جديدًا، لذلك لا يمكن لـ $ بعد مطابقة من ثلاثة أحرف أن يصل إلى النهاية أبدًا. تشغيل m يعطي cat وcar، واحدة في كل سطر. وإضافة g تُبلغ عن كلتيهما؛ ومن دون g فإن exec في JavaScript يُرجع الأولى فقط. والعلم i مستقل: سيسمح للنمط بقبول Cat أيضًا. كل علم يقلب افتراضًا واحدًا بالضبط: معنى المرساة، أو حالة الأحرف، أو عدد النتائج المُرجَعة.

Flagsالأعلام (flags)

g
global: report every match, a JavaScript and regex101 flag; Python uses findall and sub insteadعام (global): أبلِغ عن كل مطابقة، وهو علم في JavaScript وregex101؛ أما Python فتستخدم findall وsub بدلًا منه
m
multiline: ^ and $ also match at each line breakمتعدد الأسطر (multiline): ^ و$ يطابقان أيضًا عند كل فاصل سطر
i
case-insensitiveغير حسّاس لحالة الأحرف (case-insensitive)
SyntaxالصيغةNameالاسمExampleمثالResultالنتيجة
(?=...)(?=...)positive lookaheadlookahead موجبfoo(?=bar)foo(?=bar)foo in foobar onlyfoo في foobar فقط
(?!...)(?!...)negative lookaheadlookahead سالبfoo(?!bar)foo(?!bar)foo in foobaz onlyfoo في foobaz فقط
(?<=...)(?<=...)positive lookbehindlookbehind موجب(?<=#)\d+(?<=#)\d+42 and 8 in #42 and 7 and #842 و8 في #42 and 7 and #8
(?<!...)(?<!...)negative lookbehindlookbehind سالب(?<!#)\b\d+(?<!#)\b\d+7 in the same string7 في النص نفسه
Lookarounds, tested on foobar foobaz and on #42 and 7 and #8الـ lookarounds، مُختبَرة على foobar foobaz وعلى #42 and 7 and #8

One detail to read closely in the table: (?<!#)\b\d+ needs its \b. Without the boundary, the engine could pick up the 2 of #42, because the position before that digit has 4 in front of it, not #, so the negative lookbehind does not object and the lookahead for a digit succeeds. The \b forces the match to begin where a word begins, so the 2 is ruled out and only 7 survives.

تفصيل يستحق القراءة بتمعّن في الجدول: (?<!#)\b\d+ يحتاج إلى الـ \b الخاص به. من دون الحدّ، قد يلتقط المحرّك الرقم 2 من #42، لأن الموضع الذي قبل هذا الرقم يسبقه 4 وليس #، فلا يعترض الـ lookbehind السالب، وينجح البحث عن رقم. الـ \b يجبر المطابقة على أن تبدأ حيث تبدأ كلمة، فيُستبعد الـ 2 ولا يبقى إلا 7.

Recall

Which lookbehinds does Python re accept, and what do JavaScript and the regex module allow instead?ما الـ lookbehinds التي تقبلها Python re، وماذا تسمح به JavaScript ووحدة regex بدلًا من ذلك؟

Python re accepts only fixed-width lookbehind, for example (?<=#) or (?<=a|b). JavaScript (ES2018) and the regex module allow variable-length lookbehind such as (?<=a+).
تقبل Python re الـ lookbehind ذا العرض الثابت فقط، مثل (?<=#) أو (?<=a|b). أما JavaScript (ES2018) ووحدة regex فتسمحان بـ lookbehind متغيّر الطول مثل (?<=a+).

Part 07 showed you this pattern as a list of six rules; now that you can read regex syntax, walk it character by character. Before GPT-2's BPE merges touch a sentence, one regex cuts it into pieces. On We're 350 dogs! Um, lunch? the pieces are We, 're, ␣350, ␣dogs, !, ␣Um, ,, ␣lunch and ?, nine in all, where ␣ marks a leading space that stays inside the piece. That regex is the pre-tokenizer, and the whole behavior of the tokenizer on numbers, punctuation and spaces is decided by it.

عرض لك الجزء 07 هذا النمط كقائمة من ست قواعد؛ والآن بعد أن أصبحت تقرأ صيغة الـ regex، امشِ عليه محرفًا محرفًا. قبل أن تلمس merges الـ BPE في GPT-2 أي جملة، يقطعها regex واحد إلى قطع. على الجملة We're 350 dogs! Um, lunch? تكون القطع: We، 're، ␣350، ␣dogs، !، ␣Um، ,، ␣lunch و?، أي تسع قطع في المجموع، حيث يرمز ␣ إلى مسافة بادئة تبقى داخل القطعة. هذا الـ regex هو الـ pre-tokenizer، وهو الذي يحدّد سلوك الـ tokenizer كله مع الأرقام وعلامات الترقيم والمسافات.

Recall

From part 07: what does a pre-tokenizer do, and why do BPE merges normally not cross its pieces?من الجزء 07: ماذا يفعل الـ pre-tokenizer، ولماذا لا تعبر عمليات دمج BPE قطعه عادةً؟

It runs a regex that splits text into words with their leading space, digit runs, punctuation, contractions and whitespace before BPE. Pair counts and merges are computed only inside a piece, so a pair split across two pieces is never a candidate and the final tokens are always parts of pieces.
يشغّل regex يقسم النص قبل BPE إلى كلمات مع المسافة التي تسبقها، وسلاسل أرقام، وعلامات ترقيم، واختصارات (contractions)، ومسافات بيضاء. عدّ الأزواج وعمليات الدمج تُحسب داخل القطعة فقط، لذلك فالزوج المقسوم بين قطعتين لا يكون مرشّحًا أبدًا، والـ tokens النهائية هي دائمًا أجزاء من القطع.
Eight cuts turn the sentence into nine pieces. Each piece keeps its own leading space, and BPE never merges across a cut.ثمانية قطوع تحوّل الجملة إلى تسع قطع. كل قطعة تحتفظ بمسافتها البادئة، وBPE لا يدمج أبدًا عبر خط القطع.

The pattern from the GPT-2 repository's encoder.py is 's|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+. It is a disjunction of six alternatives, and the engine tries them left to right at every position, taking the first that matches. Read it as six rules in priority order.

النمط في الملف encoder.py من مستودع GPT-2 هو 's|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+. إنه اختيار (disjunction) بين ستة بدائل، والمحرّك يجرّبها من اليسار إلى اليمين عند كل موضع، ويأخذ أول بديل يطابق. اقرأه كست قواعد مرتّبة حسب الأولوية.

AlternativeالبديلMeaningالمعنىPieces it producedالقطع التي أنتجها
's|'t|'re|'ve|'m|'ll|'d's|'t|'re|'ve|'m|'ll|'da contraction suffix, tried firstلاحقة اختصار (contraction)، وتُجرَّب أولًا're're
?\p{L}+ ?\p{L}+an optional space then one or more Unicode lettersمسافة اختيارية ثم حرف Unicode واحد أو أكثرWe, ␣dogs, ␣Um, ␣lunchWe, ␣dogs, ␣Um, ␣lunch
?\p{N}+ ?\p{N}+an optional space then one or more Unicode digitsمسافة اختيارية ثم رقم Unicode واحد أو أكثر␣350␣350
?[^\s\p{L}\p{N}]+ ?[^\s\p{L}\p{N}]+an optional space then a run that is neither space, letter nor digitمسافة اختيارية ثم سلسلة ليست مسافة ولا حرفًا ولا رقمًا!, comma, ?!، الفاصلة، ?
\s+(?!\S)\s+(?!\S)whitespace not followed by a non-space, so the last space is left for the next word; also any trailing whitespaceمسافات بيضاء لا يتبعها محرف غير أبيض، فتُترك المسافة الأخيرة للكلمة التالية؛ وكذلك أي مسافات بيضاء في نهاية النص␣␣ in Hello␣␣␣world␣␣ في Hello␣␣␣world
\s+\s+a single whitespace character directly before a non-space that the rule above cannot take: a lone newline or tab before a wordمحرف مسافة بيضاء واحد مباشرة قبل محرف غير أبيض لا تستطيع القاعدة السابقة أخذه: سطر جديد أو tab منفرد قبل كلمة⏎ in a⏎b⏎ في a⏎b
The six alternatives on We're 350 dogs! Um, lunch? and on Hello worldالبدائل الستة على We're 350 dogs! Um, lunch? وعلى Hello world

Order is the design. Contractions come first so that We're becomes We plus 're: at position 0 no contraction matches, so the letter rule takes We and stops at the apostrophe; at position 2 the contraction rule fires before the punctuation rule can grab the apostrophe alone. Had the letter rule come first the result would be the same for We, but if punctuation came before contractions you would get ' and then re, and the model would never learn that 're is one unit.

الترتيب هو التصميم. الاختصارات تأتي أولًا حتى تصبح We're القطعة We زائد 're: عند الموضع 0 لا يطابق أي اختصار، فتأخذ قاعدة الحروف We وتتوقف عند الفاصلة العليا (apostrophe)؛ وعند الموضع 2 تنطلق قاعدة الاختصارات قبل أن تتمكن قاعدة الترقيم من التقاط الفاصلة العليا وحدها. لو جاءت قاعدة الحروف أولًا لكانت النتيجة نفسها بالنسبة لـ We، لكن لو جاءت علامات الترقيم قبل الاختصارات لحصلت على ' ثم re، ولما تعلّم النموذج أبدًا أن 're وحدة واحدة.

The space handling is the subtle part. Letters and digits carry an optional leading space, so a word is normally tokenized together with the space before it: ␣dogs is one piece, and the model learns that word-initial pieces look different from word-internal ones. Runs of spaces are handled by \s+(?!\S): whitespace that is not followed by a non-space. On Hello␣␣␣world that alternative takes the first two spaces and stops, because the third space is followed by w, and the third space is then picked up by ?\p{L}+ as part of ␣world. The result is Hello, ␣␣, ␣world. Trailing whitespace at the end of a document also goes to \s+(?!\S), because at the end of the string nothing follows and the negative lookahead succeeds. The final plain \s+ fires only in the one case the rule above cannot handle: a single whitespace character directly before a non-space that no ␣? prefix will take, which means a lone newline or tab before a word. On a⏎b it produces the ⏎ piece.

التعامل مع المسافات هو الجزء الدقيق. الحروف والأرقام تحمل مسافة بادئة اختيارية، لذلك تُقطَّع الكلمة عادةً مع المسافة التي قبلها: ␣dogs قطعة واحدة، ويتعلّم النموذج أن القطع التي تبدأ بها الكلمات تبدو مختلفة عن القطع التي تقع داخل الكلمات. أما سلاسل المسافات فيتعامل معها \s+(?!\S): مسافات بيضاء لا يتبعها محرف غير أبيض. على Hello␣␣␣world يأخذ هذا البديل المسافتين الأوليين ويتوقف، لأن المسافة الثالثة يتبعها w، ثم يلتقط ?\p{L}+ المسافة الثالثة كجزء من ␣world. والنتيجة Hello، ␣␣، ␣world. والمسافات البيضاء في نهاية المستند تذهب أيضًا إلى \s+(?!\S)، لأنه لا شيء يتبعها في نهاية النص فينجح الـ lookahead السالب. أما \s+ العادي الأخير فلا ينطلق إلا في الحالة الوحيدة التي لا تستطيع القاعدة السابقة معالجتها: محرف أبيض واحد مباشرة قبل محرف غير أبيض لا تأخذه أي بادئة ␣?، أي سطر جديد أو tab منفرد قبل كلمة. وعلى a⏎b يُنتج القطعة ⏎.

Why regex and not reلماذا regex وليس re

The slide imports the third-party regex module under the name re. The reason is \p{L} and \p{N}. These are Unicode property classes: every character whose General_Category is Letter, and every character whose category is Number, as defined in Unicode Standard Annex 44. Python's built-in re does not support them; the regex module does, and so does JavaScript with the u flag (regex module documentation, MDN). The payoff is script independence. On مرحبا بالعالم ١٢٣ the same pattern returns مرحبا, ␣بالعالم and ␣١٢٣: Arabic letters are letters and Arabic-Indic digits are digits without any special case in the pattern. A pattern written with [A-Za-z] and [0-9] would still cut at the spaces, because the negated punctuation class excludes whitespace, but all three pieces would come out through the punctuation rule, indistinguishable from runs of symbols.

تستورد الشريحة وحدة regex الخارجية تحت الاسم re. والسبب هو \p{L} و\p{N}. هذه Unicode property classes (فئات خصائص Unicode): كل محرف فئته العامة (General_Category) هي Letter، وكل محرف فئته Number، كما يعرّفها الملحق 44 من معيار Unicode. وحدة re المدمجة في Python لا تدعمها؛ ووحدة regex تدعمها، وكذلك JavaScript مع العلم u (توثيق وحدة regex، وMDN). والمكسب هو الاستقلال عن نظام الكتابة. على مرحبا بالعالم ١٢٣ يُرجع النمط نفسه مرحبا، ␣بالعالم و␣١٢٣: الحروف العربية حروف، والأرقام العربية الهندية أرقام، دون أي حالة خاصة في النمط. أما النمط المكتوب بـ [A-Za-z] و[0-9] فسيظل يقطع عند المسافات، لأن فئة الترقيم المنفية تستثني المسافات البيضاء، لكن القطع الثلاث كلها ستخرج عبر قاعدة الترقيم، فلا يمكن تمييزها عن سلاسل من الرموز.

For the exam, two ways to make a pattern Unicode-aware: use property classes such as \p{L} and \p{N} instead of ASCII ranges, and apply Unicode normalization (NFC or NFKC, from part 05) to the input before matching so that composed and decomposed forms of the same letter cannot split a match.

للامتحان، طريقتان لجعل النمط واعيًا بـ Unicode: استخدم فئات الخصائص مثل \p{L} و\p{N} بدلًا من نطاقات ASCII، وطبّق Unicode normalization (التطبيع، NFC أو NFKC، من الجزء 05) على المدخل قبل المطابقة، حتى لا تتمكن الصيغ المركّبة والمفكّكة للحرف نفسه من تقسيم المطابقة.

InteractiveتفاعليRegex workbench: match, substitute and pre-tokenize with live feedbackمنضدة Regex: طابِق واستبدل وقسّم مبدئيًا مع نتائج فورية

Run the GPT-2 pre-tokenizer pattern and read which alternative produced each piece.شغّل نمط المقسِّم المبدئي لـGPT-2 واقرأ أي بديل في النمط أنتج كل قطعة.

Literalالصيغة الحرفية/'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+/gu
26 / 300
Resultالنتيجة

Type a pattern to see live results.اكتب نمطًا لترى النتائج مباشرةً.

Patterns run in JavaScript syntax inside a worker with a 600 ms limit. Python differs in three places: backreferences in replacements are \1 not $1, named groups are (?P<name>...), and \p{L} needs the regex module.تعمل الأنماط بصياغة JavaScript داخل worker بحد زمني 600 ms. وتختلف Python في ثلاثة مواضع: المراجع الخلفية في نص الاستبدال تُكتب \1 لا $1، والمجموعات المسماة تُكتب (?P<name>...)، و\p{L} تحتاج إلى وحدة regex.

What the pieces are forما الغرض من القطع

The purpose of the cut is to separate words, numbers, punctuation, contractions and whitespace before BPE sees the text. Then the merges run inside each piece. This is literally how encoder.py is written: it loops over re.findall(self.pat, text) and calls self.bpe(token) on each piece separately (GPT-2 repository). A merge can join d and ogs inside ␣dogs, but no merge can ever join dogs with the ! that follows it, because they were never in the same piece. Byte-level operation, from part 06, applies inside each piece too, which is why the tokenizer is also a byte-level one.

الغرض من القطع هو فصل الكلمات والأرقام وعلامات الترقيم والاختصارات والمسافات قبل أن يرى BPE النص. ثم تجري عمليات الدمج داخل كل قطعة. وهكذا كُتب encoder.py حرفيًا: يمرّ في حلقة على re.findall(self.pat, text) ويستدعي self.bpe(token) على كل قطعة على حدة (مستودع GPT-2). يمكن لعملية دمج أن تضم d وogs داخل ␣dogs، لكن لا يمكن لأي دمج أبدًا أن يضم dogs إلى ! الذي يليها، لأنهما لم يكونا في القطعة نفسها قط. والعمل على مستوى البايت، من الجزء 06، ينطبق داخل كل قطعة أيضًا، ولهذا فهذا الـ tokenizer هو أيضًا tokenizer من نوع byte-level.

The cut lines are a design choice, and later tokenizers move them. tiktoken's r50k pattern for GPT-2 is the same language hardened for speed: '(?:[sdmt]|ll|ve|re)| ?\p{L}++| ?\p{N}++| ?[^\s\p{L}\p{N}]++|\s++$|\s+(?!\S)|\s, with possessive quantifiers ++ that refuse to backtrack and an explicit end-of-string whitespace rule. cl100k_base, used by GPT-4, caps numbers at three digits with \p{N}{1,3}+ and makes contractions case-insensitive (tiktoken repository). Hugging Face's ByteLevel pre-tokenizer applies the same GPT-2 pattern unless you switch its use_regex option off (Hugging Face tokenizers documentation). SuperBPE (introduced in part 07) goes the other way: after a first stage that learns subwords inside pieces, a second stage is allowed to merge across whitespace, and Liu et al. (2025) report up to 33 percent fewer tokens at a 200k vocabulary. Every one of these is a change to the pre-tokenizer, not to the BPE algorithm.

خطوط القطع خيار تصميمي، والـ tokenizers اللاحقة تحرّكها. نمط r50k في tiktoken الخاص بـ GPT-2 هو اللغة نفسها بعد تقويتها من أجل السرعة: '(?:[sdmt]|ll|ve|re)| ?\p{L}++| ?\p{N}++| ?[^\s\p{L}\p{N}]++|\s++$|\s+(?!\S)|\s، مع possessive quantifiers ++ التي ترفض التراجع (backtrack)، وقاعدة صريحة للمسافات البيضاء في نهاية النص. وcl100k_base، المستخدم في GPT-4، يحدّ الأرقام بثلاث خانات عبر \p{N}{1,3}+ ويجعل الاختصارات غير حسّاسة لحالة الأحرف (مستودع tiktoken). والـ pre-tokenizer المسمّى ByteLevel في Hugging Face يطبّق نمط GPT-2 نفسه ما لم توقف خيار use_regex فيه (توثيق Hugging Face tokenizers). أما SuperBPE (الذي قُدِّم في الجزء 07) فيذهب في الاتجاه المعاكس: بعد مرحلة أولى تتعلّم الـ subwords داخل القطع، يُسمح لمرحلة ثانية بالدمج عبر المسافات، ويذكر Liu et al. (2025) انخفاضًا يصل إلى 33% في عدد الـ tokens عند مفردات بحجم 200k. وكل واحد من هذه هو تغيير في الـ pre-tokenizer، لا في خوارزمية BPE.

Recall

Explain each alternative of the GPT-2 pattern on We're 350 dogs!اشرح كل بديل في نمط GPT-2 على We're 350 dogs!

We by ?\p{L}+; 're by the contraction list; ␣350 by ?\p{N}+; ␣dogs by ?\p{L}+; ! by ?[^\s\p{L}\p{N}]+.
We عبر ?\p{L}+؛ و're عبر قائمة الاختصارات؛ و␣350 عبر ?\p{N}+؛ و␣dogs عبر ?\p{L}+؛ و! عبر ?[^\s\p{L}\p{N}]+.

Recall

Name two ways to make a pattern Unicode-aware.سمِّ طريقتين لجعل النمط واعيًا بـ Unicode.

Use Unicode property classes (\p{L}, \p{N}, \p{Nd}) through the regex module or the JavaScript u flag instead of [A-Za-z] and ASCII \d; and normalize the input to NFC or NFKC before matching.
استخدم فئات خصائص Unicode (\p{L}، \p{N}، \p{Nd}) عبر وحدة regex أو العلم u في JavaScript بدلًا من [A-Za-z] و\d بمعنى ASCII؛ وطبّع المدخل إلى NFC أو NFKC قبل المطابقة.

Quick checkاختبار سريع

In the GPT-2 pre-tokenizer, which alternative produces the piece ' 350' with its leading space?في الـ pre-tokenizer الخاص بـ GPT-2، أي بديل يُنتج القطعة ' 350' مع مسافتها البادئة؟

Slide 78 closes the regex block with four engineering habits. Three of them you have already practiced in this part without being told. The email pattern grew from an error analysis of four addresses: start simple, add cases as failures appear. The workbench tasks are unit tests: a fixed list of inputs with expected outputs, rerun after every edit. The GPT-2 pattern was explicit about Unicode: property classes and, from part 05, normalization before matching. The fourth habit is the one that bites in production.

تختم الشريحة 78 كتلة الـ regex بأربع عادات هندسية. ثلاث منها مارستها بالفعل في هذا الجزء دون أن يُقال لك. نمط البريد الإلكتروني نما من تحليل أخطاء على أربعة عناوين: ابدأ ببساطة، وأضف حالات كلما ظهرت أخطاء. ومهام منصة التجربة هي اختبارات وحدات (unit tests): قائمة ثابتة من المدخلات مع مخرجاتها المتوقعة، يُعاد تشغيلها بعد كل تعديل. ونمط GPT-2 كان صريحًا بشأن Unicode: فئات الخصائص، والتطبيع قبل المطابقة من الجزء 05. أما العادة الرابعة فهي التي تلدغك في بيئة الإنتاج (production).

Catastrophic backtrackingالتراجع الكارثي (catastrophic backtracking)

Consider (a+)+b against aaaaaaaa!. The pattern says: one or more groups, each of one or more a's, followed by b. There is no b, so the answer is no. But a backtracking engine does not know that until it has tried every way of splitting the eight a's between the inner plus and the outer plus: all eight in one group, seven then one, one then seven, two then six, and so on. The number of ways to partition n a's into ordered runs is 2^(n-1), and each of them ends at the same exclamation mark. Add one a and the work doubles. This is catastrophic backtracking.

خذ (a+)+b على النص aaaaaaaa!. يقول النمط: مجموعة واحدة أو أكثر، كل منها حرف a واحد أو أكثر، يتبعها b. لا يوجد b، لذلك فالجواب لا. لكن المحرّك القائم على التراجع (backtracking engine) لا يعرف ذلك حتى يجرّب كل طريقة لتقسيم حروف a الثمانية بين الـ plus الداخلي والـ plus الخارجي: الثمانية كلها في مجموعة واحدة، سبعة ثم واحد، واحد ثم سبعة، اثنان ثم ستة، وهكذا. عدد طرق تقسيم n حرفًا من a إلى سلاسل مرتّبة هو 2^(n-1)، وكل واحدة منها تنتهي عند علامة التعجب نفسها. أضف حرف a واحدًا فيتضاعف العمل. هذا هو catastrophic backtracking.

Nested quantifiers branch at every a and every branch ends at the same dead end. A single plus has only one split per start position.الـ quantifiers المتداخلة تتفرّع عند كل a، وكل فرع ينتهي إلى الطريق المسدود نفسه. أما الـ plus المنفرد فله تقسيم واحد فقط لكل موضع بداية.

Worked exampleمثال محلول

Measuring the explosionقياس الانفجار

  1. Set up two patternsجهّز نمطين

    (a+)+b and a+b, both run against n a's followed by an exclamation mark, in Python re and in JavaScript.
    (a+)+b وa+b، يُشغَّل كلاهما على n حرفًا من a تتبعها علامة تعجب، في Python re وفي JavaScript.
  2. Time the nested patternقِس زمن النمط المتداخل

    Python time roughly quadruples every two extra characters (see the table below), which is doubling per character. JavaScript follows the same curve, roughly seven to fourteen times faster.
    زمن Python يتضاعف أربع مرات تقريبًا مع كل حرفين إضافيين (انظر الجدول أدناه)، أي أنه يتضاعف مرتين مع كل حرف. وJavaScript يتبع المنحنى نفسه، لكنه أسرع بنحو سبع إلى أربع عشرة مرة.
  3. Time the flat patternقِس زمن النمط المسطّح

    a+b on the same strings stays under 60 microseconds in Python at every n.a+b has only one way to split any run of a's, so the work is at most polynomial (each start position backs off once), never exponential.
    a+b على النصوص نفسها يبقى تحت 60 ميكروثانية في Python عند كل قيمة لـ n. للنمط a+b طريقة واحدة فقط لتقسيم أي سلسلة من a، لذلك فالعمل على الأكثر متعدد الحدود (polynomial)، إذ يتراجع كل موضع بداية مرة واحدة، وليس أُسّيًا أبدًا.
  4. Same strings, same answer, more than four orders of magnitude apartالنصوص نفسها، والجواب نفسه، وفرق يزيد على أربع رُتَب عشرية

    Run time depends on the structure of the pattern and the shape of the input, not on the length of the pattern.
    زمن التشغيل يعتمد على بنية النمط وشكل المدخل، لا على طول النمط.
nnPython rePython reJavaScriptJavaScript
202043 ms43 ms8.3 ms8.3 ms
2222175 ms175 ms31 ms31 ms
2424711 ms711 ms126 ms126 ms
26262900 ms2900 ms506 ms506 ms
Measured time for (a+)+b on n a's and a ! (local runs; a+b was under 0.06 ms throughout)الزمن المُقاس للنمط (a+)+b على n حرفًا من a تتبعها ! (تشغيل محلي؛ وكان a+b أقل من 0.06 ms طوال الوقت)

Russ Cox (2007) documents the classic case: a pattern of a? repeated n times followed by a repeated n times, matched against a string of n a's. At n = 29 Perl needs over sixty seconds, while a Thompson NFA, which simulates all states at once, takes about twenty microseconds. OWASP lists (a+)+ among its evil patterns because a single crafted input can pin a server's CPU, an attack it calls ReDoS. Both sources agree on the cause: an engine that explores alternatives one at a time, on a pattern with overlapping ways to match the same text.

يوثّق Russ Cox (2007) الحالة الكلاسيكية: نمط من a? مكرّرًا n مرة متبوعًا بـ a مكرّرًا n مرة، يُطابَق مع نص من n حرفًا من a. عند n = 29 يحتاج Perl إلى أكثر من ستين ثانية، بينما يستغرق Thompson NFA، الذي يحاكي كل الحالات في آن واحد، نحو عشرين ميكروثانية. وتُدرج OWASP النمط (a+)+ ضمن أنماطها الشريرة (evil patterns)، لأن مدخلًا واحدًا مصمّمًا بعناية يمكنه أن يُشغل معالج الخادم بالكامل، وهو هجوم تسمّيه ReDoS. ويتفق المصدران على السبب: محرّك يستكشف البدائل واحدًا تلو الآخر، على نمط له طرق متداخلة لمطابقة النص نفسه.

InteractiveتفاعليRegex workbench: match, substitute and pre-tokenize with live feedbackمنضدة Regex: طابِق واستبدل وقسّم مبدئيًا مع نتائج فورية

Watch the engine time as the input grows. Then change the pattern to ^a+b and try again.راقب زمن المحرك مع ازدياد طول المدخل. ثم غيّر النمط إلى ^a+b وجرّب مرة أخرى.

Literalالصيغة الحرفية/^(a+)+b/g
19 / 300
Resultالنتيجة

Type a pattern to see live results.اكتب نمطًا لترى النتائج مباشرةً.

Patterns run in JavaScript syntax inside a worker with a 600 ms limit. Python differs in three places: backreferences in replacements are \1 not $1, named groups are (?P<name>...), and \p{L} needs the regex module.تعمل الأنماط بصياغة JavaScript داخل worker بحد زمني 600 ms. وتختلف Python في ثلاثة مواضع: المراجع الخلفية في نص الاستبدال تُكتب \1 لا $1، والمجموعات المسماة تُكتب (?P<name>...)، و\p{L} تحتاج إلى وحدة regex.

Three fixes, in order of preference. Rewrite the pattern so that quantifiers do not nest over the same characters: a+b matches exactly the same strings as (a+)+b. Lazy quantifiers do not prevent the explosion either: (a+?)+b backtracks just like (a+)+b, because laziness only changes the order in which splits are tried, not how many exist. Use possessive quantifiers or atomic groups where the engine offers them: tiktoken's \p{L}++ is precisely this, a plus that refuses to give characters back. Or run a linear-time engine such as RE2, Go's regexp or Rust's regex crate, which reject backreferences and lookaround in exchange for a guarantee that time is linear in the input.

ثلاثة إصلاحات، مرتّبة حسب الأفضلية. أعد كتابة النمط بحيث لا تتداخل الـ quantifiers فوق المحارف نفسها: a+b يطابق بالضبط النصوص نفسها التي يطابقها (a+)+b. والـ quantifiers الكسولة (lazy) لا تمنع الانفجار هي الأخرى: (a+?)+b يتراجع تمامًا مثل (a+)+b، لأن الكسل يغيّر فقط ترتيب تجربة التقسيمات، لا عددها. استخدم الـ possessive quantifiers أو المجموعات الذرّية (atomic groups) حيث يوفّرها المحرّك: \p{L}++ في tiktoken هو هذا تحديدًا، plus يرفض إعادة المحارف. أو شغّل محرّكًا خطّي الزمن مثل RE2، أو regexp في Go، أو regex crate في Rust، وهي ترفض الـ backreferences والـ lookaround مقابل ضمان أن يكون الزمن خطّيًا في طول المدخل.

Recall

Give an example of catastrophic backtracking and a fix.أعطِ مثالًا على catastrophic backtracking وإصلاحًا له.

(a+)+b on 26 a's and an exclamation mark takes about 2.9 s in Python because the engine tries every split of the a's. a+b matches the same strings in microseconds.
(a+)+b على 26 حرفًا من a وعلامة تعجب يستغرق نحو 2.9 s في Python، لأن المحرّك يجرّب كل تقسيم لحروف a. أما a+b فيطابق النصوص نفسها في ميكروثوانٍ.

Quick checkاختبار سريع

Why does (a+)+b take seconds on aaaaaaaaaaaaaaaaaaaaaaaaaa! while a+b takes microseconds?لماذا يستغرق (a+)+b ثواني على aaaaaaaaaaaaaaaaaaaaaaaaaa! بينما يستغرق a+b ميكروثوانٍ؟

Recapالخلاصة

If you remember nothing elseإن لم تتذكر شيئاً آخر فتذكّر هذا

  • The metacharacters . ^ $ * + ? ( ) [ ] { } | \ need a backslash to match literally; \n and \t go the other way and give a plain letter a special meaning.المحارف الخاصة . ^ $ * + ? ( ) [ ] { } | \ تحتاج إلى backslash لتُطابَق حرفيًا؛ أما \n و\t فتعملان في الاتجاه المعاكس وتعطيان حرفًا عاديًا معنى خاصًا.
  • Precedence: parentheses, then counters, then sequences and anchors, then |. (cat|dog)s matches cats and dogs; cat|dogs matches cat and dogs.الأولوية: الأقواس، ثم العدّادات (quantifiers)، ثم التتابعات والمراسي (anchors)، ثم |. النمط (cat|dog)s يطابق cats وdogs؛ والنمط cat|dogs يطابق cat وdogs.
  • Capture groups only record. \1 in the pattern or $1 (Python \1) in the replacement reuses them; (?:...) groups without recording.مجموعات الالتقاط تسجّل فقط. \1 داخل النمط، أو $1 (و\1 في Python) داخل الاستبدال، يعيد استخدامها؛ و(?:...) يجمّع دون تسجيل.
  • (\d{4})-(\d{2})-(\d{2}) with \3/\2/\1 turns 2026-01-25 into 25/01/2026; slide 70's $2$3$1 gives 01252026 because the separators were never captured.النمط (\d{4})-(\d{2})-(\d{2}) مع \3/\2/\1 يحوّل 2026-01-25 إلى 25/01/2026؛ أما $2$3$1 في الشريحة 70 فيعطي 01252026 لأن الفواصل لم تُلتقط أصلًا.
  • Lookahead (?=...) and (?!...) are zero-width; stacking them expresses overlapping rules such as a password check.الـ lookahead بصيغتيه (?=...) و(?!...) عديم العرض (zero-width)؛ وتكديس أكثر من واحد يعبّر عن قواعد متداخلة مثل فحص كلمة المرور.
  • \d is Unicode Nd in Python re but ASCII only in JavaScript; \p{L} and \p{N} need the regex module or the u flag.الرمز \d في re الخاصة بـ Python يعني أرقام Unicode من الفئة Nd، لكنه ASCII فقط في JavaScript؛ و\p{L} و\p{N} تحتاجان إلى وحدة regex أو العلم u.
  • The GPT-2 pre-tokenizer tries contractions, letters, numbers, punctuation and whitespace in that order; BPE merges run inside each piece.الـ pre-tokenizer في GPT-2 يجرّب الاختصارات (contractions)، ثم الحروف، ثم الأرقام، ثم علامات الترقيم، ثم المسافات البيضاء بهذا الترتيب؛ وعمليات دمج BPE تجري داخل كل قطعة.
  • \s+(?!\S) eats a run of spaces but leaves the last one attached to the next word.النمط \s+(?!\S) يلتهم سلسلة من المسافات لكنه يترك المسافة الأخيرة ملتصقة بالكلمة التالية.
  • Nested quantifiers such as (a+)+b backtrack exponentially: seconds at 26 characters in Python, microseconds for a+b.الـ quantifiers المتداخلة مثل (a+)+b تتراجع (backtrack) تراجعًا أُسّيًا: ثوانٍ عند 26 حرفًا في Python، مقابل ميكروثوانٍ للنمط a+b.

Sourcesالمصادر