《高阶Perl》——1.7 HTML

1.7 HTML

我曾承诺递归对操作层次化定义的数据结构有用,我还用了文件系统作为一个例子。但它是个稍微特殊的数据结构的例子,因为通常认为数据结构是在内存里,而不是在磁盘上。

文件系统的树型结构让人联想到目录,每个目录都含有一列其他文件。任何拥有一些包含其他条目列表的领域都将含有树型结构。一个极好的例子是HTML数据。

HTML数据是一系列元素和普通文本。每个元素含有一些内容,它们又是一系列更多的元素和更多的普通文本。这是个递归的描述,类似文件系统的描述,HTML文件的结构也类似于文件系统的结构。

元素有一个开始标签(start tag),如下:

<font>

与相应的结束标签(end tag),如下:

</font>

开始标签可以有一组 属性-值对(attribute杤alue pair),如下:

<font size=3 color="red">

结束标签总是一样的。它没有属性-值对。

在开始标签和结束标签之间可以存在任何HTML文本序列,包括其他元素,也包括普通文本。这里有一个简单的HTML文件例子:

<h1>What Junior Said Next</h1>

<p>But I don't <font size=3 color="red">want</font>
to go to bed now!</p>

这个文件的结构如图1-3所示:

文件主要有三个组成部分:<h1>元素与它的内容;<p>元素与它的内容;以及它们之间的空行。<p>元素依次有三个组成部分:在<font>元素之前没有标记的文本;<font>元素与它的内容;以及<font>元素之后未标记的文本。<h1>元素有一个组成部分,就是未标记的文本What Junior Said Next。

第8章将介绍如何建立一个针对类似HTML语言的解析器。此刻将考察一个半标准的模块HTML::TreeBuilder,它把一个HTML文件转换成一个树型结构。

假设HTML数据已经在一个变量里了,如$html。下面的代码使用HTML::TreeBuilder把文本转换成一个清晰的树型结构:

use HTML::TreeBuilder;
my $tree = HTML::TreeBuilder->new;
$tree->ignore_ignorable_whitespace(0);
$tree->parse($html);
$tree->eof();

方法ignore_ignorable_whitespace()告诉HTML::TreeBuilder不允许丢弃某些空白符,如<h1>元素后的换行符,正常情况下这是可忽略的。

现在$tree表示树型结构。它是散列树,每个散列是树的一个节点并表示一个元素。每个散列有一个键_tag,它的值是它的标签名;还有一个键_content,它的值是元素内容依次排列的一个列表;_content列表中的每个条目或者是一个字符串,表示没有标签的文本,或者是另一个散列,表示另一个元素。如果标签还有属性-值对,那它们都直接存放在散列中,属性作为散列的键,相应的值作为散列的值。

例如,与例子中的<font>元素对应的树节点如下:

{ _tag => "font",
  _content => [ "want" ],
  color => "red",
  size => 3,
}

<p>元素对应的树节点包含<font>节点,如下:

{ _tag => "p",
  _content => [ "But I don't ",
                { _tag => "font",
                  _content => [ "want" ],
                  color => "red",
                  size => 3,
                },
                " to go to bed now!",
              ],
}

建立一个函数遍历这些HTML树之一并为所有文本“去标签”,即剥离标签,并不困难。对于_content列表中的每个条目,都可以通过ref()函数把它识别成一个元素,前者对元素(即散列引用)产生真,对普通字符串则是假:

### Code Library: untag-html
sub untag_html {
  my ($html) = @_;
  return $html unless ref $html; # It's a plain string

  my $text = '';
  for my $item (@{$html->{_content}}) {
    $text .= untag_html($item);
  }

  return $text;
}

函数检查传入的HTML条目是否是一个普通的字符串,如果是,函数立即返回它。如果它不是一个普通的字符串,函数假设它是一个树节点,如前所述,并迭代它的内容,递归地把每个条目转换成普通文本,累积成结果字符串并返回它。对于这个例子,就是:

What Junior Said Next But I don't want to go to bed now!

Sean Burke,HTML::TreeBuilder的作者,告诉我如此获得HTML::TreeBuilder对象的内部信息是不规范的,因为他可能在未来改变它们。健壮的程序应该使用模块提供的访问器方法。在这些例子中,将继续直接获取内部信息。
可以向dir_walk()学习,通过把这个函数分成两部分而使它更有用:一部分处理HTML树,另一部分处理累积普通文本的专门任务:

### Code Library: walk-html
sub walk_html {
  my ($html, $textfunc, $elementfunc) = @_;
  return $textfunc->($html) unless ref $html; # It's a plain string

  my @results;
  for my $item (@{$html->{_content}}) {
    push @results, walk_html($item, $textfunc, $elementfunc);
  }
  return $elementfunc->($html, @results);
}

这个函数的结构和dir_walk()的完全一样。它以两个辅助的函数为参数:$textfunc计算一个普通文本字符串的某个有意思的值,$elementfunc接受元素与其间条目的值计算与一个元素相对应的值。$textfunc类似dir_walk()中的$filefunc,$elementfunc类似$dirfunc。

现在可以把剥离器写成如下这样:

walk_html($tree, sub { $_[0] },
                 sub { shift; join '', @_ });

参数$textfunc是一个函数,它原封不动地返回它的参数。参数$elementfunc是一个函数,它丢弃元素本身,然后连接为它的内容计算得到的文本,并返回连接的文本。输出和untag_html()的一样。

假设我们想要一个文件摘要,输出在<h1>标签内的文本,而丢弃其他东西:

sub print_if_h1tag {
  my $element = shift;
  my $text = join '', @_;
  print $text if $element->{_tag} eq 'h1';
  return $text;
}
walk_html($tree, sub { $_[0] }, \&print_if_h1tag);

这本质上和untag_html()一样,除了当元素函数看到它正在处理一个<h1>元素时,它就输出未标记的文本。
如果期望函数返回(return)头部文本而不是输出它,那么必须用点小技巧。考虑一个这样的例子:

<h1>Junior</h1>
Is a naughty boy.

最好丢弃文本Is a naughty boy,这样它就不出现在结果中了。但是对于walk_html(),它只不过是另一个普通文本条目,和Junior看起来完全一样,而后者是不想丢弃的。也许应当简单地丢弃出现在非头部标签中的所有东西,但是这样行不通:

<h1>The story of <b>Junior</b></h1>

不能仅由于Junior出现在<b>标签里而丢弃它,因为<b>标签本身也在<h1>标签里,但是想保留它。

可以这样解决这个问题:从每个walk_html()的执行体把有关当前标签的上下文的信息传递到下一个执行体,但它以其他方式传回信息更简单。文件中的文本或者是“该留的”,因为知道它在一个<h1>元素内,或者是“也许该留的”,因为我们不知道。每当处理一个<h1>元素时,就将把它包含的所有“也许该留的”文本提升为“该留的”文本。最后,将输出“该留的”文本并丢弃“也许该留的”文本:

### Code Library: extract-headers
@tagged_texts = walk_html($tree, sub { ['MAYBE', $_[0]] },
                                 \&promote_if_h1tag);
sub promote_if_h1tag {
  my $element = shift;
  if ($element->{_tag} eq 'h1') {
    return ['KEEPER', join '', map {$_->[1]} @_];
  } else {
    return @_;
  }
}

从walk_htm()返回的值将是一列带标记的文本条目。每个文本条目是一个匿名数组,它的第一个元素是MAYBE或者KEEPER,而第二个条目是一个字符串。普通文本函数简单地标记它的参数为MAYBE。对于字符串Junior,它返回带标记的条目['MAYBE', 'Junior'];对于字符串Is a naughty boy.,它返回['MAYBE', 'Is a naughty boy.']。

元素函数更有趣。它得到一个元素和一列带标记的文本条目。如果元素表现为一个<h1>标签,那么函数从它的其他参数中抽取所有的文本,合并到一起,并把结果标记为KEEPER。如果元素是其他种类,函数原封不动地返回它的标签文本。这些文本将插入带标记文本的列表,然后传递给元素函数调用,作为上一层元素,比较这个与1.5节中最后的dir_walk()例子,后者以类似方式返回一列文件名。

因为最后从walk_html()返回的值是标记文本的列表,所以需要过滤它们并丢弃仍然标记为MAYBE的那些。这最后一步是不能省略的。由于函数区别对待顶层的和嵌入在<h1>标签内的不带标签的文本条目,因此就必须有某部分过程能知晓在顶层的东西。walk_html()无法做到,因为它在每层做同样的事情。所以必须建立一个最终的函数处理顶层:

sub extract_headers {
  my $tree = shift;
  my @tagged_texts = walk_html($tree, sub { ['MAYBE', $_[0]] },
                                      \&promote_if_h1tag);
  my @keepers = grep { $_->[0] eq 'KEEPER'} @tagged_texts;
  my @keeper_text = map { $_->[1] } @keepers;
  my $header_text = join '', @keeper_text;
  return $header_text;
}

或者可以写得更紧凑:

sub extract_headers {
  my $tree = shift;
  my @tagged_texts = walk_html($tree, sub { ['MAYBE', $_[0]] },
                                      \&promote_if_h1tag);
  join '', map { $_->[1] } grep { $_->[0] eq 'KEEPER'} @tagged_texts;
}

刚才看到了如何从HTML文件中抽取所有<h1>标签的文本。主要的过程是promote_if_h1tag()。但是下次也许会想要抽取更详细的摘要,包括来自<h1><h2><h3>以及其他存在的标签的所有文本。为做到这个,需要对promote_if_h1tag()做个小改动,把它变成一个新的函数:

sub promote_if_h1tag {
  my $element = shift;
  if ($element->{_tag} =~ /^h\d+$/) {
    return ['KEEPER', join '', map {$_->[1]} @_];
  } else {
    return @_;
  }
}

但是如果promote_if_h1tag能比最初意识到的更普遍适用,那将是个提取普遍有用的部分的好方法。可以把变化的部分参数化以达到此目的:

### Code Library: promote-if
sub promote_if {
  my $is_interesting = shift;
  my $element = shift;
  if ($is_interesting->($element->{_tag})) {
    return ['KEEPER', join '', map {$_->[1]} @_];
  } else {
    return @_;
  }
}

现在不必写个专门的函数promote_if_h1tag()了,可以把同样的行为表现成promote_if()的一个特殊情况。不用写成:

my @tagged_texts = walk_html($tree, sub { ['MAYBE', $_[0]] },
                                           \&promote_if_h1tag);

可以用这个:

my @tagged_texts = walk_html($tree,
                              sub { ['MAYBE', $_[0]] },
                              sub { promote_if(
                                       sub { $_[0] eq 'h1'},
                                       @_)
                              });

第7章将介绍完成此任务的更整齐的方式。

时间: 2024-09-18 06:20:01

《高阶Perl》——1.7 HTML的相关文章

《高阶Perl》——3.2 内联缓存

3.2 内联缓存 给一个函数添加缓存的最直接的方式就是给函数一个私有的散列.在这个例子里,可以使用一个数组代替散列,因为fib()的参数总是一个非负整数.但是一般需要使用一个散列,那么将会看到: ### Code Library: fib-cached # Compute the number of pairs of rabbits alive in month n { my %cache; sub fib { my ($month) = @_; unless (exists $cache{$m

《高阶Perl》——3.5 MEMOIZE模块

3.5 MEMOIZE模块 本书不是关于Perl模块的内部细节的,而是关于一些Memoize模块内部使用的并和稍后要做的事情有直接联系的技术,所以现在简短地介绍一下. Memoize得到一个函数名(或引用)作为它的参数.它制造一个新的函数,后者维护一个缓存并在其中查找它的参数.如果新的函数在缓存里找到了参数,就返回缓存的值:如果没找到,就调用原始函数,把返回的值保存入缓存,并把它返回给原始的主调者. 制造完这个新的函数,Memoize就把它装入Perl的符号表以代替原始的函数,那样当你认为你在调

《高阶Perl》——3.10 可供选择的记忆术

3.10 可供选择的记忆术 大多数纯函数提供一个缓存的机会.尽管乍一看纯函数很少,它们只以一定频率出现.纯函数特别普遍的地方是在排序中用做比较器函数. Perl内置的sort操作符是通用的,它可以把一列任何种类的数据以程序要求的任何次序排序.默认状态下,它把一列字符串以字母表次序排序,但是程序员可以任意提供一个比较器函数(comparator function),告诉Perl怎样重排sort的参数列表.比较器函数被反复调用,每次带有待排序列表中的两个不同元素,如果两个元素次序正确,就必须返回一个

《高阶Perl》——3.8 对象方法里的缓存

3.8 对象方法里的缓存 对象方法,它经常不理解地把缓存的值保存在独立的散列里.考虑一个投资银行写的程序里的Investor对象.该对象表现了银行的一个客户: package Investor; # Compute total amount currently invested sub total { my $self = shift; # ... complex computation performed here ... return $total; } 如果$total不会改变,就可以缓存

《高阶Perl》——3.7 键的生成

3.7 键的生成 先前的记忆器至少有一个严重的问题.它需要把函数的参数转变成一个散列键,它的做法是使用join: my $key = join ',', @_; 这对只带一个参数的函数有效,也对参数不含逗号的函数有效,包括所有的参数是数字的函数.但是如果函数的参数可能包含逗号,它可能失效,因为以下两个调用却计算出了相同的键: func("x,", "y"); func("x", ",y"); 第一次调用,返回的值将以键&quo

《高阶Perl》——1.2 阶乘

1.2 阶乘 假设有一个有n个不同条目的列表.为了具体点,假设这些条目是字母表的字母.这样一个列表有多少种不同的排列次序呢?显然,因为答案依赖于n,所以它是n的函数.这个函数称为阶乘函数(factorial function).n的阶乘就是n个不同条目的不同的排列次序的数量.通常以一个后缀!标记,这样n的阶乘就是n!.一般不同的次序称为排列(permutation). 下面计算一些阶乘.显然,只有一个条目的列表只有一种排列,所以1!=1.两个条目的列表有两种排列:A-B和B-A.少许笔算可以发现

《高阶Perl》——3.12 速度的好处

3.12 速度的好处 此刻有个说法比较诱人,那就是记忆术只比手动缓存技术改善了一点,因为它做了同样的事情,仅有的额外的好处是你可以快速启动或者关闭它.但这不完全正确.当工具之间的速度和便利的差别够大,它会改变你思考和使用工具的方式.自动地使一个函数带记忆仅需要消耗手动书写代码的1/100的时间.这和飞机与牛车的速度差别一样.说飞机只是更快的牛车就忽略了本质:量变如此巨大以致它也变成了实质的质变. 例如,有了自动的记忆术,就有可能为函数增加缓存行为而不必预先仔细考虑性能细节.记忆术这么简单以至于可

《高阶Perl》——1.4 层次化数据

1.4 层次化数据 已经介绍的例子显示了递归过程的风采,但是它们遗漏了重要的一点.在介绍汉诺塔问题时,我说过当你想解决的问题可以简化成同样问题的更简形式时,递归是很有用的.但是这样的问题是普遍的这一点可能不清楚. 多数递归的函数是为处理递归的数据结构的建立.一个递归的数据结构就像一个列表.一棵树或者一个堆,它们根据相同数据结构的更小的实例定义而成.最常见的例子可能是文件系统目录结构.文件可以是: 文件可以是一个目录,含有一列文件,其中的一些可以是目录,再含有另一些文件,以此类推.处理这样的结构最

《高阶Perl》——3.11 传播福音

3.11 传播福音 如果你正尝试向一个C程序员解释为什么Perl好,自动的记忆术是一个好例子.几乎所有的程序员都熟悉缓存技术.即使他们没在自己的程序里使用过任何缓存技术,他们也一定熟悉这个概念,来自网页浏览器里的,他们的计算机的缓存内存里的,DNS服务器里的,他们的网页代理服务器里的,或别的地方的缓存.缓存,就像大多数简单实用的主意,是无处不在的. 增加缓存不是非常麻烦,但至少需要几分钟修改代码.算上所有修改,你有可能犯错,这不得不计算进平均时间,一旦你完成了,可能发现缓存是个糟糕的主意,因为缓

《高阶Perl》——3.4 记忆术

3.4 记忆术 给函数添加缓存技术代码不是非常困难的.且已经看到,需要的改变对任何函数几乎一样.那么,为什么不让计算机做这些呢?若告诉Perl想要使一个函数具有缓存行为.Perl应该能自动地执行所需的转换.这样的给函数添加缓存行为的自动的转换就称为记忆术(memoization),函数则称为带记忆的(memoized). 标准的Memoize模块就是做这个的.如果Memoize模块可用,就完全不必重写fib代码.可以简单地在程序的顶部添加两行: ### Code Library: fib-aut