Track 1: What Are Regular Expressions?
The Problem: Finding Patterns in Text
Imagine you have a 10,000-line log file and you need to find every email address. Or you need to extract all dates formatted as MM/DD/YYYY, but some are written as M/D/YY. Exact string matching (like pressing Ctrl+F and typing a specific word) isn't enough because the exact text changes every time.
Regular Expressions (Regex or RegExp) are a specialized language for describing patterns rather than exact strings. Instead of searching for "john@example.com", you tell the computer: "Find a sequence of word characters, followed by an @ symbol, followed by a domain name."
# Exact match fails if the email changes
if text == "john@example.com":
pass
# Regex pattern matching finds ANY email
import re
emails = re.findall(r"[\w.-]+@[\w.-]+\.\w+", text)
*.txt) only understand "any character", regex can understand "any digit", "any capital letter", or "exactly three letters followed by a space".Where Regex Is Used
Regex is ubiquitous in software engineering and data analysis. Once you learn it, you can apply it almost everywhere:
- Command Line Tools:
grep,sed, andawkare built around regex for searching and manipulating text streams. - Programming Languages: Python (
remodule), JavaScript (RegExpobject), Java (java.util.regex), and almost every other modern language. - Text Editors & IDEs: VS Code, IntelliJ, Sublime Text, and Notepad++ all support regex in their Find/Replace features. (Look for the
.*icon in VS Code). - Databases: SQL databases support regex for flexible queries (e.g.,
SELECT * FROM users WHERE email REGEXP '^a.*@gmail\.com$'). - Data Validation: HTML5 uses regex natively for input validation (e.g.,
<input pattern="\d{5}">for a ZIP code).
# Use grep to find all lines starting with 'Error:' in a log file
grep "^Error:" /var/log/syslog
Literal Matching
The simplest regular expression is just a sequence of standard characters. For example, the regex cat will match the string "cat" anywhere it appears.
The regex engine scans the target string from left to right, looking for a match. When evaluating the string "concatenate" against the regex cat, it checks position 0 ("con" - no match), position 1 ("onc" - no match), and so on until it finds "cat" at position 3.
cat
Matches: "cat", "concatenate", "vacacation"
cat will NOT match "Cat" or "CAT". You can change this behavior using the case-insensitive flag (like /cat/i in JavaScript or re.IGNORECASE in Python).How the Regex Engine Works
Under the hood, most modern regex engines (like PCRE, Python's re, and JS RegExp) use an NFA (Nondeterministic Finite Automaton) approach. Here is what you need to know about how they execute:
- Left-to-Right Scanning: The engine starts at the first character of the input string and tries to match the entire regex pattern.
- Advancing on Failure: If the pattern fails to match starting at index 0, the engine moves to index 1 and tries again, continuing until a match is found or the string ends.
- Backtracking: When the engine reaches a point where a choice was made (like an optional character or an
ORstatement) and the subsequent match fails, it "backs up" to that choice, tries a different path, and continues. This is the source of both regex's power and its potential performance pitfalls.
Some engines (like grep or awk) use a DFA (Deterministic Finite Automaton), which doesn't backtrack and is generally faster but lacks advanced features like backreferences.
Regex Flavors
There is no single "Regex Standard". Different programming languages implement different "flavors" of regex. While the core syntax (like *, +, [], ()) is identical across all flavors, advanced features differ significantly.
- PCRE / PCRE2 (PHP, C, grep -P): The most feature-rich flavor. Supports complex lookarounds, recursion, and advanced conditional logic.
- Python (
remodule): Very similar to PCRE but traditionally lacked atomic groups and possessive quantifiers. Update: Python 3.11+ finally added support for possessive quantifiers (*+,++) and atomic groups (?>...)! Python 3.14 added the\zanchor. - JavaScript (ECMAScript): Has grown rapidly in capabilities. Modern JS supports features like the
uflag for Unicode, thevflag (Unicode Sets) for set operations in character classes, and modifier syntax like(?ims-ims:...). - POSIX (Basic/Extended): Used by classic Unix tools (like default
grep). Syntax is older and requires escaping grouping parentheses\(\)in Basic mode.
Testing Tools
Never write complex regex blindly. Use a visual tester. The industry standard is regex101.com.
Regex101 features:
- Flavor Selection: Switch between PCRE2, ECMAScript (JS), Python, Golang, Java, .NET 7.0+, Rust, and legacy PCRE.
- Explanation Panel: Breaks down your regex piece by piece in plain English.
- Match Information: Highlights matches, capture groups, and execution steps.
- Debugger: Lets you step through the regex engine's backtracking process to find performance issues.
- Code Generator: Automatically generates the wrapper code (Python, JS, etc.) for your pattern.
Other great tools include regexr.com and desktop apps like Regex Coach, but regex101 is the most comprehensive for modern flavors.
The Intimidation Factor
Look at this real-world regex for validating an email:
^(?:[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$
It looks like line noise or someone fell asleep on the keyboard. This is why regex has a reputation for being unreadable. However, regex is a dense language, not an impossible one. You don't read it like an English sentence; you read it character by character, token by token.
Once you learn the alphabet of regex, your brain will automatically parse the symbols into structural blocks: "Ah, start of line, a non-capturing group, a character class repeating..."
Reading Regex
Let's practice the step-by-step reading method on a smaller pattern:
^Error:\s+(\d+)
Deconstruction:
^: Anchor targeting the start of the line/string.Error:: Literal text matching exactly "Error:".\s+:\smeans "whitespace".+means "one or more". So, one or more spaces.( ... ): A capture group to extract the data inside it.\d+:\dmeans "digit" (0-9).+means "one or more".
Translation: "At the start of the line, find 'Error:' followed by at least one space, and capture the sequence of digits that comes next."
When NOT to Use Regex
Regex is a hammer, but not everything is a nail. Knowing when to avoid regex is just as important as knowing how to use it.
BeautifulSoup in Python or DOMParser in JS.Other times to avoid regex:
- JSON Validation: Use
JSON.parse()or schema validators. - Deeply Nested Structures: Nested parentheses in math equations or nested brackets require recursive parsers.
- Simple String Operations: If you just need to check if a string starts with "http", use
str.startswith("http"). It's faster and cleaner than^http.
The Golden Rule
Start simple, test often, and build incrementally.
Do not attempt to write a 50-character regex in one go. Write a small piece that matches the first part of your target. Test it. Add the next piece. Test it.
For complex patterns, use your language's "Verbose" or "Extended" mode to add comments and whitespace.
import re
# re.VERBOSE allows spaces and comments in the regex string
phone_regex = re.compile(r'''
^ # Start of string
(\d{3}) # Capture 3 digits (Area Code)
[-.] # Match separator (dash or dot)
(\d{3}) # Capture 3 digits (Prefix)
[-.] # Match separator
(\d{4}) # Capture 4 digits (Line Number)
$ # End of string
''', re.VERBOSE)
Track 1 Quiz
Which of the following is the BEST use case for Regular Expressions?
By default, is the regex pattern `apple` case-sensitive?
How does the regex engine generally process an input string?
Which of the following regex features was recently added in Python 3.11?
What is the recommended tool for visually testing and debugging complex regex patterns across different flavors?
Track 2: Character Classes & Basics
Literal Characters
Most characters in a regular expression match exactly themselves. These are called literal characters.
- Letters (
a-z,A-Z) match letters. - Digits (
0-9) match digits.
When you place these together, like abc, the regex engine looks for the exact sequence "a" followed by "b" followed by "c".
hello
Matches: "hello world", "Othello" (matches the "hello" inside).
The Dot (.)
The dot . is the ultimate wildcard. It matches any single character EXCEPT a newline character (\n).
c.t
Matches: "cat", "cot", "cut", "c t", "c@t", "c9t".
Does NOT match: "ct" (requires exactly one character between c and t), or "caat" (too many characters).
.* will stop at the end of the first line. To make the dot match newlines too, use the "single-line" or "dot-all" modifier (re.DOTALL in Python, or the /s flag in JavaScript).Character Classes [abc]
What if you want to match "cat" or "cot" but NOT "cut" or "c@t"? A character class [...] allows you to define a specific set of allowed characters for a single position.
c[ao]t
Matches: "cat", "cot".
Does NOT match: "cut", "caot" (the class [ao] matches exactly one character: either 'a' or 'o').
Inside a character class, most special regex characters lose their magic. For example, [.] matches a literal dot, not "any character".
Ranges [a-z] [A-Z] [0-9]
Typing out [0123456789] is tedious. You can define continuous ranges using a hyphen - inside a character class.
[a-z]matches any lowercase ASCII letter.[A-Z]matches any uppercase ASCII letter.[0-9]matches any digit from 0 to 9.
You can combine multiple ranges and literal characters in a single class without spaces:
[a-zA-Z0-9_]
This matches any letter (upper or lower case), any digit, or an underscore.
[-a-z] or [a-z-]. Otherwise, the engine thinks you're defining a range.Negated Classes [^abc]
If you place a caret ^ as the very first character inside a class, it negates the entire class. It means: "Match any single character that is NOT in this list."
[^0-9]
Matches: "a", "A", " ", "@" (any non-digit character).
Does NOT match: "5".
Example: q[^u] matches a "q" followed by any character that is NOT a "u" (like in "Iraq " - notice it matches the space after the q).
Shorthand Classes
Some character classes are so common that they have short, easy-to-type aliases starting with a backslash:
\d: Matches any digit. Equivalent to[0-9].\w: Matches any "word" character (alphanumeric plus underscore). Equivalent to[a-zA-Z0-9_].\s: Matches any whitespace character (space, tab\t, newline\n, carriage return\r, form feed, vertical tab).
\d will match Arabic numerals (e.g., Ω‘Ω’Ω£) and \w will match accented characters (e.g., Γ±, Γ©). Use the re.ASCII flag if you want strict [0-9] behavior. In JavaScript, they are ASCII-only by default unless the u flag is enabled.Negated Shorthands
The uppercase versions of the shorthands mean exactly the oppositeβthey are negated classes.
\D: Matches any non-digit. Equivalent to[^0-9].\W: Matches any non-word character (e.g., spaces, punctuation). Equivalent to[^a-zA-Z0-9_].\S: Matches any non-whitespace character.
These are incredibly useful. For instance, if you want to strip all spaces and punctuation from a phone number, you could replace all \D (non-digits) with an empty string.
Escaping Special Characters
If you actually want to match a literal dot ., an asterisk *, or a parenthesis (, you must "escape" it by placing a backslash \ in front of it.
The metacharacters that require escaping outside a character class are:
. * + ? ^ $ { } [ ] ( ) | \
google\.com
Matches: "google.com".
Without the escape (google.com), it would also match "googleXcom" or "googlercom" because the unescaped dot is the wildcard.
To match a literal backslash, you escape it with another backslash: \\.
Anchors: ^ and $
Anchors are "zero-width" assertions. They don't match any characters; instead, they match a position in the string.
^: Matches the start of the string.$: Matches the end of the string.
If you want to ensure a user input is exactly a 5-digit zip code, and nothing else, you must use anchors:
^\d{5}$
Matches: "12345".
Does NOT match: "My zip is 12345" (fails ^), or "123456" (fails $ because the string doesn't end right after 5 digits).
^ and $ match the absolute start and end of the entire string. If you enable the multiline flag (/m in JS, re.MULTILINE in Python), they match the start and end of each line within a multi-line string. If you want absolute start/end regardless of flags, use \A and \Z (or \z in Python 3.14+ for absolute end without matching trailing newlines).Word Boundaries \b
\b is another zero-width assertion. It matches the "boundary" between a word character (\w) and a non-word character (\W), or the start/end of the string.
This is crucial when you want to search for whole words only.
\bcat\b
Matches: "The cat sat".
Does NOT match: "The concatenate function", "scatter".
\B is the inverse; it matches anywhere that is NOT a word boundary. cat\B would match "cat" in "concatenate" but not "cat" by itself.
Negation vs Anchor: ^ Context
The caret symbol ^ has two completely different meanings depending on where it appears. This is a common source of confusion for beginners.
| Syntax | Meaning | Example |
|---|---|---|
Outside brackets: ^a |
Anchor (Start of string) | Must start with "a" |
Inside brackets at start: [^a] |
Negation | Any character except "a" |
Inside brackets elsewhere: [a^] |
Literal Caret | Matches "a" or "^" |
[^a] and ^a are complete opposites. The former matches a character anywhere as long as it's not 'a'. The latter matches 'a' only at position 0.
Practical: Phone Numbers, Dates, Emails
Let's combine what we've learned using character classes, shorthands, and literal characters to build practical patterns.
Phone Number: 123-456-7890
\d\d\d-\d\d\d-\d\d\d\d
(We will learn a cleaner way to write this in the next track using quantifiers!)
Date: MM/DD/YYYY
\d\d/\d\d/\d\d\d\d
Basic Email User Part (before the @)
[a-zA-Z0-9._-]+
This allows letters, digits, dots, underscores, and hyphens. (The + is a quantifier meaning "one or more", covered next!)
Track 2 Quiz
Which regex matches ANY single character except a newline?
What does the shorthand class `\W` (uppercase) match?
How do you match a literal period (dot) in a regex?
Which pattern ensures that the string contains ONLY 3 digits and nothing else?
What does the pattern `[^abc]` match?
Track 3: Quantifiers
Zero or More: *
Quantifiers tell the regex engine how many times to repeat the preceding element. The asterisk * means "match zero or more times."
ab*c
Matches: "ac" (0 b's), "abc" (1 b), "abbc" (2 b's), "abbbc" (3 b's).
Does NOT match: "bc" (needs 'a'), "ab" (needs 'c').
The * applies ONLY to the character immediately before it (the 'b' in this case). It is greedy by default, meaning it will try to match as many 'b's as possible.
One or More: +
The plus sign + means "match one or more times." Unlike *, which allows the element to be completely absent, + requires at least one occurrence to succeed.
ab+c
Matches: "abc" (1 b), "abbc" (2 b's), "abbbc" (3 b's).
Does NOT match: "ac" (0 b's - fails because it needs at least one 'b').
The + is very common. For instance, \w+ matches a whole word (one or more word characters).
Zero or One: ?
The question mark ? makes the preceding element optional. It means "match zero or one time."
colou?r
Matches: "color" (0 u's) and "colour" (1 u).
Does NOT match: "colouur" (too many u's).
Another common use case is making an 's' optional for plurals: apples? matches "apple" and "apples".
Exact Count: {n}
When you need to match an exact number of repetitions, use curly braces containing a single number.
\d{4}
Matches: "2023", "1999" (exactly 4 digits).
This is equivalent to writing \d\d\d\d but much cleaner.
Note: \w{3} matches exactly 3 word characters. If the string is "hello", \w{3} will match "hel" (the first 3 characters). Use word boundaries like \b\w{3}\b if you want to match words that are exactly 3 letters long.
Range: {n,} and {n,m}
Curly braces can also specify ranges of repetitions.
{n,m}: Matches at leastnand at mostmtimes.{n,}: Matches at leastntimes (no upper limit).
\d{2,4}
Matches 2, 3, or 4 digits. It prefers to match 4 if possible (because quantifiers are greedy by default).
Fun fact: The shorthand quantifiers are just aliases for ranges:
* = {0,}
+ = {1,}
? = {0,1}
Greedy Matching
By default, quantifiers like *, +, and {n,m} are greedy. They try to match as much of the string as possible while still allowing the overall regex to match.
Consider the string: <b>bold</b><i>italic</i>
<.*>
You might expect this to match just <b>. But because .* is greedy, it consumes the entire string to the end, then slowly backs up until it finds the last > character.
Result: It matches the entire string <b>bold</b><i>italic</i>!
Lazy (Non-Greedy) Matching
To make a quantifier lazy (or non-greedy), you append a question mark ? to it. This tells the engine to match as few characters as possible to make the overall regex succeed.
The lazy versions of the quantifiers are: *?, +?, ??, {n}?, and {n,m}?.
Using the same string: <b>bold</b><i>italic</i>
<.*?>
Now, .*? stops as soon as it sees the first > character.
Result: It matches <b> as match 1, </b> as match 2, <i> as match 3, and </i> as match 4.
Greedy vs Lazy: When It Matters
The difference between greedy and lazy is critical when extracting delimited content, such as quotes, HTML tags, or brackets.
Extracting quoted strings:
Text: He said "hello" and then said "goodbye".
- Greedy:
".*"matches"hello" and then said "goodbye"(from the first quote to the last quote). - Lazy:
".*?"matches"hello"and then"goodbye"(stops at the closing quote of each pair).
"[^"]*". This means "a quote, followed by zero or more non-quote characters, followed by a quote."Possessive Quantifiers
A third type of quantifier behavior is possessive matching. You create it by adding a plus sign + to a quantifier (e.g., *+, ++, ?+, {n,m}+).
Possessive quantifiers are like greedy quantifiers, but with one massive difference: they NEVER backtrack. They grab everything they can, and refuse to give any of it back, even if it causes the overall regex to fail.
".*+"
If you apply this to "hello", the .*+ grabs hello". Then the engine looks for the final " in the pattern, but there is no string left to match. Because it's possessive, it won't backtrack to release the last quote. The match fails.
Common Mistakes
Quantifiers are powerful, but easily misused. Watch out for these traps:
- Unnecessary
.*at the start: Writing.*error.*to find "error" anywhere in a string is usually redundant and slow. Just useerror. The engine will find it. (Exceptions exist when using full-string matching functions like Python'sre.match). - Forgetting that
*matches zero times: The pattern\d*will successfully match an empty string! If you require at least one digit, use\d+. - Overusing lazy quantifiers: While
.*?solves greediness issues, it can be slower than using a negated character class (like[^<]*) because the engine has to constantly look ahead to see if the stop condition is met.
Quantifiers on Groups
Quantifiers apply only to the single element immediately preceding them. If you want a quantifier to apply to a multi-character sequence, you must wrap the sequence in parentheses to create a group.
ha+
Matches "ha", "haa", "haaa". The + only repeats the 'a'.
(ha)+
Matches "ha", "haha", "hahaha". The + repeats the entire group (ha).
Practical: Parsing Tags, Quotes, Patterns
Let's use quantifiers to solve real problems.
Extracting simple HTML attributes (like href="..."):
href=".*?"
(A better, faster approach: href="[^"]*")
Matching an IP Address (basic validation):
\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}
This matches 4 groups of 1-3 digits separated by literal dots. (Note: This matches "999.999.999.999" which is an invalid IP, but it's often good enough for finding IPs in logs. Strict validation requires more complex logic).
Track 3 Quiz
Which quantifier requires the preceding element to appear AT LEAST once?
How do you change a greedy quantifier like `*` into a lazy (non-greedy) quantifier?
What is the result of applying `a.*b` to the string "a b c b d"?
Which regex feature, added in Python 3.11, matches greedily but NEVER backtracks?
How would you make the string match "banana" OR "bananana" OR "banananana"?
Track 4: Groups & Alternation
Grouping with Parentheses
Parentheses () in regular expressions serve multiple critical functions. Their most basic job is grouping characters together so they are treated as a single unit by the engine.
This is especially important when applying quantifiers to a sequence of characters rather than a single character.
(ha)+
Matches: "ha", "haha", "hahaha".
Compare with ha+ which matches: "ha", "haa", "haaa" (only the 'a' is repeated).
\( and \). For example, \(\d{3}\) matches an area code formatted like "(123)".Capturing Groups
By default, grouping parentheses also act as capturing groups. This means the regex engine extracts the text that matched the group and saves it for later use (either within the regex itself or in your programming language).
Groups are numbered sequentially from left to right, starting with 1. (Group 0 is always the entire matched string).
(\d{4})-(\d{2})-(\d{2})
If applied to the string "2023-10-25":
- Group 0 (Full Match): 2023-10-25
- Group 1: 2023
- Group 2: 10
- Group 3: 25
In Python, you would access these using match.group(1), match.group(2), etc.
Backreferences: \1, \2
You can refer to the text captured by a group within the same regular expression using a backreference.
A backreference is a backslash followed by the group number: \1, \2, etc.
\b(\w+)\s+\1\b
This is a classic pattern for finding doubled words (like "the the" or "is is").
\b(\w+)captures a whole word into Group 1.\s+matches the space(s) between them.\1says "match exactly the same text that Group 1 just captured".
Matches: "the the", "is is".
Does NOT match: "the dog".
Non-Capturing Groups (?:...)
Sometimes you need parentheses just to group characters for a quantifier or alternation, but you don't want to save the result. Storing captured text takes memory and can mess up your group numbering.
To create a non-capturing group, start the group with ?: (a question mark and a colon).
(?:https?|ftp)://([^/\r\n]+)
In this URL parser:
- The protocol (http, https, ftp) is matched inside a non-capturing group
(?:...). It is NOT assigned a group number. - The domain name is captured in Group 1
([^/\r\n]+).
Best Practice: Always use non-capturing groups unless you explicitly need to extract the data or use a backreference.
Alternation: |
The pipe character | acts as a boolean OR operator. It allows you to match one sequence or another.
cat|dog|bird
Matches "cat", "dog", or "bird".
cat|caterpillar and search the text "caterpillar", it will only match "cat" because "cat" succeeded first! Always put the longer, more specific alternative first: caterpillar|cat.Alternation Scope
A common mistake is failing to control the scope of the alternation operator. The | operator has the lowest precedence of all regex operators, meaning it splits the entire regex in half unless constrained by parentheses.
Incorrect:
I love cat|dog
This matches "I love cat" OR "dog".
Correct (using a group):
I love (?:cat|dog)
This matches "I love cat" OR "I love dog". (We used a non-capturing group to be efficient).
Named Groups
Relying on numbered groups (1, 2, 3...) becomes fragile when you modify a complex regex. Adding a new group shifts all the numbers down the line.
Named Capture Groups allow you to assign an explicit variable name to a group. The syntax varies slightly by flavor.
Python Syntax: (?P<name>...)
import re
m = re.match(r"(?P\d{4})-(?P\d{2})", "2023-10")
print(m.group("year")) # Output: 2023
JavaScript & PCRE Syntax: (?<name>...)
const match = "2023-10".match(/(?\d{4})-(?\d{2})/);
console.log(match.groups.year); // Output: "2023"
To backreference a named group within the same regex: Python uses (?P=name), JS/PCRE use \k<name>.
Practical Extraction
Combining groups, character classes, and quantifiers allows you to extract precise data fields from structured text.
Extracting Username and Domain from an Email:
^([a-zA-Z0-9._-]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$
Group 1 contains the username, Group 2 contains the domain.
Parsing a simple URL:
^(https?)://([^/:]+)(:\d+)?(/.*)?$
- Group 1: Protocol (http or https)
- Group 2: Domain name
- Group 3: Port (optional, e.g., :8080)
- Group 4: Path (optional, e.g., /index.html)
Nested Groups
Capture groups can be nested inside one another. The rule for numbering nested groups is simple: Count the opening parentheses from left to right.
((a)(b(c)))
If applied to the string "abc":
- Group 1:
((a)(b(c)))matches "abc" (Outer parenthesis) - Group 2:
(a)matches "a" - Group 3:
(b(c))matches "bc" - Group 4:
(c)matches "c"
Remembering to count the opening parenthesis makes it foolproof.
Groups in Replacement
One of the most powerful uses of capturing groups is transforming text via "Search and Replace". You can reference captured groups in your replacement string.
Syntax varies by environment:
- Python (
re.sub): Use\1or\g<name>. - JavaScript (
String.replace): Use$1or$<name>. - sed / vim / VS Code: Often
\1or$1.
Example: Reformatting Dates
Convert MM/DD/YYYY to YYYY-MM-DD
Search Regex: (\d{2})/(\d{2})/(\d{4})
Replacement (JS/VS Code): $3-$1-$2
Conditional Patterns
Advanced regex engines (like PCRE and Python, but NOT JavaScript) support if/then conditional statements inside the regex itself.
The syntax is: (?(condition)yes-pattern|no-pattern)
The most common condition is checking if a previous capture group matched. E.g., (?(1)...|...) means "If Group 1 matched, use the yes-pattern; otherwise use the no-pattern."
Example: Validating a phone number with optional parentheses.
We want to match (123) 456-7890 or 123-456-7890, but NOT (123-456-7890 (mismatched parens).
^(\()?(\d{3})(?(1)\)|-)\d{3}-\d{4}$
This says: Capture an optional opening parenthesis into Group 1. Then match 3 digits. Then, if Group 1 matched, require a closing parenthesis. Otherwise, require a hyphen.
Practical: Logs and Formatting
Swapping First and Last Names:
Input: Smith, John
Search Regex: ^([A-Za-z]+),\s*([A-Za-z]+)$
Replacement: $2 $1
Output: John Smith
Parsing an Apache Log Line:
Input: 127.0.0.1 - - [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326
^(\S+) \S+ \S+ \[([^\]]+)\] "([A-Z]+) ([^"]+) HTTP/\d\.\d" (\d{3}) (\d+)$
This massive regex gracefully plucks out the IP (Group 1), Timestamp (Group 2), Method (Group 3), Path (Group 4), Status Code (Group 5), and Bytes (Group 6). Notice the heavy use of negated character classes like [^\]]+ and [^"]+ for efficiency!
Track 4 Quiz
How does the regex engine determine the group number for a capturing group?
What is the syntax to create a non-capturing group?
If you apply the regex `(\w+)\s+\1` to the text "hello hello world", what does `\1` do?
Why might the pattern `January|Jan` fail to match "January" in some standard regex engines?
Which of the following creates a named capture group called "id" in Python?
Track 5: Lookahead & Lookbehind
What Are Zero-Width Assertions?
Zero-width assertions are unique elements in regular expressions that match a position rather than the characters themselves. Unlike standard tokens, they "look" at the surrounding text without "consuming" it.
You already know two zero-width assertions: ^ (start of line) and $ (end of line). Lookarounds (lookahead and lookbehind) extend this concept, allowing you to define custom conditions that must be met before or after your actual match.
# Standard matching consumes characters:
q[^u] # Matches "q" followed by anything other than "u", consuming 2 characters
# Lookahead asserts without consuming:
q(?=[^u]) # Matches "q" only if followed by non-"u", consuming 1 character
Positive Lookahead (?=...)
A positive lookahead is written as (?=...). It translates to: "Ensure that the following characters match this pattern, but don't include them in the overall match."
This is extremely useful when you want to extract a value that is followed by a specific unit or keyword, but you only want the value itself.
\d+(?= dollars)
In the text 100 dollars, the pattern matches 100. It checks for " dollars" immediately after the digits, but " dollars" is not part of the returned match. In 100 cats, it fails entirely.
Negative Lookahead (?!...)
A negative lookahead is written as (?!...). It translates to: "Ensure that the following characters do NOT match this pattern."
This provides an elegant way to implement exclusions in regex, which is historically difficult because regex is designed to match, not to exclude.
\d+(?! dollars)
In the text 100 cats, this matches 100. In the text 100 dollars, the match fails because the negative lookahead condition is violated.
.*(?!foo) will often still match a string containing "foo" by backtracking and stopping one character earlier. Usually, you want to anchor or constrain the preceding pattern.Positive Lookbehind (?<=...)
A positive lookbehind is written as (?<=...). It translates to: "Ensure that the preceding characters match this pattern."
It looks backwards from the current position. This is ideal for extracting values that follow a specific prefix, like a currency symbol.
(?<=\$)\d+
In the text $50, this matches 50. The $ is required to be right before the digits, but it is not included in the matched text. In 50 units, it fails.
\$. Unescaped, $ means "end of line", which would make the lookbehind impossible in most contexts.Negative Lookbehind (?<!...)
A negative lookbehind is written as (?<!...). It translates to: "Ensure that the preceding characters do NOT match this pattern."
Use this when you want to match something only if it isn't preceded by a specific marker.
(?<!\$)\b\d+\b
This matches 50 in 50 units, but ignores the 50 in $50. We use word boundaries \b to ensure we don't accidentally match the 0 in $50 (since 0 is preceded by 5, not $!).
Why "Zero-Width"?
To truly master lookarounds, you must understand the concept of "zero-width". When a lookaround evaluates, the matching engine's cursor stays in the exact same spot.
Because the cursor doesn't move, you can chain multiple lookarounds together, and they will all evaluate from the same starting position.
(?=[A-Z])(?=.*[0-9])
Here, the engine checks if the current position is followed immediately by an uppercase letter. Then, from that exact same position, it checks if there's a digit somewhere ahead. It essentially acts as a logical AND.
Password Validation Pattern
One of the most famous applications of stacked lookaheads is password validation. Regex fundamentally reads left-to-right, making it hard to enforce "must contain A, B, and C in any order." Zero-width lookaheads solve this.
^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$
Let's break it down:
^- Start at the beginning of the string.(?=.*\d)- Look ahead: is there a digit somewhere?(?=.*[a-z])- Look ahead from the start: is there a lowercase letter?(?=.*[A-Z])- Look ahead from the start: is there an uppercase letter?.{8,}- If all assertions pass, actually consume 8 or more characters.$- Ensure we've reached the end of the string.
Flavor Differences
While lookaheads are universally supported, lookbehinds are a notorious source of cross-platform incompatibility, specifically regarding variable-length lookbehinds.
- Fixed-length only: Some older engines only allow lookbehinds with a fixed length.
(?<=abc)is fine, but(?<=a+)or(?<=a|ab)will throw an error. - JavaScript: Now fully supports variable-length lookbehinds (ECMAScript 2018+).
- Python: Historically restricted, but Python's
remodule supports variable-length lookbehind. PCRE2 also supports it. - regex101.com: When testing, make sure you set the correct flavor. regex101 supports PCRE2, ECMAScript, Python, Golang, Java, .NET 7.0, and Rust. Note that Rust's standard regex engine does NOT support lookarounds at all for performance reasons!
Combining Lookahead and Lookbehind
You can use both lookaround types simultaneously to extract text nestled between delimiters, without capturing the delimiters themselves.
(?<=\[).*?(?=\])
This matches everything inside square brackets [like this], returning just like this.
Another classic example is formatting numbers by adding commas:
(?<=\d)(?=(\d{3})+$)
This matches a position that is preceded by a digit, and followed by multiples of 3 digits until the end of the string. You can replace this zero-width position with a comma!
Performance and Alternatives
While lookarounds are magical, they can introduce severe performance penalties if misused. An unanchored lookaround with complex quantifiers forces the engine to test the condition at every single character position.
When to use Capture Groups instead:
If your environment supports capture group extraction (like Python's re.search().group(1)), it is often more efficient and readable to use groups rather than lookarounds.
# Lookaround approach:
(?<=Order: )\d+
# Capture group approach (often faster and simpler):
Order: (\d+)
Track 5 Quiz
What does "zero-width" mean in the context of lookarounds?
Which regex uses a negative lookahead to match a word NOT followed by "ing"?
In the password pattern ^(?=.*\d)(?=.*[A-Z]).{8,}$, why do we use multiple lookaheads from the start ^?
Does JavaScript support variable-length lookbehinds?
Track 6: Regex in Programming Languages
Python re Module Basics
Python's built-in re module provides powerful regex capabilities. The most common functions are:
re.search(pattern, string)- Scans the string and returns the first match.re.match(pattern, string)- Warning: Only checks for a match at the start of the string!re.fullmatch(pattern, string)- Requires the entire string to match the pattern.re.findall(pattern, string)- Returns a list of all non-overlapping matches as strings.re.finditer(pattern, string)- Returns an iterator yielding Match objects (better for large texts).
import re
match = re.search(r'(\d+)-(\w+)', 'ID: 123-abc')
if match:
print(match.group()) # '123-abc' (full match)
print(match.group(1)) # '123' (first capture group)
print(match.groups()) # ('123', 'abc') (all capture groups)
print(match.span()) # (4, 11) (start and end indices)
Python re.sub() and re.split()
Modifying text is just as important as finding it. re.sub() performs replacements, and re.split() splits strings using a regex delimiter.
import re
text = "Last, First"
# Substitution with backreferences
# \2 refers to group 2, \1 refers to group 1
swapped = re.sub(r'(\w+),\s*(\w+)', r'\2 \1', text)
print(swapped) # "First Last"
# Named group replacement syntax: \g
swapped_named = re.sub(r'(?P\w+),\s*(?P\w+)', r'\g \g', text)
# Splitting on multiple delimiters
words = re.split(r'[,;\s]+', 'apple, banana; cherry date')
# ['apple', 'banana', 'cherry', 'date']
Python re.compile() and re.VERBOSE
If you use a regex multiple times inside a loop, pre-compiling it with re.compile() can slightly improve performance. More importantly, it allows you to use flags like re.VERBOSE (or re.X).
re.VERBOSE allows you to write multi-line regexes with comments, ignoring unescaped whitespace. This is crucial for maintaining complex patterns.
import re
phone_regex = re.compile(r"""
(\d{3}) # Area code
[-.\s]? # Optional separator
(\d{3}) # Prefix
[-.\s]? # Optional separator
(\d{4}) # Line number
""", re.VERBOSE)
match = phone_regex.search("Call 555-123-4567")
Python Raw Strings
In Python, the backslash \ is an escape character for standard strings (e.g., \n for newline). However, regex also uses backslashes extensively (e.g., \d for digit).
To avoid a clashβknown as "backslash plague"βalways use Python raw strings by prefixing your string with r.
# Bad: Standard string
# Python tries to evaluate \b as backspace!
pattern = "\\bword\\b"
# Good: Raw string
# Python leaves \b alone, passing it safely to the regex engine
pattern = r"\bword\b"
r"\" is an invalid Python string!JavaScript RegExp Basics
JavaScript supports regex as first-class citizens. You can create them via literal syntax /pattern/flags or the constructor new RegExp("pattern", "flags").
Recent additions to JavaScript (stage 3 proposal, landing in engines) include RegExp.escape() to safely escape user input before using it in a RegExp.
const regex = /(\d+)-(\w+)/;
const str = "ID: 123-abc";
// RegExp methods
console.log(regex.test(str)); // true
console.log(regex.exec(str)); // ["123-abc", "123", "abc", index, input]
// String methods
console.log(str.match(regex));
console.log(Array.from(str.matchAll(/(\w+)/g))); // Returns iterator of matches
console.log(str.replace(/(\w+)/g, "X")); // "ID: X-X"
console.log(str.search(/123/)); // Returns index 4
JavaScript Flags Deep Dive
JavaScript regular expressions have powerful flags appended after the closing slash:
g(global): Match all occurrences, not just the first.i(ignoreCase): Case-insensitive match.m(multiline):^and$match start/end of lines, not just string.s(dotAll):.matches newlines.u(unicode): Full Unicode support. Treats surrogate pairs as single characters.v(unicodeSets): Advanced Unicode features, set operations like intersection/difference in character classes.d(hasIndices): Output array includes.indicesmapping start/end positions of capture groups.y(sticky): Match exactly atlastIndexposition.
Modern JS also supports inline flag modifiers: (?ims-ims:...) allows turning flags on/off locally inside the pattern!
grep and ripgrep
In the terminal, grep is the standard tool for regex search. ripgrep (rg) is a modern, dramatically faster alternative designed for massive codebases.
# Basic grep (Basic Regular Expressions)
grep "error" /var/log/syslog
# grep -E (Extended Regex - standard modern regex)
grep -E "error|warning" /var/log/syslog
# grep -P (PCRE - Perl Compatible Regex, supports lookarounds!)
grep -P "(?<=User: )\w+" access.log
# ripgrep (rg) - extremely fast, searches recursively by default
rg -i "TODO:.*" src/
Common flags: -i (case-insensitive), -v (invert match), -c (count matches), -n (line numbers).
sed
sed (stream editor) is a terminal utility for parsing and transforming text. It excels at bulk replacements.
# sed 's/pattern/replacement/flags'
# The 'g' flag replaces all occurrences on a line
sed 's/foo/bar/g' input.txt > output.txt
# Capture groups use \1, \2 (Note: sed uses Basic Regex by default, so escape parens)
sed 's/\([A-Z]\)/\1_/g' input.txt
# Use -E for Extended Regex syntax (no need to escape parens)
sed -E 's/([A-Z])/\1_/g' input.txt
sed 's|http://|https://|g'.VS Code Find & Replace
Modern editors like VS Code have robust regex support built into Find & Replace (toggle with Alt+R or the .* icon).
- Use
$1,$2for capture group replacements. - Use
$<name>for named capture groups. - Case transforms: You can change the case of captured text during replacement!
Find: const (\w+) =
Replace: let \L$1 = # \L lowercases the capture group
# Other transforms:
# \U - Uppercase the rest of the group
# \u - Uppercase the first character
# \l - Lowercase the first character
SQL, Unicode, and Performance
SQL: While SQL uses LIKE '%pattern%' for simple wildcards, most modern databases support REGEXP or RLIKE for full regex matching.
Unicode: To match across multiple languages, standard \w (which usually matches ASCII [a-zA-Z0-9_]) isn't enough. Use Unicode properties like \p{L} (any letter in any language) or \p{Script=Han} (CJK characters). This requires proper flags (like JS u flag or Python's third-party regex module).
Python 3.13+ Updates: Python 3.13 introduced re.PatternError to replace the old re.error. Python 3.14 introduced the \z anchor for end-of-string matching.
Track 6 Quiz
In Python, what is the difference between re.match() and re.search()?
Why do we prefix Python regex strings with 'r' (e.g., r"\d+")?
In JavaScript, what does the 'v' flag do?
How do you reference capture group 1 in a sed substitution (e.g., sed 's/pat/rep/')?
Track 7: Real-World Patterns
Email Validation
Email validation is the most debated regex topic. The official RFC 5322 specification allows for incredibly complex emails, including comments and quotes. Writing a 100% compliant regex is famously a multi-page monstrosity.
For 99% of applications, you should use a simplified validation and let the mail server handle actual verification (by sending a confirmation link).
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
user+tag@example.museum because they forget about the + symbol or restrict TLDs to 3 characters.URL Matching
Extracting URLs from text requires parsing multiple optional components: protocol, domain, port, path, query parameters, and fragment.
^(https?:\/\/)?([\w.-]+)(:\d+)?(\/[^\s]*)?(\?[^\s]*)?(#[^\s]*)?$
Let's break this down:
(https?:\/\/)?- Optionalhttp://orhttps://.([\w.-]+)- The domain name.(:\d+)?- Optional port number like:8080.(\/[^\s]*)?- Optional path, grabbing everything until a whitespace.(\?[^\s]*)?- Optional query string.(#[^\s]*)?- Optional fragment identifier.
IP Address Validation
Validating an IPv4 address exposes a limitation of regex: it doesn't understand math. An IP octet must be between 0 and 255. A naive regex like \d{1,3}\.\d{1,3}... will incorrectly match 999.999.999.999.
To restrict numbers to 0-255, we must map out the digit possibilities:
(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)
Here, 25[0-5] handles 250-255, 2[0-4]\d handles 200-249, and [01]?\d\d? handles 0-199.
::1).Date and Time Patterns
Matching ISO 8601 timestamps (the standard format) is straightforward:
^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z?$
However, just like IP addresses, regex is bad at calendar logic. While you can write a pattern for YYYY-MM-DD, preventing February 31st or leap year bugs solely with regex is an exercise in futility. Extract the values with regex, then use a programming language's Date parsing library for actual validation.
Log Parsing
Server logs, like Apache or Nginx access logs, are perfect candidates for regex parsing. A common log line looks like:
127.0.0.1 - - [10/Oct/2023:13:55:36 -0700] "GET /index.html HTTP/1.1" 200 2326
^(\S+) \S+ \S+ \[([^\]]+)\] "(.*?)" (\d{3}) (\d+|-)
We use \S+ (non-whitespace) to grab the IP, and lazy quantifiers .*? inside quotes to extract the HTTP request.
CSV with Quoted Fields
Splitting a CSV line by commas is easy. Splitting it by commas unless the comma is inside quotes is a classic regex puzzle.
(?:^|,)("(?:[^"]*(?:""[^"]*)*)"|[^,]*)
This pattern matches either a quoted field (handling double quotes "" as escaped quotes) or an unquoted field. While impressive, in production code, always use a dedicated CSV parser library instead of writing this by hand.
Markdown Patterns
Regex is commonly used to build lightweight Markdown parsers or syntax highlighters.
# Headings (H1-H6)
^#{1,6}\s+.+$
# Links: [Text](URL)
\[([^\]]+)\]\(([^)]+)\)
# Bold text: **text**
\*\*([^*]+)\*\*
Note: Full Markdown parsing requires state machines, as regex alone cannot easily handle nested elements (like bold text inside a link).
Version Numbers (Semver)
Semantic Versioning (Semver) follows the MAJOR.MINOR.PATCH format, with optional pre-release tags and build metadata (e.g., 1.0.0-alpha+001).
^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
This intimidating, official pattern ensures that no leading zeros are used in numeric parts, and properly separates the pre-release hyphen and build metadata plus sign.
File Paths
Validating and extracting components from file paths differs drastically by OS.
# Unix/Linux absolute path
^(\/[^/]+)+\/?$
# Extracting filename and extension
^.*\/([^/]+)\.([^/.]+)$
# Windows path (C:\Folder\file.txt)
^[a-zA-Z]:\\(?:[^\\/:*?"<>|\r\n]+\\)*[^\\/:*?"<>|\r\n]*$
Phone Numbers
Phone numbers are notoriously messy. Formatting varies wildly globally. The best approach is a loose regex that allows digits, spaces, hyphens, dots, and a leading plus.
# Loose International
^\+?\d{1,3}[-.\s]?\(?\d{1,4}\)?[-.\s]?\d{1,4}[-.\s]?\d{1,9}$
# Strict US Format (e.g., (555) 123-4567)
^\(?([0-9]{3})\)?[-.\s]?([0-9]{3})[-.\s]?([0-9]{4})$
Code Patterns
Regex is a staple for codebase analysis. You can quickly map out project architecture by finding function definitions or extracting comments.
# Python function definitions
^\s*def\s+([a-zA-Z_]\w*)\s*\(
# JavaScript functions/arrow functions
(?:function\s+([a-zA-Z_]\w*)|([a-zA-Z_]\w*)\s*=\s*(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>)
# Finding technical debt
\/\/\s*(TODO|FIXME):.*$
Cleanup Patterns
Finally, regex is unmatched for text sanitization and cleanup tasks.
# Strip trailing whitespace from line ends
[ \t]+$ -> Replace with empty string
# Normalize Windows line endings to Unix
\r\n -> Replace with \n
# Collapse multiple blank lines into one
\n{3,} -> Replace with \n\n
# Collapse multiple spaces into a single space
[ \t]{2,} -> Replace with " "
Track 7 Quiz
Why is validating dates (like leap years) purely with regex a bad idea?
In the URL pattern `(https?:\/\/)?`, what does the `?` do?
Which regex correctly strips trailing spaces from the end of a line?
When parsing CSV lines, why is splitting merely on commas (,) insufficient?
For IPv4 validation, why must we write out ranges like `2[0-4]\d`?
Track 8: Advanced Patterns & Mastery
Atomic Groups (?>...)
Atomic groups lock in a match. Once the regex engine successfully matches the content inside an atomic group (?>...), it discards all backtracking positions for that group. It will never try other permutations inside it, even if the overall match fails later.
Supported in PCRE, Python 3.11+, .NET, and Java. Not supported in JavaScript.
# Standard group:
(a|ab)c # Matches "ac" and "abc". Backtracks if 'a' fails to match 'c'.
# Atomic group:
(?>a|ab)c # Matches "ac", but FAILS on "abc"!
Why does it fail on "abc"? It tests a, succeeds, locks it in. Then it looks for c, sees b, and fails. It refuses to backtrack and try the ab option.
Possessive Quantifiers *+ ++ ?+
Possessive quantifiers act like greedy quantifiers, but with atomic properties: they grab as much as possible and refuse to give any back.
Supported in Python 3.11+, PCRE, Java. Not supported in JavaScript.
# Greedy (backtracks to succeed)
".*" matches "foo"
# Possessive (consumes quotes, fails)
".*+" fails on "foo"
In the possessive example, .*+ eagerly consumes the rest of the string, including the final quote. When the engine looks for the trailing quote token, it's gone. The possessive quantifier refuses to give it back, causing the match to fail.
Recursive Patterns (?R)
Can regex match indefinitely nested brackets? Generally, no, because regex engines are built on Finite State Automata. However, PCRE introduced Recursion, turning regex into Context-Free Grammars!
Supported in PCRE and Python's third-party regex module. Not in standard Python re or JavaScript.
\((?:[^()]*|(?R))*\)
This matches an open parenthesis, followed by either non-parenthesis characters OR the entire regex pattern itself recursively (?R), followed by a closing parenthesis. It can match (a(b(c)d)e) perfectly.
Conditional Patterns
Conditionals allow regex to say "If X happened earlier, match Y; otherwise, match Z." Syntax: (?(condition)yes-pattern|no-pattern).
Supported in PCRE and Python re.
(<)?\w+(?(1)>|)
This pattern optionally matches an opening bracket (<)? as Group 1. Later, (?(1)>|) says: If Group 1 exists, match a closing bracket >. Else, match nothing. It ensures balanced optional delimiters!
Branch Reset Groups (?|...)
Normally, each open parenthesis increments the capture group number. In a branch reset group, alternatives share the same group numbers.
Supported in PCRE and Python's regex module. Not standard Python re or JavaScript.
# Standard: (Group 1) or (Group 2)
(foo)|(bar)
# Branch Reset: Group 1 is either "foo" or "bar"
(?|(foo)|(bar))
Unicode Categories
Modern text is Unicode. [a-zA-Z] fails on names like "RenΓ©" or Arabic scripts. Unicode property escapes solve this via \p{Property}.
\p{L}- Any letter in any language.\p{Lu}- Uppercase letter.\p{Ll}- Lowercase letter.\p{Nd}- Decimal digit.\p{P}- Punctuation.\p{Emoji}- Emojis.
Requires the u or v flag in JS. Supported in PCRE. Standard Python re supports \w for unicode letters but lacks the fine-grained \p{} syntax (use the regex module instead).
Catastrophic Backtracking
Catastrophic backtracking happens when a regex engine creates an exponentially massive number of execution paths trying to find a match, usually causing the program to freeze or crash.
It is triggered by nested quantifiers, like (a+)+, evaluating a string like "aaaaaaaaaaaaaaaaaaaaab".
a+ or the outer +. The engine tries every single combination before realizing 'b' makes the match impossible. 20 'a's require over a million backtracking steps!ReDoS: Regular Expression Denial of Service
When a server evaluates user input using a vulnerable regex, an attacker can send a crafted string (like the "aaaa...b" example) to trigger catastrophic backtracking, locking up the CPU.
Famous incidents include major outages at Cloudflare and Stack Overflow. To protect your apps:
- Limit input length strictly.
- Avoid overlapping alternatives like
(a|a)+or nested quantifiers(.*)*. - Use static analysis tools like ESLint plugins to detect ReDoS vectors.
- Use timeout settings if your engine supports them, or use non-backtracking engines like Google's RE2 (or Rust's standard regex).
Optimization Techniques
Write high-performance regex by guiding the engine:
- Anchor early: Using
^or$drastically reduces the locations the engine tests. - Atomic groups / Possessive quantifiers: Prevent unnecessary backtracking.
- Character class over alternation:
[abc]is much faster thana|b|c. - Be specific: Instead of
.*", use[^"]*". It prevents the engine from scanning to the end of the string and backtracking.
Debugging Regex
When regex breaks, don't guess. Use tools.
- regex101.com: Features a built-in step-by-step debugger. You can view the exact backtracking trace and see exactly how many steps your regex took.
- Python's re.DEBUG: Compile with
re.compile(pattern, re.DEBUG)to print the internal bytecode tree of how Python parses your pattern. - Divide and Conquer: Remove parts of the regex until it works, then add pieces back one by one.
When to Abandon Regex
Regex is a tool, not a religion. You should abandon regex when:
- Parsing nested structures: HTML, XML, JSON. Use dedicated parsers (like BeautifulSoup or
JSON.parse). A famous Stack Overflow answer curses the very idea of parsing HTML with regex. - Maintainability: If your pattern is over 100 characters long, lacks comments, and takes 10 minutes to understand, rewrite it in standard code logic. Code is read far more often than it is written.
Regex Puzzles
To achieve true mastery, practice! Try solving these challenges:
- Match a palindrome of any length (requires backreferences or recursion).
- Validate a mathematical expression with balanced parentheses.
- Play "Regex Golf" (finding the shortest possible regex to match/unmatch lists of words).
- Solve puzzles on sites like Regex Crossword.
Congratulations! You have tamed the Pattern-Matching Beast.
Track 8 Quiz
What happens when an atomic group successfully matches?
Which of the following causes catastrophic backtracking?
Which syntax represents a recursive pattern in PCRE?
Which property escape matches any uppercase letter in Unicode?
What is the best way to parse arbitrary HTML in production code?
βEvery quest begins with a single line of code.β
Β© Built with β€οΈ by C.W.K. (Creative Works of Knowledge) & Pippa