Process HTML with a Perl Module, Redux
Sixteen years ago, a long time ago, in a Perl far, far away, I wrote an article for Inform IT to promote Effective Perl Programming. In Process HTML with a Perl Module, I used HTML::TreeBuilder to change around some HTML. Today I’d do something completely different and much simpler.
In that article, I started with this HTML, which is itself dated with search.cpan.org went away a long time ago, although MetaCPAN still serves its links:
<html>
<head>
<title>This is the title</title>
</head>
<body>
<h1>This is the heading</h1>
<a href="http://search.cpan.org">Search CPAN</a>
<a href="http://perldoc.perl.org">Perldoc</a>
<div>
<a href="http://testers.cpan.org">Testers</a>
<img src="http://www.example.com/images/logo.gif">
</div>
</body>
</html>The first task was to extract the title and top heading of the HTML. Mojo::DOM’s has a nice interface to whatever data structure it builds so I don’t have to know about the low-level details:
use v5.10;
use Mojo::DOM;
use Mojo::File;
my $dom = Mojo::DOM->new( Mojo::File->new($ARGV[0])->slurp('UTF-8') );
say "Title: " . eval { $dom->at('title')->all_text } // '<no title>';
say "H1: " . eval { $dom->at('h1')->all_text } // '<no h1>';Title: This is the title
H1: This is the headingThe second task extracted all the links. Previously, I searched every tag and queried its type. I can use find instead (and may have been able to back then too). Mojolicious uses a Collection that has objects to access and manipulate each found item, with convenience functions such as attr to extract parts:
say "Links:\n\t" . $dom->find('a')->map( attr => 'href' )->join("\n\t");I didn’t have to make my own loops to handle this because the map method does that for me:
Links:
http://search.cpan.org
http://perldoc.perl.org
http://testers.cpan.orgI probably could have used a Perl map in the old article, but when the internals get too complicated, I tend to use a foreach. I think I used to do that because I found people didn’t like map that much, and I care much less about accommodating that now.
The next part was to expand that example to also pull out src values from the img tags:
say "Links:\n\t" . $dom->find('a, img')
->map( sub { $_->attr( $_->tag eq 'a' ? 'href' : 'src' ) } )
->join("\n\t");Links:
http://search.cpan.org
http://perldoc.perl.org
http://testers.cpan.org
http://www.example.com/images/logo.gifI can make that extendable by using a hash to map the tag name to the attribute I want:
my %Attrs = qw(
a href
img src
);
say "Links:\n\t" . $dom->find('a, img')
->map( sub { $_->attr( $Attrs{$_->tag} ) } )
->join("\n\t");Finally, I added a class attribute to each a and img tag. With two arguments, attr sets the value in the DOM:
my %Classes = qw(
a link
img picture
);
$dom->find('a, img')
->map( sub { $_->attr( class => $Classes{$_->tag} ) } )
->join("\n\t");
say $dom;When I output the DOM, I see the new classes in those elements. In the old example, HTML::TreeBuilder reflowed everything, smooshing the tags together. This output preserved the whitespace, although I don’t think Mojo::DOM guarantees that:
<html>
<head>
<title>This is the title</title>
</head>
<body>
<h1>This is the heading</h1>
<a class="link" href="http://search.cpan.org">Search CPAN</a>
<a class="link" href="http://perldoc.perl.org">Perldoc</a>
<div>
<a class="link" href="http://testers.cpan.org">Testers</a>
<img class="picture" src="http://www.example.com/images/logo.gif">
</div>
</body>
</html>There are times that I don’t want to use Mojo::DOM though: it tries to fix broken markup, and sometimes the way it does that makes later tasks a bit harder.