Mojo::DOM CSS Selectors for HTML Parsing
This page walks through how to parse HTML in Perl usingMojo::DOM and CSS selectors. You will learn how to build a DOM object, run queries, extract data, and chain results without writing fragile regular expressions.
That snippet grabs every link from an HTML document by using a CSS selector. You already know this syntax from jQuery, from your browser's DevTools, and from every stylesheet you have written.use Mojo::DOM; my $dom = Mojo::DOM->new($html); $dom->find('a[href]')->each(sub { say $_->{href}; });
Mojo::DOM comes bundled with the Mojolicious web framework. You do not need extra dependencies, you do not need to build a parse tree by hand, and you do not need to learn XPath. You just use CSS selectors and chain methods.
Part 1: CREATING A DOM OBJECT
You feed it raw HTML and you get back an object you can query:From a file:use Mojo::DOM; my $dom = Mojo::DOM->new('<div><p>Hello</p><p>World</p></div>'); say $dom->at('p')->text; # Hello say $dom->find('p')->size; # 2
From a web request (if you are using the full Mojolicious stack):use Mojo::File qw(path); use Mojo::DOM; my $html = path('page.html')->slurp; my $dom = Mojo::DOM->new($html);
That final line fetches a page and hands you a DOM object in a single chain. It keeps the code short and readable.use Mojo::UserAgent; my $ua = Mojo::UserAgent->new; my $dom = $ua->get('https://example.com')->result->dom;
Part 2: CSS SELECTOR BASICS
If you have written CSS, you already know how this works:Mojo::DOM supports nearly the entire CSS3 selector specification. If a selector works in your browser'sSELECTOR MATCHES -------- ------- div All <div> elements .classname Elements with class="classname" #myid Element with id="myid" a[href] <a> tags that have an href attribute input[type="text"] <input> with type="text" div > p <p> directly inside <div> div p <p> anywhere inside <div> ul > li:first-child First <li> in a <ul> h2 + p <p> immediately after <h2>
document.querySelectorAll() method, it will almost certainly work here.
my $dom = Mojo::DOM->new($html); # by tag $dom->find('p'); # by class $dom->find('.warning'); # by id $dom->at('#main-content'); # by attribute $dom->find('img[alt]'); # by attribute value $dom->find('a[href="https://perl.org"]'); # descendant $dom->find('div.content p'); # direct child $dom->find('ul > li');
Part 3: FIND VS AT
There are two main query methods, and you should learn the difference between them.find() returns all matches as a Mojo::Collection:
my $links = $dom->find('a'); say $links->size; # number of matches
at() returns the first match as a single element, or undef if nothing matches:
Usemy $title = $dom->at('title'); say $title->text; # page title
find when you expect multiple results. Use at when you only need one specific element.
Always check for# all paragraphs $dom->find('p')->each(sub { say $_->text }); # the one h1 on the page my $heading = $dom->at('h1'); say $heading->text if $heading;
undef when you use at. If no element matches your query, the method returns nothing, and calling ->text on undef will crash your script.
Part 4: EXTRACTING TEXT AND ATTRIBUTES
You can grab the text content of any element like this:Themy $dom = Mojo::DOM->new('<p>Hello <b>World</b></p>'); say $dom->at('p')->text; # Hello say $dom->at('p')->all_text; # Hello World say $dom->at('b')->text; # World
text method returns the direct text of that element, skipping nested tags. The all_text method returns everything, recursively.
You can read attributes with hash-style access:
Bothmy $dom = Mojo::DOM->new('<a href="https://perl.org" class="ext">Perl</a>'); my $link = $dom->at('a'); say $link->{href}; # https://perl.org say $link->{class}; # ext say $link->attr('href'); # same thing, method style
$element->{attr} and $element->attr('attr') work. The hash dereference is shorter and feels more Perlish.
Part 5: CHAINING WITH MAP AND GREP
Sincefind() returns a Mojo::Collection, you get chainable functional operations for free:
The# extract all URLs from a page my @urls = $dom->find('a[href]') ->map(attr => 'href') ->each; # extract all image sources my @images = $dom->find('img[src]') ->map(attr => 'src') ->grep(sub { m~\.jpg$~i }) ->each;
->map(attr => 'href') shorthand calls ->attr('href') on each element. It is a Mojo::Collection trick for calling methods on every item in the collection.
You can filter and transform in a single pipeline:
The# find all external links with their text $dom->find('a[href^="http"]')->each(sub { printf "%-30s %s\n", $_->text, $_->{href}; });
[href^="http"] selector means "href attribute starts with http." CSS attribute selectors are quite powerful. The ^= operator checks for a prefix, $= checks for a suffix, and *= checks for a substring.
Part 6: REAL WEB SCRAPING
Let us scrape something real and extract a table into a Perl data structure:Eachuse Mojo::DOM; use Mojo::UserAgent; my $ua = Mojo::UserAgent->new; my $dom = $ua->get('https://example.com/data')->result->dom; my @rows; $dom->find('table.data-table tr')->each(sub { my @cells = $_->find('td')->map('text')->each; push @rows, \@cells if @cells; }); for my $row (@rows) { say join "\t", @$row; }
<tr> is found, and inside each row we find all <td> elements, extract their text, and collect it into an array. The job is done.
You can also extract a definition list:
Themy %glossary; $dom->find('dl dt')->each(sub { my $term = $_->text; my $def = $_->next->text; # next sibling (the <dd>) $glossary{$term} = $def; });
->next method returns the next sibling element. You also have ->previous, ->parent, ->children, and ->ancestors available for DOM traversal.
Part 7: HANDLING MALFORMED HTML
Real-world HTML is often messy. Missing closing tags, nested tables from the early two-thousands, and inline styles everywhere are common. Mojo::DOM handles all of that gracefully.It does not crash. It does not die. It makes a best effort to parse the mess and lets you query it anyway. Is the parse tree exactly what the author intended? The author probably did not know either, but you can still extract data from it.my $mess = '<p>Unclosed paragraph<p>Another one<div>Mixed up</p></div>'; my $dom = Mojo::DOM->new($mess); $dom->find('p')->each(sub { say $_->all_text; });
This is a major advantage over strict XML parsers that refuse to touch anything that is not well-formed.
Part 8: COMPARISON TO HTML::TREEBUILDER
The old-school way to parse HTML in Perl wasHTML::TreeBuilder from the HTML::Tree distribution:
Mojo::DOM wins on every front:# HTML::TreeBuilder (the old way) use HTML::TreeBuilder; my $tree = HTML::TreeBuilder->new_from_content($html); my @links = $tree->look_down('_tag', 'a', sub { $_[0]->attr('href') }); for my $link (@links) { say $link->attr('href'); } $tree->delete; # manual cleanup! # Mojo::DOM (the new way) use Mojo::DOM; Mojo::DOM->new($html)->find('a[href]')->each(sub { say $_->{href}; });
FEATURE HTML::TreeBuilder Mojo::DOM ------- ----------------- --------- Selector syntax look_down() CSS selectors Cleanup required Yes ($tree->delete) No (automatic) Chaining No Yes Collection methods No map, grep, each Dependencies HTML::Tree dist Mojolicious Learning curve Moderate Low (if you know CSS)
HTML::TreeBuilder is not a bad module. It served the community well for decades. Mojo::DOM is what you should reach for in 2026.
Part 9: ADVANCED SELECTORS
Mojo::DOM supports several powerful selectors that most developers never use:You can combine them for surgical precision:# nth-child $dom->find('tr:nth-child(odd)'); # odd rows $dom->find('tr:nth-child(2n)'); # even rows $dom->find('li:nth-child(3n+1)'); # every third, starting at 1 # not $dom->find('p:not(.hidden)'); # paragraphs without .hidden # has (contains matching descendant) $dom->find('div:has(> img)'); # divs with direct img child # empty $dom->find('td:empty'); # empty table cells # multiple selectors (OR) $dom->find('h1, h2, h3'); # all top-level headings
That single selector does what would otherwise take ten lines of manual tree walking.# external links in the main content area, excluding nav $dom->find('#content a[href^="http"]:not(.internal)')->each(sub { say "$_->{href}: " . $_->text; });
Part 10: PRACTICAL DATA EXTRACTION
You can pull structured data from a page and convert it to a Perl data structure:Eachuse Mojo::DOM; use Mojo::JSON qw(encode_json); my $dom = Mojo::DOM->new($html); my @products = $dom->find('div.product')->map(sub { { name => $_->at('h3')->text, price => $_->at('.price')->text, link => $_->at('a.details')->{href}, image => $_->at('img')->{src}, } })->each; say encode_json(\@products);
div.product is found, and inside each one we extract the name, price, link, and image using at() to grab specific child elements. The map block returns a hashref for each product, and the result is a clean Perl data structure ready for JSON export or database insertion.
Want it as CSV instead?
You can go from HTML to CSV in six lines. There is no regex-based HTML parsing and no fragile string matching. You just use CSS selectors to point at exactly what you want.$dom->find('div.product')->each(sub { my $name = $_->at('h3')->text; my $price = $_->at('.price')->text; $price =~ s~[^\d.]~~g; # strip $ and commas say "$name,$price"; });
perl.gg$html | Mojo::DOM | find('selector') | +---+---+---+ | | | | [1] [2] [3] [4] Mojo::Collection | map / grep / each | data jQuery for Perl. No browser required. .--. |o_o | |:_/ | // \ \ (| | ) /'\_ _/`\ \___)=(___/