File Coverage

blib/lib/HTML/Summary.pm
Criterion Covered Total %
statement 109 121 90.0
branch 22 32 68.7
condition 20 24 83.3
subroutine 18 20 90.0
pod 4 4 100.0
total 173 201 86.0


foobar

you should get
line stmt bran cond sub pod time code
1             package HTML::Summary;
2             $HTML::Summary::VERSION = '0.022';
3             #==============================================================================
4             #
5             # Start of POD
6             #
7             #==============================================================================
8              
9             =head1 NAME
10              
11             HTML::Summary - generate a summary from a web page
12              
13             =head1 SYNOPSIS
14              
15             use HTML::Summary;
16             use HTML::TreeBuilder;
17            
18             my $tree = HTML::TreeBuilder->new;
19             $tree->parse( $document );
20              
21             my $summarizer = HTML::Summary->new(
22             LENGTH => 200,
23             USE_META => 1,
24             );
25              
26             $summary = $summarizer->generate( $tree );
27             $summarizer->option( 'USE_META' => 1 );
28             $length = $summarizer->option( 'LENGTH' );
29             if ( $summarizer->meta_used() ) {
30             # do something
31             }
32              
33             =head1 DESCRIPTION
34              
35             The C module produces summaries from the textual content of
36             web pages. It does so using the location heuristic, which determines the value
37             of a given sentence based on its position and status within the document; for
38             example, headings, section titles and opening paragraph sentences may be
39             favoured over other textual content. A LENGTH option can be used to restrict
40             the length of the summary produced.
41              
42             =head1 CONSTRUCTOR
43              
44             =head2 new( $attr1 => $value1 [, $attr2 => $value2 ] )
45              
46             Possible attributes are:
47              
48             =over 4
49              
50             =item VERBOSE
51              
52             Generate verbose messages to STDERR.
53              
54             =item LENGTH
55              
56             Maximum length of summary (in bytes). Default is 500.
57              
58             =item USE_META
59              
60             Flag to tell summarizer whether to use the content of the C<> tag
61             in the page header, if one is present, instead of generating a summary from the
62             body text. B if the USE_META flag is set, this overrides the LENGTH
63             flag - in other words, the summary provided by the C<> tag is
64             returned in full, even if it is greater than LENGTH bytes. Default is 0 (no).
65              
66             =back
67              
68             my $summarizer = HTML::Summary->new(LENGTH => 200);
69              
70             =head1 METHODS
71              
72             =head2 option( )
73              
74             Get / set HTML::Summary configuration options.
75              
76             my $length = $summarizer->option( 'LENGTH' );
77             $summarizer->option( 'USE_META' => 1 );
78              
79             =head2 generate( $tree )
80              
81             Takes an HTML::Element object, and generates a summary from it.
82              
83             my $tree = HTML::TreeBuilder->new;
84             $tree->parse( $document );
85             my $summary = $summarizer->generate( $tree );
86              
87             =head2 meta_used( )
88              
89             Returns 1 if the META tag description was used to generate the summary.
90              
91             if ( $summarizer->meta_used() ) {
92             # do something ...
93             }
94              
95             =head1 SEE ALSO
96              
97             L,
98             L,
99             L,
100             L.
101              
102             =head1 REPOSITORY
103              
104             L
105              
106             =head1 AUTHORS
107              
108             This module was originally whipped up by Neil Bowers and Tony Rose.
109             It was then developed and maintained by Ave Wrigley and Tony Rose.
110              
111             Neil Bowers is currently maintaining the HTML-Summary distribution.
112              
113             Neil Bowers Eneilb@cpan.orgE
114              
115             =head1 COPYRIGHT AND LICENSE
116              
117             Copyright (c) 1997 Canon Research Centre Europe (CRE). All rights reserved.
118              
119             This is free software; you can redistribute it and/or modify it under
120             the same terms as the Perl 5 programming language system itself.
121              
122             =cut
123              
124             #==============================================================================
125             #
126             # End of POD
127             #
128             #==============================================================================
129              
130             #==============================================================================
131             #
132             # Pragmas
133             #
134             #==============================================================================
135              
136             require 5.006;
137 2     2   1378 use strict;
  2         2  
  2         44  
138 2     2   4 use warnings;
  2         2  
  2         43  
139              
140             #==============================================================================
141             #
142             # Modules
143             #
144             #==============================================================================
145              
146 2     2   653 use Text::Sentence qw( split_sentences );
  2         4  
  2         124  
147 2     2   696 use Lingua::JA::Jtruncate qw( jtruncate );
  2         5  
  2         113  
148              
149             #==============================================================================
150             #
151             # Constants
152             #
153             #==============================================================================
154              
155 2     2   9 use constant IGNORE_TEXT => 1;
  2         2  
  2         2140  
156              
157             #==============================================================================
158             #
159             # Private globals
160             #
161             #==============================================================================
162              
163             my $DEFAULT_SCORE = 0;
164              
165             my %ELEMENT_SCORES = (
166             'p' => 100,
167             'h1' => 90,
168             'h2' => 80,
169             'h3' => 70,
170             );
171              
172             my %DEFAULTS = (
173             'USE_META' => 0,
174             'VERBOSE' => 0,
175             'LENGTH' => 500,
176             );
177              
178             #==============================================================================
179             #
180             # Public methods
181             #
182             #==============================================================================
183              
184             #------------------------------------------------------------------------------
185             #
186             # new - constructor. Configuration through "hash" type arguments, i.e.
187             # my $abs = HTML::Summary->new( VAR1 => 'foo', VAR2 => 'bar' );
188             #
189             #------------------------------------------------------------------------------
190              
191             sub new
192             {
193 25     25 1 549033 my $class = shift;
194 25         60 my $self = bless { }, $class;
195 25         90 return $self->_initialize( @_ );
196             }
197              
198             #------------------------------------------------------------------------------
199             #
200             # generate - main public interface method to generate a summary
201             #
202             #------------------------------------------------------------------------------
203              
204             sub generate
205             {
206 26     26 1 1978 my $self = shift;
207 26         26 my $tree = shift;
208              
209 26         29 my $summary;
210              
211 26         61 $self->_verbose( 'Generate summary ...' );
212              
213             # check to see if there is a summary already defined in a META tag ...
214              
215 26 100 100     92 if (
216             $self->{ USE_META } and
217             $summary = $self->_get_summary_from_meta( $tree )
218             )
219             {
220 1         3 $self->_verbose( "use summary from META tag ..." );
221 1         3 $self->_verbose( $summary );
222 1         2 return $summary;
223             }
224              
225             # traverse the HTML tree, building up @summary array
226              
227 25         75 my @summary = $self->_get_summary( $tree );
228              
229             # sort @summary by score, truncate if it is greater than LENGTH
230             # characters, and the re-sort by original order. Truncate AFTER the LENGTH
231             # has been exceeded, so that last sentence is truncated later by
232             # jtruncate
233              
234 25         157 @summary = sort { $b->{ score } <=> $a->{ score } } @summary;
  1261         930  
235              
236 25         29 my $tot_length = 0;
237 25         40 my @truncated = ();
238              
239 25         51 for ( @summary )
240             {
241 135         101 push( @truncated, $_ );
242 135 100       238 last if ( $tot_length += $_->{ 'length' } ) > $self->{ LENGTH };
243             }
244 25         31 @truncated = sort { $a->{ order } <=> $b->{ order } } @truncated;
  236         192  
245              
246             # these whitespaces will push the length over LENGTH, but jtruncate
247             # should take care of this
248              
249 25         32 $summary = join( ' ', map { $_->{ text } } @truncated );
  135         189  
250 25         70 $self->_verbose( "truncate the summary to ", $self->{ LENGTH } );
251 25         97 $summary = jtruncate( $summary, $self->{ LENGTH } );
252 25         278 return $summary;
253             }
254              
255             #------------------------------------------------------------------------------
256             #
257             # meta_used - tells whether the description from the META tag was used; returns
258             # 1 if it was, 0 if the summary was generated automatically
259             #
260             #------------------------------------------------------------------------------
261              
262             sub meta_used
263             {
264 0     0 1 0 my $self = shift;
265              
266 0         0 return $self->{ META_USED };
267             }
268              
269             #------------------------------------------------------------------------------
270             #
271             # option - get / set configuration option
272             #
273             #------------------------------------------------------------------------------
274              
275             sub option
276             {
277 0     0 1 0 my $self = shift;
278 0         0 my $option = shift;
279 0         0 my $val = shift;
280              
281 0 0       0 die "No HTML::Summary option name given" unless defined $option;
282             die "$option is not an HTML::Summary option" unless
283 0 0       0 grep { $_ eq $option } keys %DEFAULTS
  0         0  
284             ;
285              
286 0 0       0 if ( defined $val )
287             {
288 0         0 $self->{ $option } = $val;
289             }
290              
291 0         0 return $self->{ $option } = $val;
292             }
293              
294             #==============================================================================
295             #
296             # Private methods
297             #
298             #==============================================================================
299              
300             #------------------------------------------------------------------------------
301             #
302             # _initialize - supports sub-classing
303             #
304             #------------------------------------------------------------------------------
305              
306             sub _initialize
307             {
308 25     25   36 my $self = shift;
309              
310 25 50       103 return undef unless @_ % 2 == 0; # check that config hash has even no.
311             # of elements
312              
313 25         76 %{ $self } = ( %DEFAULTS, @_ ); # set options from defaults / config.
  25         111  
314             # hash passed as arguments
315              
316 25         91 return $self;
317             }
318              
319             #------------------------------------------------------------------------------
320             #
321             # _verbose - generate verbose error messages, if the VERBOSE option has been
322             # selected
323             #
324             #------------------------------------------------------------------------------
325              
326             sub _verbose
327             {
328 867     867   688 my $self = shift;
329              
330 867 50       1474 return unless $self->{ VERBOSE };
331 0         0 print STDERR @_, "\n";
332             }
333              
334             #------------------------------------------------------------------------------
335             #
336             # _get_summary - get sentences from an element to generate the summary from.
337             # Uses lexically scoped array @sentences to build up result from the traversal
338             # callback
339             #
340             #------------------------------------------------------------------------------
341              
342             sub _get_summary
343             {
344 25     25   27 my $self = shift;
345 25         28 my $tree = shift;
346              
347 25         47 my @summary = ();
348             my $add_sentence = sub {
349 470     470   329 my $text = shift;
350 470         300 my $tag = shift;
351 470   66     808 my $score = shift || $DEFAULT_SCORE;
352              
353 470 100       967 return unless $text =~ /\w/;
354              
355 440         839 $text =~ s!^\s*!!; # remove leading ...
356 440         3047 $text =~ s!\s*$!!; # ... and trailing whitespace
357              
358 440         1440 $summary[ scalar( @summary ) ] = {
359             'text' => $text,
360             'length' => length( $text ),
361             'tag' => $tag,
362             'score' => $score,
363             'order' => scalar( @summary ),
364             };
365 25         133 };
366             $tree->traverse(
367             sub {
368 1339     1339   12146 my $node = shift;
369 1339         918 my $flag = shift;
370              
371 1339 100       1666 if ( $flag ) # entering node ...
372             {
373 1001         1403 my $tag = $node->tag;
374 1001 100       3678 return 0 if $tag eq 'head';
375              
376             # add sentences which either are scoring, or span no other
377             # scoring sentences (and have a score of 0). In this way, all
378             # text is captured, even if it scores 0; the only exception is
379             # something like some text

foobar

, where
380             # everything but "foobar" will be lost. However, if you have
381             # some text
382             # all the text.
383              
384 976 100 100     1856 if (
385             $ELEMENT_SCORES{ $tag } ||
386             ! _has_scoring_element( $node )
387             )
388             {
389 638         735 my $text = _get_text( $node );
390 638         767 foreach ( $text ) # alias $_ to $text
391             {
392             # get rid of whitespace (including  ) from start /
393             # end of $text
394 638         1656 s/^[\s\160]*//;
395 638         3909 s/[\s\160]*$//;
396             # get rid of any spurious tags that have slipped
397             # through the HTML::TreeBuilder
398 638         807 s!<[^>]+>!!g;
399             }
400              
401 638 100       1146 if ( $text =~ /\S/ )
402             {
403 344   66     639 my $score = $ELEMENT_SCORES{ $tag } || $DEFAULT_SCORE;
404              
405             # add all the sentences in the text. Only the first
406             # sentence gets the element score - the rest get the
407             # default score
408              
409 344         780 $self->_verbose( "TEXT: $text" );
410 344         922 for my $sentence (
411             split_sentences( $text, $self->{ 'LOCALE' } )
412             )
413             {
414 470         902 $self->_verbose( "SENTENCE: $text" );
415 470         597 $add_sentence->( $sentence, $tag, $score );
416 470         569 $score = $DEFAULT_SCORE;
417             }
418             }
419              
420             # return 0 to avoid getting the same sentence in a scoring
421             # "daughter" element
422              
423 638         1241 return 0;
424             }
425             }
426              
427             # continue traversal ...
428              
429 676         836 return 1;
430             },
431 25         170 IGNORE_TEXT
432             );
433 25         444 return @summary;
434             }
435              
436             #------------------------------------------------------------------------------
437             #
438             # _get_summary_from_meta - check to see if there is already a summary
439             # defined in the META tag in the HEAD
440             #
441             #------------------------------------------------------------------------------
442              
443             sub _get_summary_from_meta
444             {
445 2     2   2 my $self = shift;
446 2         1 my $tree = shift;
447              
448 2         2 my $summary;
449              
450             $tree->traverse(
451             sub {
452 23     23   276 my $node = shift;
453 23         16 my $flag = shift;
454              
455 23 50 100     29 if ($node->tag eq 'meta'
      66        
      66        
456             && defined($node->attr('name'))
457             && lc( $node->attr('name') ) eq 'description'
458             && defined($node->attr('content')))
459             {
460 1         29 $summary = $node->attr( 'content' );
461 1 50       9 $summary = undef if $summary eq 'content';
462 1         2 return 0;
463             }
464 22         109 return 1;
465             },
466 2         15 IGNORE_TEXT
467             );
468              
469 2 100       24 $self->{ META_USED } = defined( $summary ) ? 1 : 0;
470 2         7 return $summary;
471             }
472              
473             #==============================================================================
474             #
475             # Private functions
476             #
477             #==============================================================================
478              
479             #------------------------------------------------------------------------------
480             #
481             # _get_text - get all the text spanned by an element. Uses lexically scoped
482             # variable $html to build up result from the traversal callback
483             #
484             #------------------------------------------------------------------------------
485            
486             sub _get_text
487             {
488 638     638   535 my $node = shift;
489            
490 638         474 my $html = '';
491             $node->traverse(
492             sub {
493 7356     7356   60734 my $node = shift;
494 7356 100       8983 $html .= $node unless ref( $node );
495 7356         7985 return 1;
496             }
497 638         1863 );
498 638         5794 return $html;
499             }
500              
501             #------------------------------------------------------------------------------
502             #
503             # _has_scoring_element - check to see if this element spans any scoring
504             # element. Uses lexically scoped variable $has_scoring_element to build up
505             # result from the traversal callback.
506             #
507             #------------------------------------------------------------------------------
508              
509             sub _has_scoring_element
510             {
511 518     518   414 my $node = shift;
512            
513 518         349 my $has_scoring_element = 0;
514             $node->traverse(
515             sub {
516 34325     34325   322505 my $node = shift;
517 34325         38967 my $tag = $node->tag;
518 34325   100     110618 $has_scoring_element ||= $ELEMENT_SCORES{ $tag };
519 34325         35607 return 1;
520             },
521 518         1718 IGNORE_TEXT
522             );
523 518         5772 return $has_scoring_element;
524             }
525              
526             #==============================================================================
527             #
528             # Return TRUE
529             #
530             #==============================================================================
531              
532             1;