扶凯

取势 明道 优术

作者为 扶 凯 发表

IT•技术

每个人都会用到的就是解析 HTML, 很多人都是使用正则来进行解析. 当然我们是可以使用正则, 但是相比起我最喜欢的方案使用 Mojo::DOM 这个模块所提供的 CSS3 的选择器可以直接进行 DOM 元素的操作来讲, 这个方案有意思多了.
相比起早期我来尝试记住和使用 XPATH 来讲, 这个 Mojo 也更好.

这的 DOM 是指 "文档对象模型". 它可以用于解析和组织信息, 并用来访问和查询其中的一些内容, 如 "找到全面的 div 的标记" 或 "找到指定类的全部内容". 这样不需要我们自己来操作文本文件本身.

如果我们使用 Mojo::UserAgent, 我们可以直接从 HTTP 的响应中取得 DOM 对象:

use Mojo::UserAgent;
my $ua = Mojo::UserAgent->new;

my $dom = $ua->get( 'http://search.cpan.org/~bdfoy/' )
    ->res
    ->dom;

在这魔咒 (Mojolicious) 是使用的方法链风格的方式进行方法调用. 对于复杂的任务, 这个非常有优势.

我们这时并不一定需要靠 Mojo::UserAgent 才能得到 DOM 对象. 我们也可以解析本地 (非服务器) 的 HTML 文件. 这也非常容易.

我们还能手动处理和删除我们并不想要的部分. 这并不需要使用正则就能做到. 这也不需要保存中间的状态. 直接使用 DOM 的操作就行, 没问题.

还是上面这个例子, 假设, 我们给 http://search.cpan.org/~bdfoy/' 这个链接的内容下载到本地 (这是 CPAN 搜索作者的页面), 并读出来了存到 $string 变量中.

use Mojo::DOM;
use 5.010;

my $string = ...;

my $dom = Mojo::DOM->new( $string );

say  $dom
    ->find('a')
    ->join("\n");

在这, 我们有一个 $dom 的对象, 我们使用 find 的方式来选择元素, 这是使用的 CSS3 的选择器, 这时会找到所有的 a 标签的所有链接. 因为内容是一堆链接, 所以会返回 Mojo::Collection 对象(多个结果),

我们可以给这个对象想象成一个数组一样的列表和它能做些什么.

魔咒 (Mojolicious) 使用了大量的这种方法链的方式来调用, 这时我们对于处理的结果, 需要输出来, 这时, 我们使用 join 来加入了一些换行符. 输出的结果如下:

<a href="/"><img alt="CPAN" src="http://st.pimg.net/tucs/img/cpan_banner.png"></a>
<a href="/">Home</a>
<a href="/author/">Authors</a>
<a href="/recent">Recent</a>
<a href="http://log.perl.org/cpansearch/">News</a>
<a href="/mirror">Mirrors</a>
<a href="/faq.html">FAQ</a>
<a href="/feedback">Feedback</a>
<a href="Acme-BDFOY-0.01/">Acme-BDFOY-0.01</a>
<a href="/CPAN/authors/id/B/BD/BDFOY/Acme-BDFOY-0.01.tar.gz">Download</a>
<a href="/src/BDFOY/Acme-BDFOY-0.01/">Browse</a>

这是一个非常好的开始, 接着我们可能想对提取出来的所有的这些链接, 来进行限制, 从上面看得到, 我们要的是模块名和链接地址, 但上面还输出了网页中的 HEAD 中一些分类之类的休息.

所以这时我们要对提取的内容进行限制, 由下可以见到, 我们是需要的是 tr 标签中有  td 的内容下面的 a 标签的链接.

<tr class=s>
    <td><a href="Data-Constraint-1.17/">Data-Constraint-1.17</a></td>
    <td>prototypical value checking</td>
    <td><small>[<a href="/CPAN/authors/id/B/BD/BDFOY/Data-Constraint-1.17.tar.gz">Download</a>] [<a
      href="/src/BDFOY/Data-Constraint-1.17/">Browse</a>]</small></td>
    <td nowrap>26 Aug 2014</td>
   </tr>

我们这时修改我们的选择器, 对于链接我们需的是 tr 表格中第一行中的第一个单元格 td:

say $dom
    ->find('tr td:first-child a:first-child')
    ->join("\n");

使用 'tr td:first-child a:first-child' 之后(这是 CSS 选择器的语法, 很简单, 值得看看), 现在我得到的新的列表, 是象下面这样. 这是 HTML 的文本:

<a href="Acme-BDFOY-0.01/">Acme-BDFOY-0.01</a>
<a href="Apache-Htaccess-1.4/">Apache-Htaccess-1.4</a>
<a href="Apache-iTunes-0.11/">Apache-iTunes-0.11</a>
<a href="App-Module-Lister-0.15/">App-Module-Lister-0.15</a>
<a href="App-PPI-Dumper-1.02/">App-PPI-Dumper-1.02</a>

所以这时我们还有一些工作需要做, 我们只需要信息, 给无用的标签去掉, 我们可以直接提取 href 的属性. 我们可以通过 map 的方法来操作 Mojo::Collection 对象中每个元素:

say $dom
    ->find('tr td:first-child a:first-child')
    ->map( attr => 'href' )
    ->join("\n");

上面的列表产生的每一个 collection 的内容, 其实也是一个 Mojo::DOM 对象. 所以在这的 map 方法会调用会默认使用 Mojo::DOM 对象, 第一个参数会是这个对象的方法, 其它的内容会以参数传递给方法.

在这个例子中, 我们每行在对象上调用的是 attr('href') 方法.现在我们可以得到如下的值:

Acme-BDFOY-0.01/
Apache-Htaccess-1.4/
Apache-iTunes-0.11/
App-Module-Lister-0.15/
App-PPI-Dumper-1.02/

我们并不想要这个斜线, 我们还可以接着使用 map 在处理一次, 这次直接使用匿名函数, 来对结果集中所有元素进行替换.

say $dom
    ->find('tr td:first-child a:first-child')
    ->map( attr => 'href' )
    ->map(sub {s|\/||;$_})
    ->join("\n");

这时我们可以取到的列表是这样:


Acme-BDFOY-0.01
Apache-Htaccess-1.4
Apache-iTunes-0.11
App-Module-Lister-0.15
App-PPI-Dumper-1.02

我们并不想按行换行, 我们可能想给结果存起来做为一个列表, 我们只需要在方法链中使用 each 就行.

my @module_list = $dom
    ->find('tr td:first-child a:first-child')
    ->map( attr => 'href' )
    ->map(sub {s|\/||;$_})
    ->each;

print join "\n", @module_list;

我可以更酷些, 我们同时得到名称, 并且加上版本号, 我们可以使用 CPAN::DistnameInfo 来加上这个.
我们给找到的每个链接转换成一对名称和版本的数组. 这个模块就是做这个. 因为这个模块需要文件的名字, 所以我们需要加上 .tar.gz 的名字.

use Data::Printer;
use CPAN::DistnameInfo;

my $dom = Mojo::DOM->new( $string );

my @module_list = $dom
    ->find('tr td:first-child a:first-child')
    ->map( attr => 'href' )
    ->map(sub {s|\/||;$_})
    ->map( sub {
        my $d = CPAN::DistnameInfo->new( "$_.tar.gz" );
        [ map { $d->$_() } qw(dist version) ];
         } )
    ->each;

p @module_list;

这时我们解析收集出来的每个元素会是下面这样, 这是使用 Data::Printer 的 p 函数输出的显示结果:

[
    [0]   [
        [0] "Acme-BDFOY",
        [1] 0.01
    ],
    [1]   [
        [0] "Apache-Htaccess",
        [1] 1.4
    ],
    [2]   [
        [0] "Apache-iTunes",
        [1] 0.11
    ],
    [3]   [
        [0] "App-Module-Lister",
        [1] 0.15
    ],

这时我只想要看开发版本的模块, 我们可以在 Mojo::Collection 上使用 grep 方法:

my @module_list = $dom
    ->find('tr td:first-child a:first-child')
    ->map( attr => 'href' )
    ->map(sub {s|\/||;$_})
    ->map( sub {
        my $d = CPAN::DistnameInfo->new( "$_.tar.gz" );
        [ map { $d->$_() } qw(dist version) ];
         } )
    ->grep( sub { $_->[-1] =~ /_/ } )
    ->each;

我们可以使用 grep 来选择 collection 对象中来进行过滤, 给相应的内容返回真:

[
    [0]  [
        [0] "Brick",
        [1] "0.227_01"
    ],
    [1]  [
        [0] "Distribution-Guess-BuildSystem",
        [1] "0.12_02"
    ],
    [2]  [
        [0] "File-Fingerprint",
        [1] "0.10_02"
    ],
    [3]  [
        [0] "Geo-GeoNames",
        [1] "1.01_01"
    ],

本教程到这就完了, 这时没有 HTML 的代码, 只有我们想要的元素了.

PS: 译者注: 这个文档, 有些代码为了方便和容易读, 我做了简单的修改, 其实 Mojo::DOM 比这个教程要强大得多, 很早我就想写这个文章, 但不知道从那开始.这个模块还可以做为服务器端来对网页的 DOM 结构进行修改然后输出之类,还可以单行来实现这些功能, 还有命令行版本的这个选择器.非常非常强大.
原文地址: Extracting from HTML with Mojo::DOM

来了就留个评论吧! 没有评论