File Coverage

blib/lib/WWW/Sitemap.pm
Criterion Covered Total %
statement 125 173 72.2
branch 22 68 32.3
condition 7 39 17.9
subroutine 23 28 82.1
pod 12 16 75.0
total 189 324 58.3


line stmt bran cond sub pod time code
1             package WWW::Sitemap;
2              
3             #==============================================================================
4             #
5             # Start of POD
6             #
7             #==============================================================================
8              
9             =head1 NAME
10              
11             WWW::Sitemap - functions for generating a site map for a given site URL.
12              
13             =head1 SYNOPSIS
14              
15             use WWW::Sitemap;
16             use LWP::UserAgent;
17              
18             my $ua = new LWP::UserAgent;
19             my $sitemap = new WWW::Sitemap(
20             EMAIL => 'your@email.address',
21             USERAGENT => $ua,
22             ROOT => 'http://www.my.com/'
23             );
24              
25             $sitemap->url_callback(
26             sub {
27             my ( $url, $depth, $title, $summary ) = @_;
28             print STDERR "URL: $url\n";
29             print STDERR "DEPTH: $depth\n";
30             print STDERR "TITLE: $title\n";
31             print STDERR "SUMMARY: $summary\n";
32             print STDERR "\n";
33             }
34             );
35             $sitemap->generate();
36             $sitemap->option( 'VERBOSE' => 1 );
37             my $len = $sitemap->option( 'SUMMARY_LENGTH' );
38              
39             my $root = $sitemap->root();
40             for my $url ( $sitemap->urls() )
41             {
42             if ( $sitemap->is_internal_url( $url ) )
43             {
44             # do something ...
45             }
46             my @links = $sitemap->links( $url );
47             my $title = $sitemap->title( $url );
48             my $summary = $sitemap->summary( $url );
49             my $depth = $sitemap->depth( $url );
50             }
51             $sitemap->traverse(
52             sub {
53             my ( $sitemap, $url, $depth, $flag ) = @_;
54             if ( $flag == 0 )
55             {
56             # do something at the start of a list of sub-pages ...
57             }
58             elsif( $flag == 1 )
59             {
60             # do something for each page ...
61             }
62             elsif( $flag == 2 )
63             {
64             # do something at the end of a list of sub-pages ...
65             }
66             }
67             )
68              
69              
70             =head1 DESCRIPTION
71              
72             The C module creates a sitemap for a site, by traversing the
73             site using the WWW::Robot module. The sitemap object has methods to access a
74             list of all the urls in the site, and a list of all the links for each of these
75             urls. It is also possible to access the title of each url, and a summary
76             generated from each url. The depth of each url can also be accessed; the depth
77             is the minimum number of links from the root URL to that page.
78              
79             =head1 CONSTRUCTOR
80              
81             =head2 WWW::Sitemap->new [ $option => $value ] ...
82              
83             Possible option are:
84              
85             =over 4
86              
87             =item USERAGENT
88              
89             User agent used to do the robot traversal. Defaults to LWP::UserAgent.
90              
91             =item VERBOSE
92              
93             Verbose flag, for printing out useful messages during traversal [0|1]. Defaults
94             to 0.
95              
96             =item SUMMARY_LENGTH
97              
98             Maximum length of (automatically generated) summary.
99              
100             =item EMAIL
101              
102             E-Mail address robot uses to identify itself with. This option is required.
103              
104             =item DEPTH
105              
106             Maximum depth of traversal.
107              
108             =item ROOT
109              
110             Root URL of the site for which the sitemap is being created. This option is
111             required.
112              
113             my $sitemap = new WWW::Sitemap(
114             EMAIL => 'your@email.address',
115             USERAGENT => $ua,
116             ROOT => 'http://www.my.com/'
117             );
118              
119             =head1 METHODS
120              
121             =head2 generate( )
122              
123             Method for generating the sitemap, based on the constructor options.
124              
125             $sitemap->generate();
126              
127             =head2 url_callback( sub { ... } )
128              
129             This method allows you to define a callback that will be invoked on every URL
130             that is traversed while generating the sitemap. This is basically to allow
131             bespoke verbose reporting. The callback should be of the form:
132              
133             sub {
134             my ( $url, $depth, $title, $summary ) = @_;
135              
136             # do something ...
137              
138             }
139              
140             =head2 option( $option [ => $value ] )
141              
142             Iterface to get / set options after object construction.
143              
144             $sitemap->option( 'VERBOSE' => 1 );
145             my $len = $sitemap->option( 'SUMMARY_LENGTH' );
146              
147             =head2 root()
148              
149             returns the root URL for the site.
150              
151             my $root = $sitemap->root();
152              
153             =head2 urls()
154              
155             Returns a list of all the URLs on the sitemap.
156              
157             for my $url ( $sitemap->urls() )
158             {
159             # do something ...
160             }
161              
162             =head2 is_internal_url( $url )
163              
164             Returns 1 if $url is an internal URL (i.e. if C<$url =~ /^$root/>.
165              
166             if ( $sitemap->is_internal_url( $url ) )
167             {
168             # do something ...
169             }
170              
171             =head2 links( $url )
172              
173             Returns a list of all the links from a given URL in the site map.
174              
175             my @links = $sitemap->links( $url );
176              
177             =head2 title( $url )
178              
179             Returns the title of the URL.
180              
181             my $title = $sitemap->title( $url );
182              
183             =head2 summary( $url )
184              
185             Returns a summary of the URL - either from the C<> tag
186             or generated automatically using HTML::Summary.
187              
188             my $summary = $sitemap->summary( $url );
189            
190             =head2 depth( $url )
191              
192             Returns the minimum number of links to traverse from the root URL of the site
193             to this URL.
194              
195             my $depth = $sitemap->depth( $url );
196              
197             =head2 traverse( \&callback )
198              
199             The travese method traverses the sitemap, starting at the root node, and
200             visiting each URL in the order that they would be displayed in a sequential
201             sitemap of the site. The callback is called in a number of places in the
202             traversal, indicated by the $flag argument to the callback:
203              
204             =over 4
205              
206             =item $flag = 0
207              
208             Before each set of daughter URLs of a given URL.
209              
210             =item $flag = 1
211              
212             For each URL.
213              
214             =item $flag = 2
215              
216             After each set of daughter URLs of a given URL.
217              
218             =back
219              
220             See the sitemapper.pl script distributed with this module for an example of the
221             use of the traverse method.
222              
223             $sitemap->traverse(
224             sub {
225             my ( $sitemap, $url, $depth, $flag ) = @_;
226             if ( $flag == 0 )
227             {
228             # do something at the start of a list of sub-pages ...
229             }
230             elsif( $flag == 1 )
231             {
232             # do something for each page ...
233             }
234             elsif( $flag == 2 )
235             {
236             # do something at the end of a list of sub-pages ...
237             }
238             }
239             );
240              
241             =head1 SEE ALSO
242              
243             LWP::UserAgent
244             HTML::Summary
245             WWW::Robot
246              
247             =head1 AUTHOR
248              
249             Ave Wrigley EAve.Wrigley@itn.co.ukE
250              
251             =head1 COPYRIGHT
252              
253             Copyright (c) 1997 Canon Research Centre Europe (CRE). All rights reserved.
254             This script and any associated documentation or files cannot be distributed
255             outside of CRE without express prior permission from CRE.
256              
257             =cut
258              
259             #==============================================================================
260             #
261             # End of POD
262             #
263             #==============================================================================
264              
265             #==============================================================================
266             #
267             # Pragmas
268             #
269             #==============================================================================
270              
271             require 5.003;
272 1     1   743 use strict;
  1         2  
  1         27  
273              
274             #==============================================================================
275             #
276             # Modules
277             #
278             #==============================================================================
279              
280 1     1   923 use WWW::Robot;
  1         732636  
  1         38  
281 1     1   1090 use HTML::Summary;
  1         34378  
  1         36  
282 1     1   12 use HTML::TreeBuilder;
  1         1  
  1         13  
283 1     1   30 use Digest::MD5 qw( md5_hex );
  1         2  
  1         70  
284              
285             #==============================================================================
286             #
287             # Public globals
288             #
289             #==============================================================================
290              
291 1     1   6 use vars qw( $VERSION );
  1         2  
  1         2911  
292              
293             $VERSION = '0.002';
294              
295             #==============================================================================
296             #
297             # Private globals
298             #
299             #==============================================================================
300              
301             my %OPTIONS = (
302             'VERBOSE' => 0,
303             'SUMMARY_LENGTH' => 200,
304             'DEPTH' => undef,
305             'EMAIL' => undef,
306             'USERAGENT' => new LWP::UserAgent,
307             'ROOT' => undef,
308             );
309              
310             my %REQUIRED = (
311             'EMAIL' => 1,
312             'ROOT' => 1,
313             );
314              
315             #==============================================================================
316             #
317             # Public methods
318             #
319             #==============================================================================
320              
321             #------------------------------------------------------------------------------
322             #
323             # new - constructor. Configuration through "hash" type arguments, i.e.
324             # my $sitemap = new WWW::Sitemap( VAR1 => 'foo', VAR2 => 'bar' );
325             #
326             #------------------------------------------------------------------------------
327              
328             sub new
329             {
330 1     1 1 606 my $class = shift;
331 1         3 my $self = bless { }, $class;
332 1         8 return $self->initialize( $class, @_ );
333             }
334              
335             #------------------------------------------------------------------------------
336             #
337             # root - returns the root url for the site
338             #
339             #------------------------------------------------------------------------------
340              
341             sub root
342             {
343 2     2 1 48 my $self = shift;
344              
345 2         28 return $self->{ 'ROOT' };
346             }
347              
348             #------------------------------------------------------------------------------
349             #
350             # is_internal_url - returns TRUE if $url is an internal URL, FALSE otherwise
351             #
352             #------------------------------------------------------------------------------
353              
354             sub is_internal_url
355             {
356 45     45 1 51 my $self = shift;
357 45         47 my $url = shift;
358              
359 45         204 return $url =~ /$self->{ ROOT }/;
360             }
361              
362             #------------------------------------------------------------------------------
363             #
364             # urls - returns a list of the URLs in the sitemap
365             #
366             #------------------------------------------------------------------------------
367              
368             sub urls
369             {
370 1     1 1 6 my $self = shift;
371              
372 1         2 return keys %{ $self->{ 'urls' } };
  1         6  
373             }
374              
375             #------------------------------------------------------------------------------
376             #
377             # links - returns a list of the links from a given URL in the sitemap
378             #
379             #------------------------------------------------------------------------------
380              
381             sub links
382             {
383 2     2 1 3 my $self = shift;
384 2         4 my $url = shift;
385              
386 2         2 return keys %{ $self->{ 'link' }{ $url } };
  2         42  
387             }
388              
389             #------------------------------------------------------------------------------
390             #
391             # depth - returns the depth of a given URL
392             #
393             #------------------------------------------------------------------------------
394              
395             sub depth
396             {
397 0     0 1 0 my $self = shift;
398 0         0 my $url = shift;
399              
400 0         0 return $self->{ 'depth' }{ $url };
401             }
402              
403             #------------------------------------------------------------------------------
404             #
405             # title - returns the title of a given URL
406             #
407             #------------------------------------------------------------------------------
408              
409             sub title
410             {
411 1     1 1 7 my $self = shift;
412 1         2 my $url = shift;
413              
414 1         6 return $self->{ 'title' }{ $url };
415             }
416              
417             #------------------------------------------------------------------------------
418             #
419             # summary - returns the summary of a given URL
420             #
421             #------------------------------------------------------------------------------
422              
423             sub summary
424             {
425 1     1 1 2 my $self = shift;
426 1         1 my $url = shift;
427              
428 1         5 return $self->{ 'summary' }{ $url };
429             }
430              
431             #------------------------------------------------------------------------------
432             #
433             # option - get / set configuration option
434             #
435             #------------------------------------------------------------------------------
436              
437             sub option
438             {
439 0     0 1 0 my $self = shift;
440 0         0 my $option = shift;
441 0         0 my $val = shift;
442              
443 0 0       0 die "No WWW::Sitemap option name given" unless defined $option;
444 0         0 die "$option is not an WWW::Sitemap option" unless
445 0 0       0 grep { $_ eq $option } keys %OPTIONS
446             ;
447              
448 0 0       0 if ( defined $val )
449             {
450 0         0 $self->{ $option } = $val;
451             }
452              
453 0         0 return $self->{ $option } = $val;
454             }
455              
456             #------------------------------------------------------------------------------
457             #
458             # url_callback - specify a callback for each URL visited in generating the
459             # sitemap. This is basically to allow some status output for traversing big
460             # sites
461             #
462             #------------------------------------------------------------------------------
463              
464             sub url_callback
465             {
466 0     0 1 0 my $self = shift;
467 0         0 my $callback = shift;
468              
469 0 0       0 return unless ref( $callback ) eq 'CODE';
470 0         0 $self->{ 'url-callback' } = $callback;
471             }
472              
473             #------------------------------------------------------------------------------
474             #
475             # generate - generate the sitemap
476             #
477             #------------------------------------------------------------------------------
478              
479             sub generate
480             {
481 1     1 1 6 my $self = shift;
482              
483 1 50       10 $self->{ 'ROOT' } = "$self->{ 'ROOT' }/"
484             unless $self->{ 'ROOT' } =~ m{/$}
485             ;
486              
487             # Create HTML::Summary
488              
489 1         12 $self->{ 'summarizer' } =
490             new HTML::Summary LENGTH => $self->{ 'SUMMARY_LENGTH' }
491             ;
492              
493             # Create WWW::Robot
494              
495             $self->{ 'robot' } = new WWW::Robot(
496             'NAME' => 'WWW::Sitemap',
497             'VERSION' => $VERSION,
498             'EMAIL' => $self->{ EMAIL },
499             'TRAVERSAL' => 'breadth',
500             'USERAGENT' => $self->{ USERAGENT },
501             'CHECK_MIME_TYPES' => 0,
502 1 50       35 'VERBOSE' => $self->{ VERBOSE } >= 2 ? 1 : 0,
503             );
504              
505             $self->{ 'robot' }->addHook(
506             'invoke-on-get-error',
507             sub {
508 0     0   0 my( $robot, $hook, $url, $response, $structure ) = @_;
509 0         0 $self->{ 'urls' }{ $url }++;
510 0         0 $self->{ 'title' }{ $url } = 'Error ' . $response->code();
511 0         0 $self->{ 'summary' }{ $url } = $response->message();
512             }
513 1         132 );
514              
515             $self->{ 'robot' }->addHook(
516             'invoke-on-contents',
517             sub {
518 1     1   866 my( $robot, $hook, $url, $response, $structure ) = @_;
519 1         8 my $contents = $response->content();
520 1         393 $contents =~ s{<(script|style).*?>.*?}{}sgi;
521 1         11 my $element = new HTML::TreeBuilder;
522 1         264 $element->parse( $contents );
523 1         89635 my $MD5_digest = md5_hex( $contents );
524 1 50       9 if ( exists( $self->{ 'MD5_digest' }{ $MD5_digest } ) )
525             {
526 0         0 $self->{ 'equiv' }{ $url }
527             = $self->{ 'MD5_digest' }{ $MD5_digest }
528             ;
529             }
530             else
531             {
532 1         5 $self->{ 'MD5_digest' }{ $MD5_digest } = $url;
533 1         34 $self->{ 'urls' }{ $url }++;
534 1         35 $self->get_title( $url, $element );
535 1   50     20 $self->{ 'summary' }{ $url } =
536             $self->{ 'summarizer' }->generate( $element ) ||
537             'NO SUMMARY'
538             ;
539 1         77102 shrink_whitespace( $self->{ 'summary' }{ $url } );
540 1 50       12 $self->{ 'url-callback' }->(
541             $url,
542             $self->{ 'depth' }{ $url },
543             $self->{ 'title' }{ $url },
544             $self->{ 'summary' }{ $url }
545             ) if defined $self->{ 'url-callback' };
546 1         6 $self->verbose( "url: ", $url );
547 1         67 $self->verbose( "depth: ", $self->{ 'depth' }{ $url } );
548 1         1105 $self->verbose( "title: ", $self->{ 'title' }{ $url } );
549 1         8 $self->verbose( "summary: ", $self->{ 'summary' }{ $url } );
550             }
551             }
552 1         22 );
553              
554             $self->{ 'robot' }->addHook(
555             'invoke-on-link',
556             sub {
557 0     0   0 my( $robot, $hook, $from_url, $to_url ) = @_;
558             # don't add links that don't look like HTML links
559 0 0       0 return unless $to_url =~ m{(?:/|\.s?html?)$};
560 0 0       0 if ( not defined( $self->{ 'depth' }{ $to_url } ) )
561             {
562 0         0 my $from = $self->{ 'depth' }{ $from_url };
563 0         0 $self->{ 'depth' }{ $to_url } = $from + 1;
564             }
565             # check the current depth, if the DEPTH option is set
566             return if (
567             defined $self->{ DEPTH } and
568             defined $self->{ 'depth' }{ $to_url } and
569             $self->{ 'depth' }{ $to_url } >= $self->{ DEPTH }
570 0 0 0     0 );
      0        
571 0         0 $self->{ 'link' }{ $from_url }{ $to_url }++;
572 0         0 $self->verbose( "link: $from_url -> $to_url" );
573             }
574 1         18 );
575              
576             $self->{ 'robot' }->addHook(
577             'add-url-test',
578             sub {
579 44     44   1529507 my( $robot, $hook, $url ) = @_;
580             # don't follow links that aren't internal to the site
581 44 50       160 return 0 unless $self->is_internal_url( $url );
582             # don't follow links that don't look like HTML links
583 0 0       0 return 0 unless $url =~ m{(?:/|\.s?html?)$};
584             # check the current depth, if the DEPTH option is set
585             return 0 if (
586             defined $self->{ DEPTH } and
587             defined $self->{ 'depth' }{ $url } and
588             $self->{ 'depth' }{ $url } >= $self->{ DEPTH }
589 0 0 0     0 );
      0        
590 0         0 return 1;
591             }
592 1         34 );
593              
594             $self->{ 'robot' }->addHook(
595             'follow-url-test',
596             sub {
597 1     1   261 my( $robot, $hook, $url ) = @_;
598             # don't follow links that aren't internal to the site
599 1 50       9 return 0 unless $self->is_internal_url( $url );
600             # don't follow links that don't look like HTML links
601 1 50       21 return 0 unless $url =~ m{(?:/|\.s?html?)$};
602             # check the current depth, if the DEPTH option is set
603              
604             return 0 if (
605             defined $self->{ DEPTH } and
606             $self->{ 'depth' }{ $url } >= $self->{ DEPTH }
607 1 50 33     30 );
608 1         18 return 1;
609             }
610 1         19 );
611              
612 1         16 $self->{ 'robot' }->addUrl( $self->{ 'ROOT' } );
613 1         9628 $self->{ 'depth' }{ $self->{ 'ROOT' } } = 0;
614 1         9 $self->{ 'robot' }->run();
615              
616             # Substitute equivilent links
617              
618 1         3812 for my $from_url ( keys %{ $self->{ 'link' } } )
  1         8  
619             {
620 0         0 for my $to_url ( keys %{ $self->{ 'link' }{ $from_url } } )
  0         0  
621             {
622 0 0 0     0 if (
623             exists( $self->{ 'equiv' }{ $from_url } ) or
624             exists( $self->{ 'equiv' }{ $to_url } )
625             )
626             {
627 0         0 my $no = delete $self->{ 'link' }{ $from_url }{ $to_url };
628 0   0     0 $from_url = $self->{ 'equiv' }{ $from_url } || $from_url;
629 0   0     0 $to_url = $self->{ 'equiv' }{ $to_url } || $to_url;
630 0         0 $self->{ 'link' }{ $from_url }{ $to_url } += $no;
631             }
632             }
633             }
634             }
635              
636             #------------------------------------------------------------------------------
637             #
638             # traverse - traverse the sitemap
639             #
640             #------------------------------------------------------------------------------
641              
642             sub traverse
643             {
644 1     1 1 14 my $self = shift;
645 1         2 my $callback = shift;
646 1   33     5 my $url = shift || $self->root();
647 1   50     6 my $depth = shift || 0;
648              
649 1 50       5 $self->{ 'visited' } = () if $depth == 0;
650 1         3 &$callback( $self, $url, $depth, 1 );
651 1         31 $self->{ 'visited' }{ $url }++;
652              
653             # Build up a list of non-external, not already visited, links from this URL
654              
655 1         3 my @links = ();
656 1         3 for( $self->links( $url ) )
657             {
658             # This is not the minimum depth for this URL ... leave it
659             # so that it will be visited later
660              
661 0 0       0 next unless $self->depth( $_ ) == $depth + 1;
662 0 0       0 next unless $self->is_internal_url( $_ );
663 0 0       0 next if $self->{ 'visited' }{ $_ };
664 0         0 push( @links, $_ );
665             }
666              
667 1 50       8 &$callback( $self, $url, $depth, 0 ) if @links;
668 1         3 for ( @links )
669             {
670 0         0 $self->traverse( $callback, $_, $depth+1 );
671             }
672 1 50       110 &$callback( $self, $url, $depth, 2 ) if @links;
673             }
674              
675             #==============================================================================
676             #
677             # Private methods
678             #
679             #==============================================================================
680              
681             #------------------------------------------------------------------------------
682             #
683             # initialize - supports sub-classing
684             #
685             #------------------------------------------------------------------------------
686              
687             sub initialize
688             {
689 1     1 0 2 my $self = shift;
690 1         2 my $class = shift;
691              
692 1 50       7 return undef unless @_ % 2 == 0; # check that config hash has even no.
693             # of elements
694              
695 1         6 %{ $self } = ( %OPTIONS, @_ ); # set options from defaults / config.
  1         14  
696             # hash passed as arguments
697              
698 1         3 for ( keys %{ $self } )
  1         5  
699             {
700 6 50       16 unless ( exists( $OPTIONS{ $_ } ) )
701             {
702 0         0 print STDERR "$_ is not a valid $class option\n";
703 0         0 return undef;
704             }
705             }
706 1         5 for ( keys %REQUIRED ) # Check that required options are
707             { # present
708 2 50       7 unless ( defined $self->{ $_ } )
709             {
710 0         0 print STDERR "the $_ option is required\n";
711 0         0 return undef;
712             }
713             }
714 1         4 return $self;
715             }
716              
717             #------------------------------------------------------------------------------
718             #
719             # get_title - get the title for an HTML string
720             #
721             #------------------------------------------------------------------------------
722              
723             sub get_title
724             {
725 1     1 0 2 my $self = shift;
726 1         3 my $url = shift;
727 1         2 my $structure = shift;
728              
729             $structure->traverse(
730             sub {
731 526     526   10888 my $node = shift;
732 526         554 my $start_flag = shift; # NOT USED
733 526         445 my $depth = shift; # NOT USED
734              
735 526 100       1067 return 1 if $node->tag ne 'title';
736 1 50       10 return 0 if $start_flag == 0;
737              
738 1 50 33     6 if (
739             defined( $node->content ) and
740             ref( $node->content ) eq 'ARRAY'
741             )
742             {
743 1         17 foreach my $bit ( @{ $node->content } )
  1         3  
744             {
745 1 50 33     8 next if not defined $bit || ref( $bit ) ne '';
746 1 50       7 $self->{ 'title' }{ $url } =
747             (
748             defined $self->{ 'title' }{ $url } ?
749             "$self->{ 'title' }{ $url } $bit"
750             :
751             $bit
752             )
753             ;
754             }
755             }
756             },
757 1         12 1
758             );
759              
760 1 50       31 if ( defined( $self->{ 'title' }{ $url } ) )
761             {
762 1         18 shrink_whitespace( $self->{ 'title' }{ $url } );
763             }
764 1   50     6 $self->{ 'title' }{ $url } ||= 'NO TITLE';
765             }
766              
767             #------------------------------------------------------------------------------
768             #
769             # shrink_whitespace - clean up text - remove leading / trailing whitespace,
770             # and multiple spaces
771             #
772             #------------------------------------------------------------------------------
773              
774             sub shrink_whitespace
775             {
776 2     2 0 25 $_[ 0 ] =~ s!\240=! !g;
777 2         11 $_[ 0 ] =~ s!^\s*!!;
778 2         42 $_[ 0 ] =~ s!\s*$!!;
779 2         39 $_[ 0 ] =~ s!\s+! !g;
780 2         8 $_[ 0 ] =~ s!\r!!g;
781             }
782              
783             #------------------------------------------------------------------------------
784             #
785             # verbose - generate verbose error messages, if the VERBOSE option has been
786             # selected
787             #
788             #------------------------------------------------------------------------------
789              
790             sub verbose
791             {
792 4     4 0 44 my $self = shift;
793              
794 4 50       15 return unless $self->{ VERBOSE };
795 4         573 print STDERR @_, "\n";
796             }
797              
798             1;