By Language 11 min read

Perl Web Scraping with Regular Expressions

Perl web scraping explained: regex syntax, matching operators, and LWP examples for extracting data from HTML pages. Dust off Perl and scrape along.

ST
Scraping.Pro Team
Data collection for business needs
Published: 2 October 2025

Perl was the web's original text-wrangling language, and text-wrangling is exactly what scraping is. Long before Python's requests and BeautifulSoup, people were pulling data off pages with a few lines of Perl and a well-aimed regular expression. In 2026, Perl is no longer the default choice — but it is still an excellent, and often underrated, tool for Perl web scraping: it has mature HTTP clients, real HTML/DOM parsers, first-class JSON support, and the most powerful regular expressions built into any mainstream language.

This guide covers the full stack: fetching pages, parsing HTML, and — the part Perl is famous for — using regex in Perl to extract and clean the final values. If Perl is already in your toolbox (or your legacy codebase), this is how to scrape with it properly.

Where Perl fits in 2026

Let's be honest about the landscape. For a brand-new scraping project, most teams reach for Python or Node.js because of ecosystem size and headless-browser tooling. Perl earns its place when:

  • you already run Perl in production and want the scraper to live in the same codebase;
  • the target is server-rendered HTML (no heavy JavaScript), which is the sweet spot for a fast HTTP-plus-regex scraper;
  • you need serious text processing — log-like formats, messy inline data, oddly structured pages — where Perl's regex engine is genuinely best-in-class;
  • you want a small, dependency-light script that starts instantly and runs anywhere Perl is installed (which is nearly everywhere on Unix).

Where it struggles: JavaScript-rendered pages need a headless browser, and Perl's browser-automation bindings, while they exist, are less polished than Python's or Node's. For those targets, consider driving a browser (see below) or a different stack.

The Perl scraping toolkit

A modern Perl scraper is built from three layers.

1. An HTTP client to fetch pages:

  • HTTP::Tiny — ships with core Perl, zero dependencies, perfect for simple GETs.
  • LWP::UserAgent (the libwww-perl suite) — the classic, full-featured client: cookies, redirects, proxies, auth.
  • Mojo::UserAgent (from the Mojolicious framework) — a modern async client with a built-in DOM parser and CSS selectors. This is the one I'd start a new scraper with.

2. An HTML/DOM parser to navigate markup:

  • Mojo::DOM — clean CSS-selector access, comes with Mojo::UserAgent.
  • HTML::TreeBuilder / HTML::TreeBuilder::XPath — a full tree with XPath support.
  • Web::Scraper — a small DSL that maps selectors to a data structure.

3. Regular expressions for the "last mile" — cleaning and extracting values out of the text you've already isolated.

The golden rule, same as in every language: use a parser to find the right node, use regex to clean the text inside it. Don't try to parse whole HTML documents with a single regular expression — HTML is nested and irregular, and that road leads to brittle code.

Fetching a page

The minimal fetch with core Perl:

perl
use strict;
use warnings;
use HTTP::Tiny;

my $res = HTTP::Tiny->new(
    agent => 'Mozilla/5.0 (compatible; MyScraper/1.0)',
)->get('https://example.com/products');

die "Failed: $res->{status}\n" unless $res->{success};
my $html = $res->{content};

The same idea with LWP::UserAgent, which gives you cookies, redirects, and proxy support out of the box:

perl
use strict;
use warnings;
use LWP::UserAgent;

my $ua = LWP::UserAgent->new(
    agent   => 'Mozilla/5.0 (compatible; MyScraper/1.0)',
    timeout => 20,
);
$ua->proxy(['http', 'https'], 'http://user:pass@proxy.example:8000');  # rotating proxy

my $resp = $ua->get('https://example.com/products');
die $resp->status_line unless $resp->is_success;
my $html = $resp->decoded_content;

For anything nontrivial you'll want to set a realistic User-Agent, handle cookies, and route requests through rotating proxies to avoid rate limits and blocks — the same discipline as any other stack.

Regex in Perl: the core of the extraction step

This is where Perl shines. Perl's regular-expression syntax is woven directly into the language, which makes matching and substitution concise and fast. The engine compiles a pattern into a compact set of opcodes and runs them very quickly.

The binding operators

You apply a pattern to a string with the binding operators =~ (match) and !~ (does-not-match):

perl
my $s = 'new variable';
print "has space\n"  if $s =~ /\s/;   # true: contains whitespace
print "no digit\n"   if $s !~ /\d/;   # true: contains no digit

Without an explicit binding, a pattern is applied to the default variable $_.

Match and substitute

There are two workhorse operators:

  • Matchm/PATTERN/ (the m is optional when you use / as the delimiter).
  • Substitutes/PATTERN/REPLACEMENT/.
perl
my $time = '14:30:52';

# Capture groups fill $1, $2, $3 on a successful match:
if ($time =~ m/(\d\d):(\d\d):(\d\d)/) {
    my ($h, $m, $s) = ($1, $2, $3);
}

# Or capture straight into a list, in one line:
my ($h, $m, $s) = $time =~ /(\d+):(\d+):(\d+)/;

Substitution replaces matches; add the g flag to replace every occurrence and the r flag (modern Perl) to return the result non-destructively instead of modifying in place:

perl
my $raw   = "Price:  $1,299.50 ";
my $clean = $raw =~ s/[^\d.]//gr;   # "1299.50" — original left untouched

Delimiters

The / characters are just delimiters; you can swap in others (any non-alphanumeric, non-whitespace character), which is handy when the pattern itself contains slashes — for example, matching a URL path:

perl
$text =~ m{https?://example\.com/(\w+)};   # no need to escape the slashes
$text =~ s{<br\s*/?>}{\n}g;                 # tidy line breaks

Paired delimiters like {} and () work for both halves of a substitution: s{find}{replace}.

Capture groups and useful variables

Parentheses ( ) capture matched fragments into $1, $2, and so on, left to right. Named captures make patterns self-documenting and reorder-proof:

perl
if ($line =~ /(?<dollars>\d+)\.(?<cents>\d{2})/) {
    print "$+{dollars} dollars, $+{cents} cents\n";
}

After a match, Perl also fills a set of special variables (all read-only):

  • $& — the entire matched string
  • $` — the text preceding the match
  • $' — the text following the match
  • $+ — the last captured group
  • @- and @+ — arrays of the start and end offsets of the match and its groups

A quick global-match example, list context:

perl
my $text = '1st 2nd 3rd 4th';
my @matches = $text =~ /(\d)\w+/g;   # all matches of the group
# @matches = ('1', '2', '3', '4')

In list context with the g flag, every capture from every match is flattened into the returned list — handy for pulling repeated fields off a page in one pass.

Pattern modifiers you'll actually use

Append flags after the closing delimiter:

  • i — case-insensitive (/price/i matches "Price", "PRICE").
  • g — global: all matches, not just the first.
  • s — let . match newlines (essential when a value spans lines in the HTML).
  • m — multiline: ^ and $ match at each line boundary.
  • x — extended: ignore whitespace in the pattern so you can lay it out and comment it.
  • r — return the modified copy instead of mutating (works with s/// and tr///).
  • a — restrict \d, \w, \s to ASCII, which keeps English-only parsing from being surprised by Unicode.
  • e — evaluate the replacement side of s/// as Perl code (great for computed replacements).

A note on the old o modifier (compile the pattern only once): in modern Perl this is essentially obsolete. The interpreter caches compiled patterns automatically, and /o can cause subtle bugs when the pattern interpolates a variable. Leave it out.

A worked Perl scraper

Here's a realistic scrape that ties the three layers together — fetch with Mojo::UserAgent, locate nodes with Mojo::DOM CSS selectors, then clean each value with a regex:

perl
use strict;
use warnings;
use Mojo::UserAgent;

my $ua  = Mojo::UserAgent->new;
$ua->transactor->name('Mozilla/5.0 (compatible; MyScraper/1.0)');

my $dom = $ua->get('https://example.com/products')->result->dom;

my @products;
for my $card ($dom->find('div.product')->each) {
    my $name  = $card->at('h2.title')->text;
    my $raw   = $card->at('span.price')->text;   # e.g. "Price: $1,299.50"

    # regex last mile: pull a clean number out of the messy price string
    my ($price) = $raw =~ /(\d[\d,]*(?:\.\d{2})?)/;
    $price =~ s/,//g if defined $price;

    push @products, { name => $name, price => $price };
}

printf "%-40s %s\n", $_->{name}, $_->{price} for @products;

The selector answers where the data is; the regex answers what exactly to keep from the text. That separation is what makes a Perl scraper both precise and readable, and it's identical in spirit to how you'd combine selectors and regular expressions in any language.

Scraping a JSON API with Perl

Not every scrape needs HTML parsing. Many sites load their data from a JSON endpoint that a browser calls in the background — and hitting that directly is faster and sturdier than scraping rendered markup. This is where a web scraping API in Perl comes together with a couple of core modules:

perl
use strict;
use warnings;
use HTTP::Tiny;
use Cpanel::JSON::XS qw(decode_json);   # or the core JSON::PP

my $res = HTTP::Tiny->new->get(
    'https://api.example.com/v1/products?page=1'
);
die "HTTP $res->{status}\n" unless $res->{success};

my $data = decode_json($res->{content});
for my $item (@{ $data->{results} }) {
    printf "%s - \$%.2f\n", $item->{name}, $item->{price};
}

If you're consuming a commercial scraping API (which returns already-extracted data or renders JavaScript for you on the server side), the pattern is the same: HTTP::Tiny or LWP for the request, decode_json for the response. Whenever a clean API endpoint exists, prefer it over parsing HTML — you skip the fragile markup entirely.

JavaScript-heavy pages

If the content only appears after JavaScript runs, a plain HTTP fetch won't see it. In Perl you have two main routes:

  • WWW::Mechanize::Chrome — drives a real Chrome/Chromium instance over the DevTools protocol, so JavaScript executes and you scrape the final DOM.
  • Selenium::Remote::Driver — the Perl binding for Selenium, controlling a browser through WebDriver.

Both work, but they carry the usual browser-automation costs — memory, speed, and detectability. Before reaching for them, check whether the page is really dynamic or whether the data is sitting in a background JSON call you can hit directly (see above). Nine times out of ten, the API route is simpler.

Regex or a parser? A quick rule

  • Structure (finding the price cell, the product title, the next-page link): use Mojo::DOM, HTML::TreeBuilder, or XPath.
  • Values inside a node (the number in "Price: $1,299.50", a SKU, a date, an email): use regex.
  • Whole HTML documents: never regex. Parse them.

Follow that split and your Perl scrapers stay maintainable even as target pages change.

Bottom line

Perl remains a capable scraping language in 2026, especially for server-rendered pages and text-heavy extraction. The stack is small and dependable: Mojo::UserAgent or LWP to fetch, Mojo::DOM or HTML::TreeBuilder to navigate, and Perl's unmatched regular expressions to clean the final values. For JSON endpoints, HTTP::Tiny plus decode_json is all you need; for JavaScript pages, drive a headless browser — or find the underlying API.

If you'd rather have the data than build and babysit a Perl scraper, scraping.pro delivers structured results as a done-for-you web scraping service — you get clean records in the format you want, whatever language sits on your side of the pipe.