第五天 - 复合选择器比正则表达式更容易

img

当人们告诉我我不能(他们的意思是不应该)用正则表达式解析 HTML 时,我说“拿酒来”。这不仅仅是技巧或态度,而是方便。以正确的方式做到并不总是那么容易(我记得 HTML 0.9 是一个大问题)。最近,我一直在使用 Mojo::DOM 为我做这件事。它比旧的,权宜之计更容易。

诀窍始终是隔离感兴趣的 HTML。我可以做到这一点切除有趣部分周围的所有数据:

1
2
3
my $html = ...;
$html =~ s/.*?<table class="foo".*?>//;
$html =~ s/<\/table>.*//;

现在我不必解析所有的 HTML;我可以考虑一下表格。即使这是权宜之计,也不是那么好。在我用更好的东西替换它之前,我会快速绕道而行。

层叠样式表

您可能知道层叠样式表(CSS)使你的网页看起来很漂亮(但不是我的,真的)。您可以将元数据添加到标记上:

1
2
3
<img id="bender" class="robot" src="..." />
<img id="fry" class="human" src="..." />
<img id="leela" class="mutant" src="..." />

CSS 规则可以通过其 ID 或类来处理这些项目以将样式应用于它们。这个寻址是一个“选择器”,拥有比我更好的技能的人使用这些来使演示非常漂亮:

1
2
3
img#fry   { border: 1px; }

img.robot { margin: 20px; }

HTML 可能有点复杂。也许那些有趣的标签在列表中。这包裹了数据的另一层 HTML 结构:

1
2
3
4
5
<ul class="employees">
<li><img id="bender" class="robot" src="..." /></li>
<li><img id="fry" class="human" src="..." /></li>
<li><img id="leela" class="mutant" src="..." /></li>
</ul>

如果我只想影响该列表中的那些图像,而只影响该列表中的项目。我可以用复合选择器指定祖先(两个或多个一起使用)。选择器之间只有一个空格,这意味着第二个选择器包含在第一个选择器中(“后代”):

1
2
3
ul.employees img.human { border: 1px; }

ul.employees img.robot { margin: 20px; }

但是,本文并不是关于使用选择器可以做的所有奇特事情。你知道它们存在,你可以在 Mojo::CSS::Selectors 中看到它们的可能性。我将在下一节向您展示一些示例。

在 Mojo 中使用选择器

但是你可以用这些来做更多的事情。使用 Mojo::DOM,它支持 CSS Selectors Level 3(以及 Level 4 中的一些东西),您可以使用相同的寻址来提取数据。

从一些 HTML 开始。请注意 Perl 5.26 中引入的花哨的新缩进文档语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use v5.28;
use utf8;
use strict;
use warnings;

use Mojo::DOM;

my $selector = $ARGV[0] // 'img';

my $html =<<~'HTML';

<img id="farnworth " class="human" src="..." />
<ul class="employees">
<li><img id="bender" class="robot" src="..." /></li>
<li><img id="fry" class="human" src="..." /></li>
<li><img id="leela" class="mutant" src="..." /></li>
</ul>
HTML

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

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

不用参数运行这个程序,我看到所有的 img 标签:

1
2
3
4
5
$ perl html.pl
<img class="human" id="farnworth " src="...">
<img class="robot" id="bender" src="...">
<img class="human" id="fry" src="...">
<img class="mutant" id="leela" src="...">

通过论证我可以选择我喜欢的任何部分。在这里,我得到以 li 标签开头的部件:

1
2
3
4
$ perl html.pl li
<li><img class="robot" id="bender" src="..."></li>
<li><img class="human" id="fry" src="..."></li>
<li><img class="mutant" id="leela" src="..."></li>

我可以选择具有某个类的所有图像:

1
2
3
$ perl html.pl img.human
<img class="human" id="farnworth " src="...">
<img class="human" id="fry" src="...">

但是,如果我只想要列表中的人类图像怎么办?我必须努力工作。我指定了一个复合选择器,它指出 img 必须在 li 标签中:

1
2
$ perl html.pl "li img.human"
<img class="human" id="fry" src="...">

想象一下,更复杂的 HTML 与其他列表也有图像?我可以添加另一个选择器,说它必须在某种 ul 标签中:

1
2
$ perl html.pl "ul.employees li img.human"
<img class="human" id="fry" src="...">

如果这些标签之间没有任何内容。我可以将选择器与 > 连接,以表示那些应该是直接的孩子而不是后代:

1
2
$ perl html.pl "ul.employees > li > img.human"
<img class="human" id="fry" src="...">

现在,考虑一下我在那里做了多少工作。几乎没有。我创建了一个 DOM 对象,应用了一个选择器,并且我已经隔离了部分数据。这与我以前做过的努力是一回事。这种方式更好,而不是更多的工作。这就是我喜欢 Mojolicious 的原因!

那些新的表情符号怎么样?

在 Perl v5.26 中写关于 Unicode 9 更新时,我想知道我可以展示哪些内容可能很有趣。怎么弄清楚哪个新的表情符号出现了?

我的第一次尝试只是遍历每个字符并比较各种 Unicode 属性,以查看哪些代码编号从 Unassigned 更改为 Present_In。那很好,但后来我发现有人已经列出了所有新的表情符号,我可以抓取他们的网站。

我不会解释这个程序中的所有内容。相信我使用 Mojo::UserAgent 来获取数据,提取 DOM,并使用复合选择器 ul:not( [class] ) li a 找到我想要的文本。其余的仅仅是对提取的列表进行转换。那些 mapjoin 来自 Mojo::Collection。这比使用正则表达式更容易实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use v5.28;
use utf8;
use strict;
use warnings;
use open qw(:std :utf8);
use charnames qw();

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

my $url = 'https://blog.emojipedia.org/new-unicode-9-emojis/';
my $tx = $ua->get( $url );

die "That didn't work!\n" if $tx->error;

say $tx->result
->dom
->find( 'ul:not( [class] ) li a' )
->map( 'text' )
->map( sub {
my $c = substr $_, 0, 1;
[ $c, ord($c), charnames::viacode( ord($c) ) ]
})
->sort( sub { $a->[1] <=> $b->[1] } )
->map( sub {
sprintf '%s (U+%05X) %s', $_->@*
} )
->join( "\n" );

这是一个很好的列表,如下所示:

1
2
3
4
5
6
7
🕺 (U+1F57A) MAN DANCING
🖤 (U+1F5A4) BLACK HEART
🛑 (U+1F6D1) OCTAGONAL SIGN
🛒 (U+1F6D2) SHOPPING TROLLEY
🛴 (U+1F6F4) SCOOTER
🛵 (U+1F6F5) MOTOR SCOOTER
🛶 (U+1F6F6) CANOE

我使用相同的程序来查找 v5.28 中的 Unicode 10 更新

从表中提取列

没有留下深刻印象?用 CSS 选择器切表怎么样?这是一个包含ID,名称和分数列的短表。我想加总所有的分数。

我不害怕用正则表达式做这个(再次强调!)但是 Mojo::DOM 更容易。复合选择器按类查找表,选择每一行,并按位置寻址表格单元格(在本例中为::last-child):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
use v5.26;
use utf8;
use strict;
use warnings;

use List::Util qw(sum);
use Mojo::DOM;

my $html = <<~'HTML';
<table class="scores">
<tr><th>ID</th><th>Name</th><th>Score</th></tr>

<tr><td>1</td> <td>Nibbler</td> <td>1023</td></tr>
<tr><td>27</td><td>Scruffy</td> <td>39</td> </tr>
<tr><td>5</td> <td>Zoidberg</td><td>5834</td></tr>
</table>
HTML

my @scores = Mojo::DOM->new( $html )
->find( 'table.scores > tr > td:last-child' )
->map( 'text' )
->each
;

my $grand = sum( @scores );
say "Grand total: $grand";

结论

即使对于像我这样的老程序员来说,通过 Mojolicious 应用的 CSS 选择器处理 HTML 比我之前做的要容易得多(这比过去做得简单得多)。通过创建复合选择器的一点技巧,我可以得到我想要的任何部分。