Regex: Character Classes, Groups and Lookarounds Reference Guide
By DevShelfHub
Character classes, anchors, quantifiers, groups, lookarounds — with examples that actually parse the things you need.
65 items
◷ 5 min
Patterns
Groups
Lookarounds
Start hereQuick start · 6 you’ll reach for daily
Word\bword\b
Digits\d+
Whitespace\s+
Any char.
Either / orfoo|bar
Named group(?P<name>…)
flavors coveredVersions
Flavors:PCRE / Python reJavaScriptPOSIX ERE
Most syntax in this sheet works in PCRE, Python re, and modern
JavaScript (ES2018+). Where they differ — named groups, lookbehind, possessive quantifiers — the row
flags it. POSIX BRE (basic grep, default
sed) is older; use grep -E
or sed -E to get ERE.
positions, not charactersAnchors
^
Start of string (or line with m flag).
$
End of string (or line with m flag).
\A
Start of string, always. Ignores m.
\Z
End of string, always. Ignores m.
\b
Word boundary. Between \w and non-\w.
\B
Non-boundary. Inside a word.
Anchors match a position, not a character. They consume zero width — useful inside lookarounds
and at split points.
match a setCharacter classes
.
Any char except newline. s flag makes it match \n too.
[abc]
Any of a, b, c.
[^abc]
Anything except those.
[a-z]
Range. Mix: [A-Za-z0-9_].
\d / \D
Digit / non-digit.
\w / \W
Word char ([A-Za-z0-9_]) / non.
\s / \S
Whitespace / non.
\p{L} / \p{N} / \p{Sc}
Unicode property: Letter / Number / Currency. PCRE + JS with u + Python regex module.
[[:alpha:]] / [[:digit:]]
POSIX classes. Work inside [].
\t \n \r \f \v
Tab / LF / CR / form-feed / vertical tab.
how manyQuantifiers
?
0 or 1.
*
0 or more.
+
1 or more.
{n}
Exactly N.
{n,}
N or more.
{n,m}
Between N and M, inclusive.
*? +? ?? {n,m}?
Preferred Lazy / non-greedy. Match as little as possible.
*+ ++ ?+ {n,m}+
Possessive. No backtracking. PCRE / Java. Massive perf win on big inputs.
Default quantifiers are greedy.<.*> on
<a><b> matches the whole thing. Use lazy
<.*?> when you want the smallest span.
capture & referenceGroups
(abc)
Capturing group. Numbered \1 from left.
(?:abc)
Non-capturing group. Use for grouping without polluting captures.
(?P<name>abc)
Preferred Named capture (Python).
(?<name>abc)
Named capture (PCRE, JS, .NET).
(?P=name) / \k<name>
Backreference to a named group.
(?i:abc) / (?m:abc) / (?s:abc)
Inline flags scoped to a group.
(?>abc)
Atomic group. Like possessive: refuses to backtrack.
\1 \2 \3 …
Backreference in the pattern (e.g. detect duplicate words: \b(\w+) \1\b).
$1 $2 (or \1 \2)
Reference in the replacement. Syntax depends on engine.
match position, not textLookarounds
(?=abc)
Positive lookahead. Followed by abc.
(?!abc)
Negative lookahead.
(?<=abc)
Positive lookbehind. Preceded by abc.
(?<!abc)
Negative lookbehind.
\d+(?=px)
Numbers followed by px. px isn’t consumed.
(?<=\$)\d+
Digits after a literal $.
JavaScript shipped variable-width lookbehind in ES2018; Python’s re still requires
fixed width. The third-party regex module on Python lifts that limit.
refer to a matchBackreferences
(\w+)\s+\1
Duplicate word: the the.
^(.+)\n\1$
Two identical lines (with s or two lines worth of context).
<(\w+)>.*?</\1>
Naive XML pair. Don’t parse XML with regex in earnest; for grep, fine.
Global. JS only — iterates all matches. Python findall is always global.
u
Unicode-aware. JS: enable \p{} + correct . width.
y
Sticky (JS). Anchors at lastIndex.
(?i) / (?m) / (?s)
Inline flag for the rest of the pattern.
flavor differencesEngines & usage
Python
python
import re
# Find / match
re.search(r"\bfoo\b", text) # first match, anywhere
re.match (r"foo", text) # only at the start
re.fullmatch(r"\d{4}", "2026") # the whole string must match
re.findall(r"\d+", text) # list of all non-overlapping
list(re.finditer(r"\d+", text)) # list of Match objects (with .span())
# Replace
re.sub(r"\s+", " ", text) # collapse whitespace
re.sub(r"(\w+)@(\w+)", r"\1 at \2", t) # backrefs in replacement
# Groups
m = re.search(r"(?P\d{3})-(?P\d{4})", "415-1234")
m.group("area"), m["num"], m.groupdict()
# Compile + flags for hot loops
pat = re.compile(r"^error:", re.MULTILINE | re.IGNORECASE)
for line in pat.finditer(log):
...
JavaScript
javascript
// Test / match
/foo/.test("foobar") // boolean
"foobar".match(/o+/g) // ["oo"] (all matches with /g)
"foobar".search(/b/) // index of first match
// Iterate with capture groups
const re = /(\d{4})-(\d{2})-(\d{2})/g;
for (const m of "2026-05-17 and 2026-01-02".matchAll(re)) {
const [, y, mo, d] = m;
console.log(y, mo, d);
}
// Named groups (ES2018+)
const pat = /(?\d{3})-(?\d{4})/;
const { groups } = "415-1234".match(pat);
groups.area; // "415"
// Replace with a function
"x = 1 + 2".replaceAll(/\d+/g, n => String(Number(n) * 10));
// Common flags
// g global i ignore case m multiline (^/$)
// s dotAll u unicode y sticky
copy-paste recipesCommon patterns
bash
# Email (good-enough, not RFC-perfect)
[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}
# IPv4 (with bounds — each octet 0–255)
\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b
# URL (http/https, no creds, optional path/query)
https?://[^\s/$.?#].[^\s]*
# ISO 8601 date (yyyy-mm-dd)
\b\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])\b
# UUID v4
\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\b
# Semver
\bv?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[\w.\-]+)?(?:\+[\w.\-]+)?\b
# Markdown link → [text](url)
\[([^\]]+)\]\(([^)]+)\)
# Trim trailing whitespace on every line
[ \t]+$ → "" (multiline)
^\s*$
Empty / whitespace-only line.
^\s+|\s+$
Leading / trailing whitespace. Replace with empty.
\b\w+\b
Tokenise into words. ASCII; use \p{L}+ for Unicode.
#[a-fA-F0-9]{3,8}\b
Hex colour, 3 / 4 / 6 / 8 digits.
^([A-Z]+):\s+
Lines like ERROR: , TODO: .
(?P<ts>\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2})
ISO timestamp capture.
(?P<k>\w+)="(?P<v>[^"]*)"
Quoted key=value pairs.
parse a log fileEnd-to-end · Log parser
Verbose-mode pattern with named groups + an optional final field. Counts errors per service.
python
import re
from pathlib import Path
# Parse "2026-05-17 12:34:56 ERROR [orders] failed: ConnectTimeout"
LINE = re.compile(
r"""
^(?P\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}) # timestamp
\s+(?PDEBUG|INFO|WARN|ERROR) # level
\s+\[(?P[\w.\-]+)\] # service
\s+(?P.+?) # message (lazy)
(?::\s+(?P\w+))?$ # optional exception
""",
re.VERBOSE,
)
errors_by_service: dict[str, int] = {}
for line in Path("app.log").read_text().splitlines():
m = LINE.match(line)
if not m or m["level"] != "ERROR":
continue
errors_by_service[m["svc"]] = errors_by_service.get(m["svc"], 0) + 1
for svc, n in sorted(errors_by_service.items(), key=lambda kv: -kv[1]):
print(f"{n:5d} {svc}")
Best practiceGood to know
Verbose mode is free.re.VERBOSE / /x let you split a pattern over
lines with comments. Once a regex needs a docstring to explain, switch.
Prefer named groups in anything you’ll re-read.(?P<ts>…) beats m.group(1). Reordering
groups later doesn’t break the call site.
Compile once when you re-use a pattern.pat = re.compile(…) outside the loop is measurably faster than
module-level re.search for hot paths.
Common trapsWatch out for
Catastrophic backtracking.
Nested unbounded quantifiers on overlapping classes (e.g. (a+)+$) explode
on near-miss inputs. Anchor, use possessive / atomic groups, or rewrite to a deterministic shape.
. excludes newlines by default.
A “match everything” pattern across lines needs the s /
DOTALL flag — or [\s\S] as a portable
workaround.
Don’t parse HTML / JSON / email addresses with regex for real work.
Use a parser. Regex is fine to find things in those formats; building a validator with it leads
to bugs that hide for months.
Regular expressions match and extract text patterns. Common uses include input validation (email, phone number, postal code), log parsing, find-and-replace in editors, URL routing, tokenizing data, and scraping structured fields from unstructured text.
What is the difference between greedy and lazy quantifiers in regex?
Greedy quantifiers (+, *, {n,}) match as many characters as possible. Lazy quantifiers (+?, *?, {n,}?) match as few as possible, stopping at the first opportunity. Use lazy matching when you need the shortest possible match, such as extracting content between two HTML tags.
What are lookaheads and lookbehinds in regex?
Lookaheads (?=...) and lookbehinds (?<=...) assert that a pattern exists ahead of or behind the current position without consuming characters. Negative variants (?!...) and (?<!...) assert absence. They are useful for matching a word only when followed or preceded by a specific context.
What is the difference between capturing and non-capturing groups?
A capturing group (...) saves the matched text and makes it available as a back-reference or in the match result. A non-capturing group (?:...) groups tokens for alternation or quantification without saving the match. Use non-capturing groups when you do not need to reference the matched text.
Do regex patterns work the same in all languages?
No. Engines differ in what they support: JavaScript lacks lookbehinds in older environments, Python re uses different escape rules than re2-style engines, and PCRE supports atomic groups and possessive quantifiers that most other engines do not. Always check engine-specific docs.