File Coverage

blib/lib/WWW/Crawl4AI/DeepCrawlIterator.pm
Criterion Covered Total %
statement 46 51 90.2
branch 19 20 95.0
condition 15 24 62.5
subroutine 5 9 55.5
pod 3 3 100.0
total 88 107 82.2


line stmt bran cond sub pod time code
1             package WWW::Crawl4AI::DeepCrawlIterator;
2             # ABSTRACT: breadth-first iterator for deep_crawl, separating frontier management from crawl logic
3 2     2   10 use Moo;
  2         4  
  2         22  
4              
5             our $VERSION = '0.001';
6              
7              
8             has crawler => (
9             is => 'ro',
10             required => 1,
11             );
12              
13              
14             has start_url => (
15             is => 'ro',
16             required => 1,
17             );
18              
19              
20             has max_pages => (
21             is => 'ro',
22             default => sub { 25 },
23             );
24              
25              
26             has max_depth => (
27             is => 'ro',
28             default => sub { 2 },
29             );
30              
31              
32             has same_host => (
33             is => 'ro',
34             default => sub { 1 },
35             );
36              
37              
38             has url_filter => (
39             is => 'ro',
40             builder => 1,
41             );
42              
43              
44 0     0   0 sub _build_url_filter { undef }
45              
46             has on_page => (
47             is => 'ro',
48             builder => 1,
49             );
50              
51              
52 0     0   0 sub _build_on_page { undef }
53              
54             # Internal state
55             has _seen => ( is => 'rwp', default => sub { {} } );
56             has _queue => ( is => 'rwp', default => sub { [] } );
57             has _results => ( is => 'rwp', default => sub { [] } );
58             has _start_host => ( is => 'rw' );
59             has _crawled => ( is => 'rw', default => sub { 0 } );
60              
61             sub _canon_url {
62 20     20   25 my ( $self, $url ) = @_;
63 20         67 require URI;
64 20 50       24 my $u = eval { URI->new($url) } or return $url;
  20         47  
65 20         9085 $u->fragment(undef);
66 20         157 return $u->as_string;
67             }
68              
69             sub _host_eq {
70 14     14   20 my ( $self, $url ) = @_;
71 14         44 require URI;
72 14   50     17 my $host = lc( eval { URI->new($url)->host } // '' );
  14         24  
73 14   50     941 return $host eq ( $self->_start_host // '' );
74             }
75              
76              
77             sub next {
78 24     24 1 45 my ( $self ) = @_;
79              
80             # Seed the queue on first call
81 24 100 66     72 if ( $self->_crawled == 0 && !@{ $self->_queue } ) {
  7         24  
82 7         15 $self->_push_url( $self->start_url, 0 );
83             }
84              
85             # Exhausted?
86 24 100       31 return undef unless @{ $self->_queue };
  24         68  
87 18 100       21 return undef if @{ $self->_results } >= $self->max_pages;
  18         46  
88              
89 17         17 my $node = shift @{ $self->_queue };
  17         28  
90 17         74 my $result = $self->crawler->crawl( $node->{url} );
91 17         466 push @{ $self->_results }, $result;
  17         37  
92 17         41 $self->_crawled( $self->_crawled + 1 );
93              
94 17         24 my $depth = $node->{depth};
95 17 100       41 $self->on_page->( $result, $depth ) if $self->on_page;
96              
97             # Lock start_host from the first real crawl (handles redirects)
98 17 100 66     63 if ( !defined $self->_start_host && $result->final_url ) {
99 7         27 require URI;
100             $self->_start_host(
101 7   50     12 lc( eval { URI->new( $result->final_url )->host } // '' ) );
  7         26  
102             }
103              
104             # Schedule links if not at max depth and result is good
105 17 100 66     660 if ( $depth < $self->max_depth && $result->ok ) {
106 10         11 for my $url ( @{ $result->urls } ) {
  10         21  
107 15 100 100     42 next if $self->same_host && !$self->_host_eq($url);
108 14 100 100     43 next if $self->url_filter && !$self->url_filter->($url);
109 13         29 $self->_push_url( $url, $depth + 1 );
110             }
111             }
112              
113 17         75 return [ $result, $depth ];
114             }
115              
116             sub _push_url {
117 20     20   34 my ( $self, $url, $depth ) = @_;
118 20         36 my $canon = $self->_canon_url($url);
119 20 100       111 return if $self->_seen->{$canon}++;
120 19         22 push @{ $self->_queue }, { url => $url, depth => $depth };
  19         69  
121             }
122              
123              
124 0     0 1   sub results { $_[0]->_results }
125              
126              
127             sub is_exhausted {
128 0     0 1   my ( $self ) = @_;
129 0   0       return !@{ $self->_queue } || @{ $self->_results } >= $self->max_pages;
130             }
131              
132             1;
133              
134             __END__