File Coverage

blib/lib/Web/Scraper.pm
Criterion Covered Total %
statement 168 201 83.5
branch 55 84 65.4
condition 30 53 56.6
subroutine 35 38 92.1
pod 1 7 14.2
total 289 383 75.4


line stmt bran cond sub pod time code
1             package Web::Scraper;
2 23     23   851001 use strict;
  23         47  
  23         705  
3 23     23   96 use warnings;
  23         30  
  23         495  
4 23     23   457 use 5.008001;
  23         93  
  23         597  
5 23     23   83 use Carp;
  23         24  
  23         1357  
6 23     23   105 use Scalar::Util qw(blessed);
  23         25  
  23         1708  
7 23     23   96 use List::Util qw(first);
  23         38  
  23         1859  
8 23     23   12511 use HTML::Entities;
  23         109102  
  23         1668  
9 23     23   10415 use HTML::Tagset;
  23         22766  
  23         750  
10 23     23   11854 use HTML::TreeBuilder::XPath;
  23         1049366  
  23         271  
11 23     23   12816 use HTML::Selector::XPath;
  23         48651  
  23         1021  
12 23     23   10841 use UNIVERSAL::require;
  23         23921  
  23         191  
13              
14             our $VERSION = '0.38';
15              
16             sub import {
17 24     24   190 my $class = shift;
18 24         48 my $pkg = caller;
19              
20 23     23   1196 no strict 'refs';
  23         38  
  23         607  
21 23     23   74 no warnings 'redefine';
  23         28  
  23         9792  
22 24         58 *{"$pkg\::scraper"} = _build_scraper($class);
  24         121  
23 24     42   57 *{"$pkg\::process"} = sub { goto &process };
  24         69  
  42         477  
24 24     1   50 *{"$pkg\::process_first"} = sub { goto &process_first };
  24         74  
  1         14  
25 24     37   49 *{"$pkg\::result"} = sub { goto &result };
  24         26574  
  37         160  
26             }
27              
28             our $UserAgent;
29              
30             sub __ua {
31 0     0   0 require LWP::UserAgent;
32 0   0     0 $UserAgent ||= LWP::UserAgent->new(agent => __PACKAGE__ . "/" . $VERSION);
33 0         0 $UserAgent;
34             }
35              
36             sub user_agent {
37 0     0 0 0 my $self = shift;
38 0 0       0 $self->{user_agent} = shift if @_;
39 0 0       0 $self->{user_agent} || __ua;
40             }
41              
42             sub define {
43 1     1 0 7185 my($class, $coderef) = @_;
44 1         5 bless { code => $coderef }, $class;
45             }
46              
47             sub _build_scraper {
48 24     24   34 my $class = shift;
49             return sub(&) {
50 41     41   266871 my($coderef) = @_;
51 41         178 bless { code => $coderef }, $class;
52 24         109 };
53             }
54              
55             sub scrape {
56 43     43 1 319 my $self = shift;
57 43         63 my($stuff, $current) = @_;
58              
59 43         44 my($html, $tree);
60              
61 43 50 66     610 if (blessed($stuff) && $stuff->isa('URI')) {
    50 66        
    100 66        
    50 33        
62 0         0 my $ua = $self->user_agent;
63 0         0 my $res = $ua->get($stuff);
64 0         0 return $self->scrape($res, $stuff->as_string);
65             } elsif (blessed($stuff) && $stuff->isa('HTTP::Response')) {
66 0 0       0 if ($stuff->is_success) {
67 0         0 $html = $stuff->decoded_content;
68             } else {
69 0         0 croak "GET " . $stuff->request->uri . " failed: ", $stuff->status_line;
70             }
71 0   0     0 $current ||= $stuff->request->uri;
72             } elsif (blessed($stuff) && $stuff->isa('HTML::Element')) {
73 3         12 $tree = $stuff->clone;
74             } elsif (ref($stuff) && ref($stuff) eq 'SCALAR') {
75 0         0 $html = $$stuff;
76             } else {
77 40         54 $html = $stuff;
78             }
79              
80 43   66     288 $tree ||= $self->build_tree($html);
81              
82 43         65 my $stash = {};
83 23     23   123 no warnings 'redefine';
  23         40  
  23         21497  
84 43         111 local *process = create_process(0, $tree, $stash, $current);
85 43         92 local *process_first = create_process(1, $tree, $stash, $current);
86              
87 43         57 my $retval;
88             local *result = sub {
89 37     37   46 $retval++;
90 37         59 my @keys = @_;
91              
92 37 50       78 if (@keys == 1) {
    0          
93 37         87 return $stash->{$keys[0]};
94             } elsif (@keys) {
95 0         0 my %res;
96 0         0 @res{@keys} = @{$stash}{@keys};
  0         0  
97 0         0 return \%res;
98             } else {
99 0         0 return $stash;
100             }
101 43         141 };
102              
103 43         269 my $ret = $self->{code}->($tree);
104 43         424 $tree->delete;
105              
106             # check user specified return value
107 43 100       4185 return $ret if $retval;
108              
109 6         204 return $stash;
110             }
111              
112             sub build_tree {
113 40     40 0 56 my($self, $html) = @_;
114              
115 40         325 my $t = HTML::TreeBuilder::XPath->new;
116 40 50       8860 $t->store_comments(1) if ($t->can('store_comments'));
117 40         452 $t->ignore_unknown(0);
118 40         927 $t->parse($html);
119 40         16185 $t->eof;
120 40         5351 $t;
121             }
122              
123             sub create_process {
124 86     86 0 109 my($first, $tree, $stash, $uri) = @_;
125              
126             sub {
127 43     43   86 my($exp, @attr) = @_;
128              
129 43 100       248 my $xpath = $exp =~ m!^(?:/|id\()! ? $exp : HTML::Selector::XPath::selector_to_xpath($exp);
130 43         3432 my @nodes = eval {
131 43         265 local $SIG{__WARN__} = sub { };
  0         0  
132 43         264 $tree->findnodes($xpath);
133             };
134              
135 43 50       77713 if ($@) {
136 0         0 die "'$xpath' doesn't look like a valid XPath expression: $@";
137             }
138              
139 43 50       109 @nodes or return;
140 43 100       93 @nodes = ($nodes[0]) if $first;
141              
142 43         187 while (my($key, $val) = splice(@attr, 0, 2)) {
143 43 100       205 if (!defined $val) {
    100          
144 3 50 33     27 if (ref($key) && ref($key) eq 'CODE') {
145 3         8 for my $node (@nodes) {
146 5         332 local $_ = $node;
147 5         15 $key->($node);
148             }
149             } else {
150 0         0 die "Don't know what to do with $key => undef";
151             }
152             } elsif ($key =~ s!\[\]$!!) {
153 9         30 $stash->{$key} = [ map __get_value($_, $val, $uri), @nodes ];
154             } else {
155 31         87 $stash->{$key} = __get_value($nodes[0], $val, $uri);
156             }
157             }
158              
159 43         582 return;
160 86         416 };
161             }
162              
163             sub __get_value {
164 73     73   136 my($node, $val, $uri) = @_;
165              
166 73 50 66     788 if (ref($val) && ref($val) eq 'CODE') {
    100 66        
    100 66        
    100 66        
    100          
    100          
    50          
167 0         0 local $_ = $node;
168 0         0 return $val->($node);
169             } elsif (blessed($val) && $val->isa('Web::Scraper')) {
170 3         15 return $val->scrape($node, $uri);
171             } elsif ($val =~ s!^@!!) {
172 9         31 my $value = $node->attr($val);
173 9 100 66     113 if ($uri && is_link_element($node, $val)) {
174 5         1743 require URI;
175 5         10637 $value = URI->new_abs($value, $uri);
176             }
177 9         21076 return $value;
178             } elsif (lc($val) eq 'content' || lc($val) eq 'text') {
179             # getValue method is used for getting a content of comment nodes
180             # from HTML::TreeBuilder::XPath (version >= 0.14)
181             # or HTML::TreeBuilder::LibXML (version >= 0.13)
182             # getValue method works like as_text in both modules
183             # for other node types
184 31 50       88 return $node->isTextNode
    100          
185             ? $node->string_value
186             : ($node->can('getValue')
187             ? $node->getValue
188             : $node->as_text);
189             } elsif (lc($val) eq 'raw' || lc($val) eq 'html') {
190 7 50       21 if ($node->isTextNode) {
191 0 0       0 if ($HTML::TreeBuilder::XPath::VERSION < 0.09) {
192 0         0 return HTML::Entities::encode($node->as_XML, q("'<>&));
193             } else {
194 0         0 return $node->as_XML;
195             }
196             }
197 7         40 my $html = $node->as_XML;
198 7         1733 $html =~ s!^<.*?>!!;
199 7         52 $html =~ s!\s*\n*$!!;
200 7         40 return $html;
201             } elsif (ref($val) eq 'HASH') {
202 3         4 my $values;
203 3         7 for my $key (keys %$val) {
204 5         82 $values->{$key} = __get_value($node, $val->{$key}, $uri);
205             }
206 3         24 return $values;
207             } elsif (ref($val) eq 'ARRAY') {
208 20         28 my $how = $val->[0];
209 20         38 my $value = __get_value($node, $how, $uri);
210 20         625 for my $filter (@$val[1..$#$val]) {
211 24         36 $value = run_filter($value, $filter);
212             }
213 20         97 return $value;
214             } else {
215 0         0 Carp::croak "Unknown value type $val";
216             }
217             }
218              
219             sub run_filter {
220 24     24 0 28 my($value, $filter) = @_;
221              
222             ## sub { s/foo/bar/g } is a valid filter
223             ## sub { DateTime::Format::Foo->parse_string(shift) } is valid too
224 24         17 my $callback;
225             my $module;
226              
227 24 100 33     71 if (ref($filter) eq 'CODE') {
    100          
    100          
    50          
228 10         13 $callback = $filter;
229 10         13 $module = "$filter";
230             } elsif (ref($filter) eq 'Regexp') {
231             $callback = sub {
232 8     8   47 my @unnamed = shift =~ /$filter/x;
233 23 100   23   11093 if (%+) {
  23 100       9026  
  23         4894  
  8         38  
234 2         20 return { %+ };
235             } elsif (@unnamed) {
236 5         11 return shift @unnamed;
237             } else {
238 1         2 return;
239             }
240 8         21 };
241 8         10 $module = "$filter";
242             } elsif (!ref($filter)) {
243 5 50       12 $module = $filter =~ s/^\+// ? $filter : "Web::Scraper::Filter::$filter";
244 5 50       32 unless ($module->isa('Web::Scraper::Filter')) {
245 0 0       0 $module->require or Carp::croak("Loading $module: $@");
246             }
247 5     5   16 $callback = sub { $module->new->filter(shift) };
  5         18  
248             } elsif (blessed($filter) && $filter->can('filter')) {
249 1     1   5 $callback = sub { $filter->filter(shift) };
  1         4  
250             } else {
251 0         0 Carp::croak("Don't know filter type $filter");
252             }
253              
254 24         30 local $_ = $value;
255 24         29 my $retval = eval { $callback->($value) };
  24         256  
256 24 50       96 if ($@) {
257 0         0 Carp::croak("Filter $module had an error: $@");
258             }
259              
260 23     23   125 no warnings 'uninitialized';
  23         30  
  23         5579  
261             # sub { s/foo/bar/ } returns number or PL_sv_no which is stringified to ''
262 24 100 100     219 if (($retval =~ /^\d+$/ and $_ ne $value) or (defined($retval) and $retval eq '')) {
      100        
      66        
263 8         19 $value = $_;
264             } else {
265 16         18 $value = $retval;
266             }
267              
268 24         84 return $value;
269             }
270              
271             sub is_link_element {
272 5     5 0 7 my($node, $attr) = @_;
273 5   50     14 my $link_elements = $HTML::Tagset::linkElements{$node->tag} || [];
274 5         39 for my $elem (@$link_elements) {
275 5 50       27 return 1 if $attr eq $elem;
276             }
277 0         0 return;
278             }
279              
280             sub __stub {
281 69     69   78 my $func = shift;
282             return sub {
283 0     0     croak "Can't call $func() outside scraper block";
284 69         300 };
285             }
286              
287             *process = __stub 'process';
288             *process_first = __stub 'process_first';
289             *result = __stub 'result';
290              
291             1;
292             __END__