Let's get the famous warning out of the way first: you cannot fully parse HTML with regex. HTML is a nested, recursive language, and regular expressions can't reliably match arbitrarily nested structures — which is why the standard advice is to use a real parser. That advice is correct. But there's a narrower, honest truth every scraper knows: when you need to pull one small, well-defined value out of a known chunk of markup, reaching for a regex is often faster than spinning up an XPath or CSS query — and lookaround (lookahead and lookbehind) makes those extractions clean. This guide covers the syntax, worked patterns, the pitfalls, and — importantly — exactly when to stop and use a parser instead.
Lookaround syntax
Lookaround assertions match a position, not characters. They let you say "match here, but only if this other text is (or isn't) next to it" — without including that other text in the result. There are four of them:
Positive lookahead — the value must be followed by expr:
match(?=expr)
Negative lookahead — the value must not be followed by expr:
match(?!expr)
Positive lookbehind — the value must be preceded by expr:
(?<=expr)match
Negative lookbehind — the value must not be preceded by expr:
(?<!expr)match
The key property: the text inside the lookaround is a condition, not part of the match. That's what makes it so tidy for extraction.
Worked example: extracting a value
Say you need the number out of this fragment:
<span>AWS service charges: $69</span>
With a capture group, you match the whole context and pull the group out afterwards:
AWS service charges:\s*\$(\d+)\s*<
The full text AWS service charges: $69 < matches, and 69 lands in group 1 —
so you have to read match.group(1), not the whole match.
With lookaround, only the number matches, and it's ready to use directly:
(?<=AWS service charges:\s*\$)\d+(?=\s*<)
Here the lookbehind checks that AWS service charges: $ sits just before the
digits, the lookahead checks that whitespace-then-< follows, and the actual
match is just 69. The second pattern looks busier, but the payoff is real: no
group bookkeeping, and you can drop the match straight into your output.
A more practical scraping example — grab an href from a specific link without
capturing the quotes:
(?<=href=")[^"]+(?=")
Or pull the text inside a tag that carries a known class, when you're certain the content has no nested tags:
(?<=<span class="price">)[^<]+(?=</span>)
That last one is exactly the kind of pattern regex is good at — a single, flat, predictable value — and exactly the kind that breaks the moment the markup nests. More on that below.
The 2026 reality on engine support
Older tutorials (including the one this article grew out of) claim "JavaScript doesn't support lookaround." That's out of date. Here's where the major engines actually stand:
- JavaScript — lookahead has always worked; lookbehind has been supported since ES2018 and now works in every current browser and in Node.js. The old limitation is gone. JavaScript also allows variable-length lookbehind.
- Python (built-in
re) — lookahead is unrestricted, but lookbehind must be fixed-width: you can't put a quantifier (*,+,{2,5}) or alternation of different lengths inside a lookbehind. If you need variable-length lookbehind, install the third-partyregexmodule, which supports it. - PCRE / PCRE2 (PHP, many others) — same fixed-width-lookbehind rule as
Python's
re. A common workaround is\K, which resets the start of the reported match, giving you a lookbehind-like effect without the width limit:AWS service charges: \$\K\d+. - .NET (C#) — the outlier: it supports variable-length lookbehind natively, so patterns Python would reject work fine.
So the honest summary is: lookahead is universal; lookbehind is widely supported now, but variable-length lookbehind is the feature that still varies by engine. When in doubt, test your pattern against the exact flavor you're targeting on an online regex tester.
Pitfalls when parsing HTML with regex
This is where "regex for a quick extraction" turns into "regex for parsing," and where it bites:
- Nesting.
[^<]+between two tags assumes there are no child tags. The moment the content contains a nested<b>or<a>, your match stops early or grabs the wrong thing. Regex has no concept of matching pairs. - Greedy matching.
<div>(.*)</div>on a page with several<div>s will match from the first opening tag to the last closing tag. Use lazy quantifiers (.*?) — and even then, be suspicious. - Attribute order and quoting.
class="x" id="y"andid="y" class='x'are the same to a browser but different strings to a regex. Real markup varies its quoting (single, double, none) and whitespace freely. - HTML entities.
&,', and friends mean your matched text isn't the decoded text. A parser decodes these for you; a regex doesn't. - Comments, CDATA, and
<script>. Markup that looks like tags but isn't (inside comments or script strings) will fool a naive pattern.
None of these are edge cases on real-world pages — they're the norm.
When to use a real parser instead
Switch to a proper HTML parser the moment any of these are true: the value can contain nested tags, you need more than one field per record, the markup varies across pages, or you're walking the document tree rather than pulling one token. That's most scraping, honestly. The standard tools:
- Python: BeautifulSoup for forgiving, readable
parsing; lxml or
parselwhen you want speed and full XPath / CSS selector support. - JavaScript/Node:
cheeriofor a jQuery-like API over server-side HTML, or the DOM directly in a headless browser.
A parser builds the document tree once and lets you query it by structure —
soup.select_one("span.price") — which survives attribute reordering, nesting,
and entity encoding that regex can't. The two approaches also compose well: use
a parser to isolate the right element, then a small regex to pull a number or
date out of that element's clean text. That last move is where lookaround
genuinely shines — see our guide to
regular expressions for scraping for
more of those patterns.
Rule of thumb
Use parsing HTML with regex for a single, flat, predictable value inside a known snippet — and let lookaround keep the match clean. Use a real parser for anything structural, nested, or repeated. If you find yourself writing a regex with several nested lookarounds to handle "the markup that changes on every page," that's the signal to stop.
Building and maintaining robust extraction across sites that change their markup weekly is a real cost — if you'd rather receive clean structured records than babysit selectors and patterns, that's what our web scraping service does.
FAQ
Can you parse HTML with regex at all? For extracting a single well-defined value from known markup, yes. For parsing a document's structure (nested, repeated, or variable elements), no — use an HTML parser.
Does JavaScript support regex lookbehind now? Yes. Lookbehind has worked in JavaScript since ES2018 and is supported in all current browsers and Node.js, including variable-length lookbehind.
Why does my Python lookbehind throw an error?
Python's built-in re requires fixed-width lookbehinds — no quantifiers or
uneven alternation inside (?<=...). Use \K-style rewrites where possible, or
the third-party regex module for variable-width lookbehind.
Groups or lookaround — which is better for extraction? Lookaround gives you the value directly with no group bookkeeping, which is cleaner for single-value extraction. Capture groups are better when you need several fields from one match or when your engine's lookbehind is too limited.