使用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::Collectionmap方法来做

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::Collectiongrep

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 PerlMojolicious Web ClientsLearning Perl Exercises的作者,同时也是Programming PerlLearning PerlIntermediate PerlEffective Perl Programming的合著者。

浏览他们的文章

反馈

这篇文章有问题吗?通过在GitHub上打开问题或pull request来帮助我们。