使用Mojo::DOM从HTML中提取

每个人都想解析HTML,很多人都会伸手去用正则表达式来做到这一点。虽然你可以使用正则表达式来解析HTML,但这不如我最喜欢的最新方法有趣:使用CSS3选择器的Mojo::DOM。我发现这种方法比尝试记住XPATH要容易得多,我还可以玩Mojo。
DOM是“文档对象模型”。幕后有一些东西解析和组织信息,并允许我通过诸如“找到所有位于div
标签内的a
标签”或“找到特定类的所有标签”等问题来查询它。我不亲自操作文本。
如果我使用Mojo::UserAgent,我可以从HTTP请求的响应对象中获取DOM对象
use Mojo::UserAgent;
my $ua = Mojo::UserAgent->new;
my $dom = $ua->get( 'https://metacpan.org/author/BDFOY' )
->res
->dom;
Mojo的单行方法链风格显示了其在处理更复杂任务时的优势。
我不必发出请求来获取DOM对象。我经常得到要解析的HTML文件,而没有服务器来提供它们。根据任务的可行性,我可能手动编辑它,删除我不希望考虑的部分,然后使用正则表达式处理其余部分。这样,我就不必做很多工作来保存状态和知道我在文档中的位置。使用DOM就没有这个问题。
在第一个示例中,我获取了http://search.cpan.org/~bdfoy/'
,我的CPAN搜索作者页面。我将从该HTML开始,假设我已经有了一个字符串。
use Mojo::DOM;
my $string = ...;
my $dom = Mojo::DOM->new( $string );
my $module_list = $dom
->find('a')
->join("\n");
print $module_list;
一旦我有了$dom
对象,我就可以使用find
来选择元素。我给find
一个CSS3选择器,在这个例子中就是a
,以找到所有的锚链接。find
返回一个Mojo::Collection对象,这是一种存储列表并对其进行操作的巧妙方式。Mojolicious风格大量使用方法链,因此需要一种方法来调用结果上的方法。在这个例子中,我只是使用换行符将元素连接起来。这些是结果
<a data-target=".slidepanel" data-toggle="slidepanel" href="#">
<i class="fa fa-bars icon-slidepanel"></i></a>
<a href="/"><img alt="MetaCPAN icon" src="/static/icons/metacpan-icon.png">Home</a>
<a href="https://grep.metacpan.org"><i class="fa fa-search"></i>grep::cpan</a>
<a href="/recent"><i class="fa fa-history"></i>Recent</a>
<a href="/about"><i class="fa fa-info"></i>About</a>
<a href="/about/faq"><i class="fa fa-question"></i>FAQ</a>
...
这是一个好的开始,但我提取了所有的链接。我想限制为到我的分布的链接。查看HTML,我发现有一个id为author_releases
的表格
<table id="author_releases"
data-default-sort="1,0"
class="table table-condensed table-striped table-releases tablesorter">
<thead>
<tr>
<th class="river-gauge"><span class="sr-only">River gauge</span></th>
<th class="name pull-left-phone">Release</th>
<th class="hidden-phone invisible no-sort"></th>
<th class="date">Uploaded</th>
</tr>
</thead>
<tbody>
我将选择器改为寻找表格行中第一个单元格的第一个锚点
my $module_list = $dom
->find('table#author_releases tr td.name a.ellipsis')
->join("\n");
print $module_list;
现在我有了一个我想要的链接列表,但包含了锚点HTML和文本
<a class="ellipsis" href="/release/App-Module-Lister" title="BDFOY/App-Module-Lister-0.153">App-Module-Lister-0.153</a>
<a class="ellipsis" href="/release/App-PPI-Dumper" title="BDFOY/App-PPI-Dumper-1.021">App-PPI-Dumper-1.021</a>
<a class="ellipsis" href="/release/App-scriptdist" title="BDFOY/App-scriptdist-0.242">App-scriptdist-0.242</a>
<a class="ellipsis" href="/release/App-unichar" title="BDFOY/App-unichar-0.012">App-unichar-0.012</a>
<a class="ellipsis" href="/release/App-url" title="BDFOY/App-url-1.004">App-url-1.004</a>
<a class="ellipsis" href="/release/Brick" title="BDFOY/Brick-0.228">Brick-0.228</a>
<a class="ellipsis" href="/release/Bundle-BDFOY" title="BDFOY/Bundle-BDFOY-20190721">Bundle-BDFOY-20190721</a>
我还有一些工作要做。我想提取href
属性的值。我可以用Mojo::Collection的map
方法来做
my $module_list = $dom
->find('table#author_releases tr td.name a.ellipsis')
->map( 'text' )
->join("\n");
print $module_list;
集合中的每个元素实际上都是一个Mojo::DOM对象。map
的第一个参数是要在每个元素上调用的方法,其余参数传递给该方法。在这种情况下,我在每个节点上调用text
,以获取在打开和关闭A
标签之间的字符串。现在我有了一个分布列表
App-Module-Lister-0.153
App-PPI-Dumper-1.021
App-scriptdist-0.242
App-unichar-0.012
App-url-1.004
Brick-0.228
Bundle-BDFOY-20190721
Business-ISBN-3.005
Business-ISBN-Data-20191107
...
由于我在方法链的末尾使用了join("\n")
,所以这仍然是一个字符串。要获取一个列表,我使用each
来获取列表,稍后我自己将其连接起来
my @module_list = $dom
->find('table#author_releases tr td.name a.ellipsis')
->map( 'text' )
->each;
print join "\n", @module_list;
我也可以使用to_array
来获取数组引用。
在版本和分发名称之间,我可以用CPAN::DistnameInfo来分割。我会将每个找到的链接转换为名称和版本的元组。由于该模块想要处理分发文件名,我在后面添加.tar.gz使其正常工作。
use CPAN::DistnameInfo;
use Mojo::Util qw(dumper);
my $dom = Mojo::DOM->new( $string );
my @module_list = $dom
->find('table#author_releases tr td.name a.ellipsis')
->map( 'text' )
->map( sub {
my $d = CPAN::DistnameInfo->new( "$_.tar.gz" );
[ map { $d->$_() } qw(dist version) ];
} )
->each;
say dumper( \@module_list );
each
从集合中提取每个元素并返回它。我使用Data::Printer来显示数组。
[
[
"App-Module-Lister",
"0.153"
],
[
"App-PPI-Dumper",
"1.021"
],
[
"App-scriptdist",
"0.242"
],
[
"App-unichar",
"0.012"
],
...
]
如果只想获取beta版本的分发(或者你想要称之为预1.0版本的分发),可以使用Mojo::Collection的grep
。
my @module_list = $dom
->find('table#author_releases tr td.name a.ellipsis')
->map( 'text' )
->map( sub {
my $d = CPAN::DistnameInfo->new( "$_.tar.gz" );
[ map { $d->$_() } qw(dist version) ];
} )
->grep( sub { $_->[-1] < 1 } )
->each;
grep
会过滤出那些子程序返回true值的集合。
[
[
[
"App-Module-Lister",
"0.153"
],
[
"App-scriptdist",
"0.242"
],
[
"App-unichar",
"0.012"
],
[
"Brick",
"0.228"
],
...
]
这就是整个过程。代码中不会出现HTML。其余的是确定如何选择我想要的特定元素。如果你对Mojo::Collection的更多示例感兴趣,可以查看Mojo Web Clients。
*本文最初发布在PerlTricks.com。在原始形式中,它与search.cpan.org一起工作,后者有不同的表格和HTML。它被更新为与MetaCPAN一起使用。查看本文的完整历史*
标签
brian d foy
brian d foy是一位Perl培训师和作家,同时也是Perl.com的高级编辑。他是Mastering Perl、Mojolicious Web Clients、Learning Perl Exercises的作者,同时也是Programming Perl、Learning Perl、Intermediate Perl和Effective Perl Programming的合著者。
浏览他们的文章
反馈
这篇文章有问题吗?通过在GitHub上打开问题或pull request来帮助我们。