Majid Al-RaimiRegular expressions: the core syntax

ICS 582Lecture 02Part 08

Regular expressions: the core syntax

From ELIZA's pattern rules to the building blocks of regular expressions: literals, bracket classes, ranges, negation, quantifiers, wildcards, anchors and the backslash aliases.

Concepts
4
Slides
56-65
Reading
24 min
Understood
0/4 concepts

Why this part matters

Every tokenizer you will build or inspect, every corpus cleaning script for Arabic or English, every date or number normalizer, and every exam question on this chapter runs through the same ten or so symbols. The GPT-2 pre-tokenizer later in this lecture is one regular expression. Edit distance in the next parts starts from tokens that a regex produced.

This part builds the core syntax from the ground up, starting with the program that made pattern matching famous. ELIZA held conversations in 1966 using nothing but keyword rules and text substitution, and people believed it understood them. From there we take the symbols one family at a time: literals and disjunction, bracket classes, quantifiers and the wildcard, anchors and the backslash aliases. The next part adds precedence, groups, substitution and lookahead on top of exactly this vocabulary, so every table here is worth being able to reproduce from memory. A live tester at the end of the part loads every example row from the slides so you can check each first match yourself.

By the end you can

  1. Explain how ELIZA produced replies from keyword substitution rules and name the Eliza effect.
  2. Read and write literals, concatenation, disjunction and bracket classes, including ranges and negation.
  3. Apply * + ? {n} {m,n} and the wildcard, and predict greedy versus lazy matches on a given string.
  4. Use ^ $ \b to match whole words and line-based formats, and explain why anchors consume no characters.
  5. Expand each alias \d \D \w \W \s \S and state where Python and JavaScript flavors differ.

In 1966 Joseph Weizenbaum at MIT published a program that imitated a Rogerian psychotherapist, the kind of therapist who reflects the patient's own words back as questions. He called it ELIZA. Read the exchange below as a patient would have, one line at a time.

SpeakerLine
UserI need some help, that much seems certain.
ELIZAWHAT WOULD IT MEAN TO YOU IF YOU GOT SOME HELP
UserPerhaps I could learn to get along with my mother.
ELIZATELL ME MORE ABOUT YOUR FAMILY
UserMy mother takes care of me.
ELIZAWHO ELSE IN YOUR FAMILY TAKES CARE OF YOU
UserMy father.
ELIZAYOUR FATHER
UserYou are like my father in some ways.
The ELIZA transcript on slide 56, with Weizenbaum's original spelling restored on line 6

It reads like attention. It is not. The whole program is a list of keywords, and for each keyword a small set of decomposition rules that split the input around the keyword, paired with reassembly rules that build a reply out of the pieces. Weizenbaum wrote the rules in his own notation. A decomposition rule such as (0 YOU 0 ME) means "an indefinite number of words, then YOU, then an indefinite number of words, then ME", and its reassembly rule (WHAT MAKES YOU THINK I 3 YOU) inserts the third component, whatever words stood between YOU and ME, into a fixed frame (Weizenbaum, 1966, p. 38). The dictionary he had built "so far" contained about 50 keywords. Nothing is parsed, nothing is understood, and the program has no memory of what "mother" means beyond the fact that it is filed under FAMILY.

Jurafsky and Martin rewrite the same idea in modern regular expression syntax, and this is the version to remember. After the input is uppercased and pronouns are swapped (I'M becomes YOU ARE, MY becomes YOUR), rules like these run in order until one fires (SLP3, section 2.6.7):

  • re.sub(r".* YOU ARE (DEPRESSED|SAD) .*", r"I AM SORRY TO HEAR YOU ARE \1", input)
  • re.sub(r".* ALWAYS .*", r"CAN YOU THINK OF A SPECIFIC EXAMPLE", input)

Worked example

One ELIZA reply, step by step

  1. User types

    He says I'm depressed much of the time.
  2. Uppercase

    HE SAYS I'M DEPRESSED MUCH OF THE TIME.
  3. Swap pronouns

    I'M becomes YOU ARE: HE SAYS YOU ARE DEPRESSED MUCH OF THE TIME.
  4. Try the rules in order

    The pattern .* YOU ARE (DEPRESSED|SAD) .* matches: .* swallows HE SAYS, the literal YOU ARE matches, the parenthesized alternative picks DEPRESSED, and the trailing .* swallows the rest.
  5. Reassemble

    The replacement I AM SORRY TO HEAR YOU ARE \1 copies whatever the parentheses captured into the slot marked \1.
  6. Result

    I AM SORRY TO HEAR YOU ARE DEPRESSED, which is the reply SLP3 derives in section 2.6.7 and the fourth reply in Weizenbaum's opening transcript. Swap DEPRESSED for SAD in the input and the same rule prints SAD.
The parenthesized alternative captures DEPRESSED, and the reassembly frame copies it into the reply. Nothing else in the sentence is read.
Weizenbaum (1966)MeaningModern regex
0 in a decomposition ruleAn indefinite number of words.*
A positive integer n in a decomposition ruleExactly n words((?:\S+ ){n}), one group holding exactly n words
3 in a reassembly ruleInsert the third component the decomposition found\1 style backreference to a capture group
Keyword dictionary, about 50 entriesWhich rule set to try, ranked by keywordA cascade of re.sub calls tried in order
Weizenbaum's 1966 notation next to the regex you would write today

The parentheses and the \1 are a capture group and a Backreference, which the next part treats properly. For now the point is the shape of the machine: a keyword selects a rule, a pattern with wildcards splits the sentence, a frame reassembles the pieces. Weizenbaum's paper opens with the exchange "Men are all alike." answered by "IN WHAT WAY", and the reply is not about men at all: a rule keyed on the wording of the input prints a fixed question with no slot to fill.

Why people believed it

Crude as the rules were, SLP3 notes that many people who talked to ELIZA came to believe that it really understood them, and that this led researchers to think for the first time about the impact of chatbots on their users (Weizenbaum, 1976). The effect now carries the program's name. The Eliza effect is the tendency to unconsciously assume that computer behaviors are analogous to human behaviors. Douglas Hofstadter defines its specific form as our susceptibility to read far more understanding than is warranted into strings of symbols strung together by computers (Hofstadter, 1995). The standard illustration is not a chatbot at all but a cash machine that prints THANK YOU at the end of a transaction, which a naive observer might take for real gratitude. ELIZA exploited the same reflex at conversational scale: because it echoed your own words, every reply seemed relevant, and relevance felt like understanding.

What regex is for in an NLP pipeline

A regular expression is fast, flexible pattern matching for text. It needs no training data, and it is precise in a way a learned model cannot be: a pattern either matches or it does not, and you can read off why. The slide lists four jobs, and it is worth having a concrete example ready for each.

Four jobs regex does in NLP (slide 57), with an example each

Tokenization and pre-tokenization
Split text into words, numbers and punctuation before a BPE tokenizer merges inside each piece. The GPT-2 pre-tokenizer in the next part is one regex.
Normalization
Recognize dates and numbers in any of their surface forms and rewrite them to one canonical form, such as dd-mm-yyyy.
Filtering and cleaning
Drop lines that are all punctuation, strip HTML tags, remove boilerplate, detect a script you do not want in a training corpus.
Feature extraction
Does the token contain a digit, start with a capital, end in -ing? Each answer is a binary feature a classifier can use.

The slide's last line matters more than it looks: regex is language- and task-dependent. A class like [a-z] says nothing about Arabic letters. A word boundary \b assumes words are made of letters, digits and underscores separated by something else, which fails outright for Chinese, where there are no spaces, and quietly misbehaves for Arabic under an ASCII-only definition of "letter". Recall the typology in part 04: an isolating language and a templatic one need different patterns for the same task, and a pattern tuned for tweets will not fit legal text. Regex gives you precision; it does not give you generality for free.

Quick check

Which mechanism produced ELIZA's reply YOUR FATHER from the input My father?

Recall

Why did ELIZA convince people, in two sentences: one about the program and one about the people?

The program: keyword-triggered decomposition and reassembly rules with pronoun swapping echo the user's own words back, so every reply is on topic. The people: the Eliza effect, our tendency to read understanding into any behavior that looks conversational.

Start with the simplest pattern there is. The regex Buttercup matches the substring Buttercup in "I'm called little Buttercup" and nowhere else (SLP3, section 2.6.1). Each letter is a literal that matches itself, and writing letters one after another means "this, then this, then this". That is concatenation, and abc matches exactly the three characters abc in that order.

Two more operators complete the algebra on slide 58. The vertical bar is disjunction: a|b matches either a or b. Parentheses group and define scope, so gupp(y|ies) matches guppy or guppies, while guppy|ies would match guppy or the bare string ies, because the bar has the lowest precedence of all and splits the whole pattern into two halves (SLP3, section 2.6.4). Regex is case sensitive throughout: s does not match S. Concatenation, disjunction and the Kleene star, which the next concept introduces, are the three operations that define regular languages in theory of computation. Parentheses only set scope, and classes, ranges, +, ? and counted repetition are all shorthand built from those three.

The square-bracket class

Disjunction between single characters is so common that it has its own notation. A Character class in square brackets matches exactly one character from the set it lists. This is the fix for case sensitivity: [mM]ary matches Mary or mary. Read every row of the next three tables as the slides present them, pattern, meaning, and the first match marked in the example string.

PatternMatchesFirst match in the example
[mM]aryMary or maryMary Ann stopped by Mona's
[abc]a, b or cIn uomini, in soldati
[1234567890]Any digitplenty of 7 to 5
Slide 59: bracket disjunction (SLP3 Fig 2.8). The marked span is the first match.

Look closely at the second row. "In uomini" contains no a, b or c, so the first match is the a in soldati, eighteen characters in. A class matches one character, so a bracket never matches "abc" as a word; SLP3 asks why [catdog] does not mean cat or dog, and the answer is the same: it is one character drawn from c, a, t, d, o, g. Listing every digit is tedious, so a dash inside brackets gives a range in code point order: [0-9], [a-z], [A-Z], but also [2-5] or [b-g].

PatternMatchesFirst match in the example
[A-Z]An upper case letterwe should call it 'Drenched Blossoms'
[a-z]A lower case lettermy beans were impatient to be hoed!
[0-9]A single digitChapter 1: Down the Rabbit Hole
Slide 60: ranges with a dash (SLP3 Fig 2.9)

The caret is the last bracket operator, and it is the one that trips people. When the caret is the first character inside brackets, the class is negated and matches any single character not in the set. Anywhere else inside brackets it is an ordinary caret. Python's documentation states both halves: the caret "has no special meaning if it's not the first character in the set", and [^^] matches any character except a caret.

PatternMatchesFirst match in the example
[^A-Z]Not an upper case letterOyfn pripetchik
[^Ss]Neither S nor sI have no exquisite reason for't
[^.]Not a periodour resident Djinn
[e^]Either e or a caretlook up ^ now
a^bThe pattern a^b, as the slide claimslook up a^b now (no match)
a\^bThe literal string a^b, escapedlook up a^b now
Slide 61: caret inside brackets (SLP3 Fig 2.10)
Three faces of one symbol: an anchor at the line start outside brackets, a negating gate when first inside brackets, a plain character elsewhere inside brackets.

Notice also that the third row needed no backslash. Inside brackets, special characters other than the backslash lose their special meaning; the Python documentation gives [(+*)] as a class that matches any of the literal characters (, +, * and ). So [^.] means "not a period" with a plain period, while outside brackets a bare period is the wildcard from the next concept and a literal period must be written \.. The two contexts have different grammars, and reading a pattern means always knowing which one you are in.

Quick check

Where must a caret sit to negate a bracket character class?

Recall

Why does [^.] need no backslash while a bare . outside brackets matches any character?

Inside brackets special characters except the backslash lose their meaning, so the period is literal and the class means "not a period". Outside brackets the period is the wildcard, so a literal one must be written \..

Counting, the wildcard, and greedy versus lazy

British and American spelling give the cleanest first example. You want one regex for color and colour. The u is optional, and colou?r says exactly that: the question mark makes the character before it appear zero or one times (SLP3, section 2.6.2). The same trick covers a plural: koalas?.

Now try sheep. Their language is baa!, baaa!, baaaa!, any number of a's of at least two. The Kleene star gives zero or more of the previous element, so baaa*! works: two literal a's, then any number more. The Kleene plus gives one or more, so baa+! says the same thing more readably. Both reject b!. Note what ba*! would do: with zero a's allowed, it accepts b!, which no sheep says. This is the recurring hazard of the star: it also matches nothing.

Quantifiers and the wildcard (slides 62 and 63, SLP3 Fig 2.11)

*
Zero or more occurrences of the previous character or expression. baa* matches ba, baa, baaa and so on.
+
One or more occurrences. baa+ requires at least one a after the first, so it rejects ba.
?
Zero or one occurrence, meaning optional. colou?r matches both color and colour.
{n}
Exactly n occurrences. a{3} matches aaa and nothing shorter or longer.
{m,n}
Between m and n occurrences, inclusive. a{2,4} accepts aa, aaa and aaaa.
{m,} and {,n}
At least m, or at most n. Omitting m sets a lower bound of zero and omitting n sets no upper bound (Python re documentation). JavaScript, and so the tester below, needs {0,n}: a bare {,n} is treated as literal text.
.
Any single character except a newline in most flavors.
.*
Any string of zero or more characters: the wildcard under the star.

A Quantifier applies to whatever is immediately before it: one character, one class, or one parenthesized group. So [0-9]+ is an integer of one or more digits, and on "Chapter 12 of 3" it finds 12 at 8 and 3 at 14. SLP3 asks why not [0-9]*, and the sheep already answered: the star matches the empty string, so [0-9]* would "match" at every position of a text with no digits at all. Reach for + whenever at least one is required.

The wildcard

The period matches any single character. In Python's default mode that is any character except a newline, and the DOTALL flag lifts the exception; in JavaScript the dot excludes the line terminators and the s flag lifts it (Python re documentation; MDN). Under a star it becomes the most powerful and most dangerous idiom in the language: .* is any string of zero or more characters. a.*b is an a, then anything, then a b, and rose.*rose finds two roses with anything between.

Greedy versus lazy

Here is the question that decides whether .* does what you meant. Run <.*> on the string <a><b>. You probably wanted the first tag. You get the whole string, because regular expressions always match the largest string they can; SLP3 says the patterns are greedy. The star grabs everything up to the end and then backs off only as far as needed for the final > to match, which is the last one. Adding a question mark after a quantifier makes it lazy: *? and +? match as little as possible, expanding only when the rest of the pattern cannot otherwise succeed (MDN, Python re documentation).

On <a><b>, the greedy <.*> sweeps across both tags in one match; the lazy <.*?> stops at the first closing bracket and then finds <b> as a second match.
PatternStringMatchesLength of first match
<.*><a><b>one match, <a><b> at 06
<.*?><a><b>two matches, <a> at 0 and <b> at 33
a.*bxaxbxbxone match, axbxb at 15
a.*?bxaxbxbxone match, axb at 13
Greedy and lazy on the same strings, computed with the JavaScript engine

Worked example

Predicting a.*b against a.*?b on xaxbxbx

  1. Find where a match can start

    The only a is at index 1, so both patterns start there.
  2. Greedy

    .* takes everything to the end, xbxbx, then gives back one character at a time until a b can match. The last b is at index 5, so the match is axbxb, indices 1 to 5.
  3. Lazy

    .*? starts by taking nothing and tries b at index 2; it is an x, so the star extends by one. Now b at index 3 matches. The match is axb, indices 1 to 3.
  4. Result

    Greedy: axbxb (5 characters). Lazy: axb (3 characters). Same start, different end.

Quick check

Which span does the lazy pattern a.*?b match first inside the string xaxbxbx?

Quick check

What does the star quantifier require of the element before it?

Recall

Predict the first match of a.*b and of a.*?b on the string xaxbxbx, with indices.

Greedy a.*b gives axbxb, indices 1 to 5. Lazy a.*?b gives axb, indices 1 to 3.

Anchors and the six aliases

Everything so far matched characters. The last two regex tools match positions. ^The matches The only at the start of a line, $ finds a trailing space at the end of one, and ^The dog\.$ matches a line that is exactly "The dog." and nothing else (SLP3, section 2.6.3).

These are anchors, and the crucial property is that they consume nothing. An anchor matches the empty string at a position that satisfies a condition, so the\b the can match "the the": the boundary sits between the e and the space without using up either. In Python, ^ matches the start of the string, and in MULTILINE mode also immediately after each newline; $ matches the end of the string or just before the final newline, and in MULTILINE mode before every newline (Python re documentation). JavaScript's m flag does the same job, which is why the tester below has one.

That is what "line-based formats" on the slide means in practice: real files are one record per line, and the multiline flag turns ^ and $ into per-line assertions. A comments filter on a corpus file, and a date extractor on a log, are the same pattern shape.

PatternText (one record per line)Matches with mMatches without m
^#.*$# comment, text line, # another2 matches: # comment and # another, each at the start of its line0
^\d{4}-\d{2}-\d{2}2026-09-16 boot, user logged in, 2026-09-17 shutdown2 matches: the timestamps opening lines 1 and 31
Line-based formats under the m flag, computed with the JavaScript engine

Without the flag, ^ sees only the start of the whole string, so the comments filter finds nothing and the date pattern catches only a first line that happens to begin with a timestamp. With the flag, each newline resets ^ and $, which is exactly how you scan a log or strip comment lines from a file. Flip the m flag on the ^The preset in the tester below to feel the difference.

AnchorPosition it assertsExample
^Start of the string, or of a line with the multiline flag^The finds The at index 0 of "The dog"
$End of the string, or of a line with the multiline flag\.$ finds the period at index 7 of "The dog."
\bBetween a word character and a non-word character, or a string edge\bthe\b finds the at index 0 of "the other theme"
\BAny position that is not a word boundary\Bthe finds the inside other
Anchors (slide 64)

The word boundary

The slide says \b is useful for matching whole words, and the canonical example is the word "the". Plain the also matches inside other and theme. \bthe\b requires a boundary on each side, so on "the other theme" it lights only the first word. A word character is a letter, a digit or an underscore, and the boundary is the seam between such a character and anything else, or a string edge (Python re documentation). Python's own examples make the rule vivid: r'\bat\b' matches at, at., (at) and the at in "as at ay", but not attempt or atlas.

Ticks mark every word boundary in the string. \bthe\b lights only the standalone the. The the inside other has letters on both sides, and the the that opens theme has a boundary on its left but an m after its e, so \bthe\b fails at both.

Digits count as word characters too, which produces the example SLP3 uses to check that you have understood the definition rather than memorized "letters". \b99\b matches in "There are 99 bottles" and in "$99", because a dollar sign is not a word character, but not in "299", because the 2 is. On "There are 299 bottles, $99 each" it finds exactly one match, the 99 at index 24.

SLP3 tells the story of the word "the" as a precision and recall exercise. the misses The at the start of a sentence, a false negative, so you write [tT]he. Now it hits other and there, false positives, so you write \b[tT]he\b. Every regex you write for a real corpus goes through the same two-sided tuning, and the boundary is usually the second fix.

The six aliases

Some classes are needed so often that they get a backslash shorthand. Slide 65 lists six, in three complementary pairs. Each lower case alias is a class, and its upper case partner is the negation of that class.

AliasExpansionMatchesFirst match in the example
\d[0-9]Any digitParty of 5
\D[^0-9]Any non-digitBlue moon
\w[a-zA-Z0-9_]Any alphanumeric or underscoreDaiyu
\W[^\w]A non-alphanumeric!!!!
\s[ \r\t\n\f]Whitespace: space, tab, newlinein Concord
\S[^\s]Non-whitespacein Concord
Aliases (slide 65, SLP3 Fig 2.13), with the first match marked

The expansions in the table are the SLP3 definitions, and they are ASCII. Real engines differ at the edges, and the differences matter for Arabic. In Python 3, a pattern on a str treats \d as any Unicode decimal digit, including Arabic-Indic digits, \w as any Unicode alphanumeric plus the underscore, so Arabic letters count, and \s as anything str.isspace() accepts, which includes the non-breaking space; the ASCII flag switches all three back to the table above, with \s as [ \t\n\r\f\v] (Python re documentation). JavaScript keeps \w at letters, digits and underscore and \d at [0-9], while its \s covers all Unicode whitespace and line terminators (MDN). So a boundary such as \b, which is defined in terms of \w, falls in different places for the same Arabic sentence in the two languages. This is the "language-dependent" warning from slide 57 in its most practical form.

A backslash works both ways

Inside a pattern, a backslash before a special character makes it literal: \. is a period, \^ is a caret. The same backslash before an ordinary letter creates an alias such as \d or \b. The full table of escaped specials, from K\*A\*P\*L\*A\*N to the raw-string question, comes in the next part.

Quick check

Which pattern matches the standalone word the but not other or theme?

Quick check

Which alias expands to the class [a-zA-Z0-9_]?

Recall

Expand \w and \s, and name their negations.

\w is [a-zA-Z0-9_], negated by \W. \s is [ \r\t\n\f], negated by \S. Python adds \v and, for str patterns, every Unicode whitespace character.

Recall

What are the three meanings of the caret, and which one does Python or JavaScript apply to a caret outside brackets?

Start-of-line anchor outside brackets, negation when first inside brackets, a literal caret elsewhere inside brackets. Outside brackets both engines always read it as an anchor, so a literal caret there must be written \^.

Recall

Write a regex for a word starting with a capital letter and say what each piece does.

\b[A-Z][a-z]*: a word boundary so the match starts a word, one upper case letter, zero or more lower case letters. \b[A-Z]\w* also accepts digits and underscores in the rest of the word.

Check every row yourself

The tester below loads every example from slides 59 to 65, the greedy and lazy pairs, and the ELIZA rule with its captured group. Load a preset, read the first match, then edit the pattern and watch the highlight move. Try a^b to see the erratum with your own eyes, and switch the m flag off on the ^The preset to see anchors change meaning.

InteractiveRegex tester: every table row from the slides, run live in your browser
HE SAYS YOU ARE DEPRESSED MUCH OF THE TIME.
  1. Match 1at index 0HE SAYS YOU ARE DEPRESSED MUCH OF THE TIME.$1 = DEPRESSED
Group 1 holds the word ELIZA copies into I AM SORRY TO HEAR YOU ARE \1.
Matches1with g
First match0index43 characters long
Groups1capturednumbered $1, $2 in JavaScript

The tester runs the browser's JavaScript RegExp engine. Its \s and \d follow the ECMAScript definitions, and . excludes the four line terminators unless s is on, so a Python result can differ by a character in edge cases. Patterns that would match the empty string, such as a*, advance one character per empty match.

Recap

If you remember nothing else

  • ELIZA is a cascade of keyword-triggered decomposition and reassembly rules: .* YOU ARE (DEPRESSED|SAD) .* becomes I AM SORRY TO HEAR YOU ARE \1. The Eliza effect names our habit of reading understanding into such echoes.
  • NLP uses regex for tokenization and pre-tokenization, normalization of dates and numbers, filtering and cleaning, and feature extraction. Regex is language- and task-dependent.
  • Literals match themselves, abc is concatenation, a|b is disjunction, parentheses set scope. [mM], [a-z] and [^0-9] each match exactly one character.
  • The caret has three faces: an anchor outside brackets, negation when first inside brackets, a literal elsewhere inside brackets. In Python and JavaScript a caret outside brackets is always an anchor, so a literal needs \^.
  • * is zero or more, + one or more, ? zero or one, {n} exactly n, {m,n} between m and n. The dot is any character except a newline, so .* is any string.
  • Quantifiers are greedy. *? and +? are lazy: <.*> eats all of <a><b>, <.*?> stops at <a> and then finds <b>.
  • ^ and $ anchor line edges, \b is a zero-width word boundary. \bthe\b matches the but not other, and \b99\b matches in $99 but not in 299.
  • \d is [0-9], \D is [^0-9], \w is [a-zA-Z0-9_], \W is [^\w], \s is [ \r\t\n\f], \S is [^\s]. Escape a literal special character with a backslash.

Sources