Web Scraper Regex: Extracting Data with PHP Examples
Regular expressions are the sharpest small tool in a scraper's kit. When you need to pull a price, an email, a product ID, or a date out of a blob of text, a well-aimed web scraper regex does the job in one line. But regex is also the tool people reach for at the wrong moment — trying to parse whole HTML documents with it — and pay for later in brittle, unmaintainable patterns. This guide shows the patterns that work, the PHP preg_* functions that run them, and, just as importantly, when to put regex down and use a proper parser instead.
We use PHP for the examples because its PCRE (Perl-Compatible Regular Expressions) functions are a clean, batteries-included way to learn, but the patterns themselves are portable to Python's re, JavaScript, Go, and anything else with a PCRE-style engine.
Where a regex web scraper fits
Think of scraping as two stages: locate the data in the page, then clean and structure it. A regex web scraper is excellent at the second stage and risky at the first.
- Good use of regex: extracting a well-defined pattern from a text fragment — the number inside
"$1,299.00", an email in a footer, aproduct_idin a URL, an ISO date in a string, a phone number in free text. - Risky use of regex: treating raw HTML as text and trying to match nested tags, attributes, and structure. HTML is not a regular language; a regex that works on today's markup breaks the moment an attribute order changes or a tag nests differently.
The reliable pattern is to locate elements with a DOM parser (XPath or CSS selectors), pull the text out of the matched node, then apply regex to that text. We will get concrete about this split after covering the mechanics.
The PHP preg functions you actually need
PHP's modern regex API is the preg_* family. (The old ereg/mb_ereg functions were removed years ago — ignore any tutorial still using them.) A PHP regex is written in Perl syntax wrapped in delimiters, usually /.../, with optional modifier letters after the closing delimiter.
preg_match — does it match, and capture one hit
preg_match() returns 1 if the pattern matches, 0 if not, and stops at the first match. Pass a third argument to capture groups:
$html = 'Price: <span class="price">$1,299.00</span> in stock';
if (preg_match('/\$([\d,]+\.\d{2})/', $html, $m)) {
echo $m[1]; // 1,299.00
echo str_replace(',', '', $m[1]); // 1299.00 (ready to cast to float)
}
$m[0] is the whole match ($1,299.00); $m[1] is the first parenthesized capture group. Capture groups are how you pull part of a match out cleanly.
preg_match_all — capture every hit
When there are many matches on the page — every price in a list, every link — use preg_match_all():
$html = '<a href="/p/101">Widget</a><a href="/p/102">Gadget</a>';
preg_match_all('/\/p\/(\d+)/', $html, $matches);
print_r($matches[1]); // ['101', '102']
$matches[0] holds the full matches, $matches[1] the first capture group across all of them. This is the workhorse for list extraction.
preg_replace — clean and reshape
preg_replace() rewrites matched text, which is invaluable for normalization — stripping tags, collapsing whitespace, reordering fields. Backreferences $1, $2 refer to captured groups:
// Reorder "March 12th" -> "12 March"
echo preg_replace('/(\w+) (\d{1,2})th/i', '$2 $1', 'March 12th'); // 12 March
// Strip any leftover tags from an extracted fragment
$clean = preg_replace('/<[^>]+>/', '', $fragment);
preg_split — break text into pieces
preg_split() splits a string on a pattern — handy when a single field packs several values with inconsistent separators:
$list = 'jan,feb mar. apr, may;june';
print_r(preg_split('/[.,;\s]+/', $list));
// ['jan', 'feb', 'mar', 'apr', 'may', 'june']
One pattern absorbs commas, dots, semicolons, and spaces in any combination. When you only ever split on a single fixed character, explode() is faster and clearer — reach for regex only when the delimiter genuinely varies.
preg_grep — filter an array
preg_grep() keeps array elements that match (or, with PREG_GREP_INVERT, those that don't):
$rows = ['SKU-1001 active', 'SKU-1002 discontinued', 'SKU-1003 active'];
print_r(preg_grep('/active$/', $rows)); // keep active rows
print_r(preg_grep('/active$/', $rows, PREG_GREP_INVERT)); // keep the rest
Capture groups and named groups
Numbered groups get unreadable fast once a pattern has several. Named groups — (?P<name>...) in PCRE — make extraction self-documenting:
$row = 'SKU-4471 | Blue Mug | $12.50';
preg_match('/(?P<sku>SKU-\d+).*\$(?P<price>[\d.]+)/', $row, $m);
echo $m['sku']; // SKU-4471
echo $m['price']; // 12.50
Reaching for $m['price'] instead of $m[2] is the kind of small thing that keeps a scraper maintainable a year later. Use non-capturing groups (?:...) when you need grouping for alternation or quantifiers but don't want to collect the value — it keeps your capture indices clean.
Pattern modifiers that matter for scraping
Modifiers go after the closing delimiter and change how the engine behaves. Four earn their keep in scraping:
i— case-insensitive. Markup casing is inconsistent (<DIV>,<div>), so this saves grief:/sale/i.s— "dotall":.also matches newlines. Essential when the text you want spans multiple lines, since by default.stops at a line break.m— multiline:^and$match at each line boundary instead of only the string's start and end. Useful for line-oriented data.U— ungreedy: makes quantifiers lazy by default. More often you will just make a single quantifier lazy with?(see below).
// Grab everything between two markers, across newlines, without overshooting
preg_match('/<article>(.*?)<\/article>/s', $html, $m);
Greedy vs. lazy: the classic scraping bug
The single most common regex-scraping mistake is a greedy quantifier swallowing too much. .* matches as much as it can, so this grabs from the first tag to the last:
$html = '<b>One</b> and <b>Two</b>';
preg_match('/<b>(.*)<\/b>/', $html, $m);
echo $m[1]; // One</b> and <b>Two <-- too much!
preg_match('/<b>(.*?)<\/b>/', $html, $m); // lazy: .*?
echo $m[1]; // One <-- correct
Add ? after * or + to make it lazy and stop at the first closing marker. Internalize this one and you avoid most "why did my scraper capture half the page" moments.
Parsing HTML with regex: the honest trade-off
You have seen regex tug at HTML above, so let's be direct about parsing HTML with regex. For a small, known, stable fragment — a value between two specific markers — a targeted pattern is fine and pragmatic. For anything involving nested tags, optional attributes, variable ordering, or the structure of the document, regex is the wrong tool: HTML is not regular, and your pattern will be fragile.
The robust approach is a DOM parser. In PHP that is DOMDocument plus DOMXPath; you select nodes structurally, then apply regex only to the extracted text:
libxml_use_internal_errors(true); // suppress warnings on messy HTML
$doc = new DOMDocument();
$doc->loadHTML($html);
$xpath = new DOMXPath($doc);
foreach ($xpath->query("//span[@class='price']") as $node) {
// DOM locates the element reliably...
if (preg_match('/([\d,]+\.\d{2})/', $node->textContent, $m)) {
// ...regex cleans the value out of the text
echo str_replace(',', '', $m[1]), "\n";
}
}
This is the pattern to remember: DOM/XPath to locate, regex to refine. It survives markup changes that would shatter a pure-regex approach. Our deeper dives on regex for web scraping, CSS selectors, and XPath cover each side of that split.
A quick pattern cookbook
Reusable building blocks for a regex web scraper:
$patterns = [
'price' => '/\$?\s?([\d,]+(?:\.\d{2})?)/',
'email' => '/[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/',
'us_phone' => '/\(?\d{3}\)?[\s.\-]?\d{3}[\s.\-]?\d{4}/',
'iso_date' => '/\b(\d{4})-(\d{2})-(\d{2})\b/',
'url' => '/https?:\/\/[^\s"\'<>]+/i',
'sku' => '/\bSKU[-\s]?(\d+)\b/i',
];
Treat these as starting points, not gospel — email and phone formats in particular have endless edge cases, so validate against your real data and loosen or tighten as needed. Test interactively before wiring a pattern into production; an online regex tester shows exactly what a pattern matches and where it backtracks.
FAQ
Should I use regex to scrape HTML? Use regex to extract patterns from text you've already isolated (prices, emails, IDs), not to parse document structure. Locate elements with a DOM parser or XPath/CSS selectors, then apply regex to the extracted text.
Why does my regex capture too much of the page?
A greedy quantifier like .* matches as much as possible. Make it lazy with .*?, or add the U modifier, so it stops at the first closing marker instead of the last.
What's the difference between preg_match and preg_match_all in PHP?
preg_match() stops at the first match; preg_match_all() returns every match on the string. Use preg_match for a single value, preg_match_all for lists.
How do I extract a group instead of the whole match?
Wrap the part you want in parentheses (a capture group) and read it from the matches array — $m[1], or $m['name'] with a named group (?P<name>...).
Is regex faster than a DOM parser? On a small fragment, often yes. But on full documents the speed rarely matters next to correctness — a parser that survives markup changes beats a fast regex that breaks weekly.
Wrapping up
Regex is the right tool for the "clean and structure" half of scraping: preg_match and preg_match_all to capture, preg_replace to normalize, preg_split and preg_grep to reshape, capture groups (named where it helps) to pull out exactly what you need, and lazy quantifiers to avoid over-matching. Pair it with a DOM parser for locating elements and you get scrapers that are both sharp and durable. When the targets grow to many sites and change often, offloading the upkeep to a managed web scraping service keeps your patterns from becoming a maintenance treadmill.