File Coverage

blib/lib/WWW/SimpleRobot.pm
Criterion Covered Total %
statement 74 85 87.0
branch 19 38 50.0
condition 1 3 33.3
subroutine 10 10 100.0
pod 0 2 0.0
total 104 138 75.3


line stmt bran cond sub pod time code
1             package WWW::SimpleRobot;
2              
3             #==============================================================================
4             #
5             # Standard pragmas
6             #
7             #==============================================================================
8              
9             require 5.005_62;
10 1     1   808 use strict;
  1         3  
  1         33  
11 1     1   6 use warnings;
  1         1  
  1         46  
12              
13             #==============================================================================
14             #
15             # Required modules
16             #
17             #==============================================================================
18              
19 1     1   936 use URI;
  1         8516  
  1         164  
20 1     1   857 use LWP::Simple;
  1         104062  
  1         8  
21 1     1   1197 use HTML::LinkExtor;
  1         12191  
  1         1148  
22              
23             #==============================================================================
24             #
25             # Private globals
26             #
27             #==============================================================================
28              
29             our $VERSION = '0.07';
30             our %OPTIONS = (
31             URLS => [],
32             FOLLOW_REGEX => '',
33             VISIT_CALLBACK => sub {},
34             BROKEN_LINK_CALLBACK=> sub {},
35             VERBOSE => 0,
36             DEPTH => undef,
37             TRAVERSAL => 'depth',
38             );
39              
40             #==============================================================================
41             #
42             # Private methods
43             #
44             #==============================================================================
45              
46             sub _verbose
47             {
48 6     6   14 my $self = shift;
49              
50 6 50       30 return unless $self->{VERBOSE};
51 0         0 print STDERR @_;
52             }
53              
54             #==============================================================================
55             #
56             # Constructor
57             #
58             #==============================================================================
59              
60             sub new
61             {
62 2     2 0 228 my $class = shift;
63 2         28 my %args = ( %OPTIONS, @_ );
64              
65 2         78 for ( keys %args )
66             {
67 14 50       33 die "Unknown option $_\n" unless exists $OPTIONS{$_};
68             }
69 2 50       20 unless ( $args{TRAVERSAL} =~ /^(depth|breadth)$/ )
70             {
71 0         0 die "option TRAVERSAL should be either 'depth' or 'breadth'\n";
72             }
73              
74 2         7 my $self = bless \%args, $class;
75              
76 2         6 return $self;
77              
78             }
79              
80             #==============================================================================
81             #
82             # Public methods
83             #
84             #==============================================================================
85              
86             sub traverse
87             {
88 2     2 0 12 my $self = shift;
89              
90 2 50       2 die "No URLS specified in constructor\n" unless @{$self->{URLS}};
  2         11  
91 2         5 $self->_verbose(
92 2         15 "Creating list of files to index from @{$self->{URLS}}...\n"
93             );
94 2         4 my @pages;
95             my %seen;
96 2         3 for my $url ( @{$self->{URLS}} )
  2         6  
97             {
98 2         12 my $uri = URI->new( $url );
99 2 50       8542 die "$uri is not a valid URL\n" unless $uri;
100 2 50       28 die "$uri is not a valid URL\n" unless $uri->scheme;
101 2 50       57 die "$uri is not a web page\n" unless $uri->scheme eq 'http';
102 2 50       37 die "can't HEAD $uri\n" unless
103             my ( $content_type, $document_length, $modified_time ) =
104             head( $uri )
105             ;
106 2         190275 $uri = $uri->canonical->as_string;
107 2         556 $seen{$uri}++;
108 2         16 my $page = {
109             modified_time => $modified_time,
110             url => $uri,
111             depth => 0,
112             linked_from => $url,
113             };
114 2         11 push( @pages, $page );
115             }
116 2         11 while ( my $page = shift( @pages ) )
117             {
118 2         6 my $url = $page->{url};
119 2         17 $self->_verbose( "GET $url\n" );
120 2         9 my $html = get( $url );
121 2 50       94575 unless( $html )
122             {
123 0         0 $self->{BROKEN_LINK_CALLBACK}( $url, $page->{linked_from}, $page->{depth} );
124             }
125 2         242 $self->_verbose( "Extract links from $url\n" );
126 2         26 my $linkxtor = HTML::LinkExtor->new( undef, $url );
127 2         997 $linkxtor->parse( $html );
128 2         47970 my @links = $linkxtor->links;
129 2         83 $self->{VISIT_CALLBACK}( $url, $page->{depth}, $html, \@links );
130 2 50 33     270 next if defined( $self->{DEPTH} ) and $page->{depth} == $self->{DEPTH};
131 2         7 for my $link ( @links )
132             {
133 162         1203 my ( $tag, %attr ) = @$link;
134 162 100       12541 next unless $tag eq 'a';
135 126 50       932 next unless my $href = $attr{href};
136 126         1768 $href =~ s/[#?].*$//;
137 126 50       1650 next unless $href = URI->new( $href );
138 126         13848 $href = $href->canonical->as_string;
139 126 100       14974 next unless $href =~ /$self->{FOLLOW_REGEX}/;
140 72         291 my ( $content_type, undef, $modified_time ) = head( $href );
141 72 50       4437154 next unless $content_type;
142 72 50       570 next unless $content_type eq 'text/html';
143 0 0       0 next if $seen{$href}++;
144 0         0 my $npages = @pages;
145 0         0 my $nseen = keys %seen;
146 0         0 my $page = {
147             modified_time => $modified_time,
148             url => $href,
149             depth => $page->{depth}+1,
150             };
151 0 0       0 splice(
152             @pages,
153             $self->{TRAVERSAL} eq 'depth' ? 0 : @pages,
154             # depth first ... unshift, breadth first ... push
155             0,
156             $page
157             );
158 0         0 $self->_verbose(
159             "$nseen/$npages : $url : $href",
160 0         0 " : ", join( ' ', map { $_->{url} } @pages ),
161             "\n"
162             );
163             }
164             }
165 2         68 $self->{pages} = \@pages;
166 2         21 $self->{urls} = [ map { $_->{url} } @pages ];
  0         0  
167             }
168              
169             #==============================================================================
170             #
171             # AUTOLOADed accessor methods
172             #
173             #==============================================================================
174              
175             sub AUTOLOAD
176             {
177 2     2   38 my $self = shift;
178 2         6 my $value = shift;
179 1     1   13 use vars qw( $AUTOLOAD );
  1         2  
  1         118  
180 2         18 my $method_name = $AUTOLOAD;
181 2         20 $method_name =~ s/.*:://;
182 2 50       11 $self->{$method_name} = $value if defined $value;
183 2         615 return $self->{$method_name};
184             }
185              
186             # Preloaded methods go here.
187              
188             1;
189             __END__