Dezi-App
view release on metacpan or search on metacpan
lib/Dezi/Aggregator/Spider.pm view on Meta::CPAN
package Dezi::Aggregator::Spider;
use Moose;
extends 'Dezi::Aggregator';
use Carp;
use Scalar::Util qw( blessed );
use URI;
use HTTP::Cookies;
use Types::Standard qw( InstanceOf Maybe Int CodeRef Str Bool ArrayRef );
use Dezi::Types qw( DeziFileRules DeziEpoch );
use Dezi::Utils;
use Dezi::Queue;
use Dezi::Cache;
use Dezi::Aggregator::Spider::UA;
use Search::Tools::UTF8;
use XML::Feed;
use WWW::Sitemap::XML;
use File::Rules;
use Class::Load;
#
# TODO tests for cookies, non-text urls needing filters
#
#
has 'agent' => (
is => 'rw',
isa => Str,
default => sub {'dezi-spider http://dezi.org/'},
);
has 'authn_callback' => ( is => 'rw', isa => CodeRef );
has 'credential_timeout' => ( is => 'rw', isa => Int, default => sub {30} );
has 'credentials' => ( is => 'rw', isa => Str );
has 'delay' => ( is => 'rw', isa => Int, default => sub {5} );
has 'email' => (
is => 'rw',
isa => Str,
default => sub {'dezi@user.failed.to.set.email.invalid'},
);
has 'file_rules' => ( is => 'rw', isa => DeziFileRules, coerce => 1, );
has 'follow_redirects' => ( is => 'rw', isa => Bool, default => sub {1} );
has 'keep_alive' => ( is => 'rw', isa => Bool, default => sub {0} );
# whitelist which HTML tags we consider "links"
# should be subset of what HTML::LinkExtor considers links
has 'link_tags' => (
is => 'rw',
isa => ArrayRef,
default => sub { [ 'a', 'frame', 'iframe' ] }
);
has 'max_depth' => ( is => 'rw', isa => Maybe [Int] );
has 'max_files' => ( is => 'rw', isa => Int, default => sub {0} );
has 'max_size' => ( is => 'rw', isa => Int, default => sub {5_000_000} );
has 'max_time' => ( is => 'rw', isa => Int, ); # TODO
has 'md5_cache' => (
is => 'rw',
isa => InstanceOf ['Dezi::Cache'],
default => sub { Dezi::Cache->new }
);
has 'modified_since' => ( is => 'rw', isa => DeziEpoch, coerce => 1, );
has 'queue' => (
is => 'rw',
isa => InstanceOf ['Dezi::Queue'],
default => sub { Dezi::Queue->new }
);
has 'remove_leading_dots' => ( is => 'rw', isa => Bool, default => sub {1} );
has 'same_hosts' => ( is => 'rw', isa => ArrayRef, default => sub { [] } );
has 'timeout' => ( is => 'rw', isa => Int, default => sub {30} );
has 'ua' => ( is => 'rw', isa => InstanceOf ['LWP::UserAgent'] );
has 'uri_cache' => (
is => 'rw',
isa => InstanceOf ['Dezi::Cache'],
default => sub { Dezi::Cache->new },
);
has 'use_md5' => ( is => 'rw', isa => Bool, default => sub {0} );
has 'use_cookies' => ( is => 'rw', isa => Bool, default => sub {1} );
#use LWP::Debug qw(+);
our $VERSION = '0.018';
# shortcut
my $UTILS = 'Dezi::Utils';
=pod
=head1 NAME
Dezi::Aggregator::Spider - web aggregator
=head1 SYNOPSIS
use Dezi::Aggregator::Spider;
my $spider = Dezi::Aggregator::Spider->new(
indexer => Dezi::Indexer->new
);
$spider->indexer->start;
$spider->crawl( 'http://swish-e.org/' );
$spider->indexer->finish;
lib/Dezi/Aggregator/Spider.pm view on Meta::CPAN
Get/set the Dezi::Cache-derived object used to track which URIs have
been fetched already.
=item md5_cache I<cache_object>
If use_md5() is true, this Dezi::Cache-derived object tracks
the URI fingerprints.
=item file_rules I<File_Rules_or_ARRAY>
Apply L<File::Rules> object in uri_ok(). I<File_Rules_or_ARRAY> should
be a L<File::Rules> object or an array of strings suitable to passing
to File::Rules->new().
=item queue I<queue_object>
Get/set the Dezi::Queue-derived object for tracking which URIs still
need to be fetched.
=item ua I<lwp_useragent>
Get/set the Dezi::Aggregator::Spider::UA object.
=item max_depth I<n>
How many levels of links to follow. B<NOTE:> This value describes the number
of links from the first argument passed to I<crawl>.
Default is unlimited depth.
=item max_time I<n>
This optional key will set the max minutes to spider. Spidering
for this host will stop after C<max_time> seconds, and move on to the
next server, if any. The default is to not limit by time.
=item max_files I<n>
This optional key sets the max number of files to spider before aborting.
The default is to not limit by number of files. This is the number of requests
made to the remote server, not the total number of files to index (see C<max_indexed>).
This count is displayed at the end of indexing as C<Unique URLs>.
This feature can (and perhaps should) be use when spidering a web site where dynamic
content may generate unique URLs to prevent run-away spidering.
=item max_size I<n>
This optional key sets the max size of a file read from the web server.
This B<defaults> to 5,000,000 bytes. If the size is exceeded the resource is
truncated per LWP::UserAgent.
Set max_size to zero for unlimited size.
=item modified_since I<date>
This optional parameter will skip any URIs that do not report having
been modified since I<date>. The C<Last-Modified> HTTP header is used to
determine modification time.
=item keep_alive I<1|0>
This optional parameter will enable keep alive requests. This can dramatically speed
up spidering and reduce the load on server being spidered. The default is to not use
keep alives, although enabling it will probably be the right thing to do.
To get the most out of keep alives, you may want to set up your web server to
allow a lot of requests per single connection (i.e MaxKeepAliveRequests on Apache).
Apache's default is 100, which should be good.
When a connection is not closed the spider does not wait the "delay"
time when making the next request. In other words, there is no delay in
requesting documents while the connection is open.
Note: you must have at least libwww-perl-5.53_90 installed to use this feature.
=item delay I<n>
Get/set the number of seconds to wait between making requests. Default is
5 seconds (a very friendly delay).
=item timeout I<n>
Get/set the number of seconds to wait before considering the remote
server unresponsive. The default is 10.
=item authn_callback I<code_ref>
CODE reference to fetch username/password credentials when necessary. See also
C<credentials>.
=item credential_timeout I<n>
Number of seconds to wait before skipping manual prompt for username/password.
=item credentials I<user:pass>
String with C<username>:C<password> pair to be used when prompted by
the server.
=item follow_redirects I<1|0>
By default, 3xx responses from the server will be followed when
they are on the same hostname. Set to false (0) to not follow
redirects.
=item link_tags
TODO
=item remove_leading_dots I<1|0>
Microsoft server hack.
=item same_hosts I<array_ref>
ARRAY ref of hostnames to be treated as identical to the original
host being spidered. By default the spider will not follow
links to different hosts.
=back
=head2 BUILD
Initializes a new spider object. Called by new().
=cut
sub BUILD {
my $self = shift;
$self->{_auth_cache} = Dezi::Cache->new; # ALWAYS inmemory cache
$self->{ua}
||= Dezi::Aggregator::Spider::UA->new( $self->agent, $self->email, );
$self->{ua}
->set_link_tags( { map { lc($_) => 1 } @{ $self->{link_tags} } } );
# we handle our own delay
$self->{ua}->delay(0);
$self->{ua}->timeout( $self->timeout );
# TODO we test this using HEAD request. Set here too?
#$self->{ua}->max_size( $self->{max_size} ) if $self->{max_size};
if ( $self->use_cookies ) {
$self->{ua}->cookie_jar( HTTP::Cookies->new() );
}
if ( $self->keep_alive ) {
if ( $self->{ua}->can('conn_cache') ) {
$self->{ua}
->conn_cache( { total_capacity => $self->keep_alive } );
}
else {
warn
"can't use keep-alive: conn_cache() method not available on ua "
. ref( $self->{ua} );
}
}
$self->{_current_depth} = 1;
$self->{same_host_lookup} = { map { $_ => 1 } @{ $self->{same_hosts} } };
if ( $self->use_md5 ) {
Class::Load::load_class('Digest::MD5');
}
# if Dezi::Indexer::Config defined, use that for some items
if ( $self->indexer and $self->indexer->config ) {
if ( $self->indexer->config->FileRules && !$self->file_rules ) {
$self->file_rules(
File::Rules->new( $self->indexer->config->FileRules ) );
}
}
# from spider.pl. not sure if we need it or not.
# Lame Microsoft
$URI::ABS_REMOTE_LEADING_DOTS = $self->remove_leading_dots;
return $self;
}
=head2 uri_ok( I<uri> )
Returns true if I<uri> is acceptable for including in an index.
The 'ok-ness' of the I<uri> is based on its base, robot rules,
and the spider configuration.
=cut
sub uri_ok {
my $self = shift;
my $uri = shift or croak "URI required";
my $str = $uri->canonical->as_string;
$str =~ s/#.*//; # target anchors create noise
if ( $self->verbose > 1 || $self->debug ) {
$self->write_log_line();
$self->write_log(
uri => $uri,
msg => "checking if ok",
);
}
if ( $uri->scheme !~ m,^http, ) {
$self->debug and $self->write_log(
uri => $uri,
msg => "skipping, unsupported scheme"
);
return 0;
}
# check if we're on the same host.
if ( $uri->rel( $self->{_base} ) eq $uri ) {
lib/Dezi/Aggregator/Spider.pm view on Meta::CPAN
sub get_doc {
my $self = shift;
# return unless we have something in the queue
return unless $self->left_in_queue();
# pop the queue and make it a URI
my $uri = $self->next_from_queue();
my $depth = $self->uri_cache->get("$uri");
$self->debug
and $self->write_log(
uri => $uri,
msg => sprintf(
"depth:%d max_depth:%s",
$depth, ( $self->max_depth || 'undef' )
),
);
return if defined $self->max_depth && $depth > $self->max_depth;
$self->{_cur_depth} = $depth;
my $doc = $self->_make_request($uri);
if ($doc) {
$self->remove_from_queue($uri);
}
return $doc;
}
=head2 get_authorized_doc( I<uri>, I<response> )
Called internally when the server returns a 401 or 403 response.
Will attempt to determine the correct credentials for I<uri>
based on the previous attempt in I<response> and what you
have configured in B<credentials>, B<authn_callback> or when
manually prompted.
=cut
sub get_authorized_doc {
my $self = shift;
my $uri = shift or croak "uri required";
my $response = shift or croak "response required";
# set up credentials
$self->_authorize( $uri, $response->http_response ) or return;
return $self->_make_request($uri);
}
sub _make_request {
my ( $self, $uri ) = @_;
# get our useragent
my $ua = $self->ua;
my $delay = 0;
if ( $self->{keep_alive} ) {
$delay = 0;
}
elsif ( !$self->{delay} or !$self->{_last_response_time} ) {
$delay = 0;
}
else {
my $elapsed = time() - $self->{_last_response_time};
$delay = $self->{delay} - $elapsed;
$delay = 0 if $delay < 0;
$self->debug
and $self->write_log(
uri => $uri,
msg => "elapsed:$elapsed delay:$delay",
);
}
$self->write_log(
uri => $uri,
msg => "GET delay:$delay",
) if $self->verbose;
my %get_args = (
uri => $uri,
delay => $delay,
debug => $self->debug,
verbose => $self->verbose,
);
if ( my ( $user, $pass ) = $self->_get_user_pass($uri) ) {
$get_args{user} = $user;
$get_args{pass} = $pass;
}
# fetch the uri. $ua handles delay internally.
my $response = $ua->get(%get_args);
my $http_response = $response->http_response;
# flag current time for next delay calc.
$self->{_last_response_time} = time();
# redirect? follow, conditionally.
if ( $response->is_redirect ) {
my $location = $response->header('location');
if ( !$location ) {
$self->write_log(
uri => $uri,
msg => "skipping, redirect without a Location header",
);
return $response->status;
}
$self->debug
and $self->write_log(
uri => $uri,
msg => "redirect: $location",
);
if ( $self->follow_redirects ) {
$self->_add_links( $uri,
URI->new_abs( $location, $http_response->base ) );
}
return $response->status;
( run in 1.203 second using v1.01-cache-2.11-cpan-4ef0a570458 )