File Coverage

blib/lib/Web/Scraper.pm
Criterion Covered Total %
statement 147 178 82.5
branch 47 70 67.1
condition 29 44 65.9
subroutine 28 32 87.5
pod 0 7 0.0
total 251 331 75.8


line stmt bran cond sub pod time code
1             package Web::Scraper;
2 18     18   15461 use strict;
  18         27  
  18         496  
3 18     18   63 use warnings;
  18         20  
  18         409  
4 18     18   72 use Carp;
  18         22  
  18         1132  
5 18     18   71 use Scalar::Util qw(blessed);
  18         18  
  18         1334  
6 18     18   74 use List::Util qw(first);
  18         30  
  18         1463  
7 18     18   9200 use HTML::Entities;
  18         87151  
  18         1331  
8 18     18   8734 use HTML::Tagset;
  18         19205  
  18         685  
9 18     18   9986 use HTML::TreeBuilder::XPath;
  18         94297  
  18         176  
10 18     18   10252 use HTML::Selector::XPath;
  18         42528  
  18         841  
11 18     18   8439 use UNIVERSAL::require;
  18         19041  
  18         158  
12              
13             our $VERSION = '0.21_01';
14              
15             sub import {
16 18     18   157 my $class = shift;
17 18         37 my $pkg = caller;
18              
19 18     18   1019 no strict 'refs';
  18         29  
  18         10787  
20 18         37 *{"$pkg\::scraper"} = \&scraper;
  18         104  
21 18     40   62 *{"$pkg\::process"} = sub { goto &process };
  18         62  
  40         107  
22 18     1   45 *{"$pkg\::process_first"} = sub { goto &process_first };
  18         76  
  1         5  
23 18     31   51 *{"$pkg\::result"} = sub { goto &result };
  18         23147  
  31         94  
24             }
25              
26             our $UserAgent;
27              
28             sub __ua {
29 0     0   0 require LWP::UserAgent;
30 0   0     0 $UserAgent ||= LWP::UserAgent->new(agent => __PACKAGE__ . "/" . $VERSION);
31 0         0 $UserAgent;
32             }
33              
34             sub user_agent {
35 0     0 0 0 my $self = shift;
36 0 0       0 $self->{user_agent} = shift if @_;
37 0 0       0 $self->{user_agent} || __ua;
38             }
39              
40             sub define {
41 1     1 0 19 my($class, $coderef) = @_;
42 1         6 bless { code => $coderef }, $class;
43             }
44              
45             sub scraper(&) {
46 39     39 0 291 my($coderef) = @_;
47 39         153 bless { code => $coderef }, __PACKAGE__;
48             }
49              
50             sub scrape {
51 41     41 0 80 my $self = shift;
52 41         67 my($stuff, $current) = @_;
53              
54 41         52 my($html, $tree);
55              
56 41 50 66     476 if (blessed($stuff) && $stuff->isa('URI')) {
    100 66        
    100 66        
57 0         0 require Encode;
58 0         0 require HTTP::Response::Encoding;
59 0         0 my $ua = $self->user_agent;
60 0         0 my $res = $ua->get($stuff);
61 0 0       0 if ($res->is_success) {
62 0         0 my @encoding = (
63             $res->encoding,
64             # could be multiple because HTTP response and META might be different
65             ($res->header('Content-Type') =~ /charset=([\w\-]+)/g),
66             "latin-1",
67             );
68 0 0   0   0 my $encoding = first { defined $_ && Encode::find_encoding($_) } @encoding;
  0         0  
69 0         0 $html = Encode::decode($encoding, $res->content);
70             } else {
71 0         0 croak "GET $stuff failed: ", $res->status_line;
72             }
73 0         0 $current = $stuff->as_string;
74             } elsif (blessed($stuff) && $stuff->isa('HTML::Element')) {
75 3         15 $tree = $stuff->clone;
76             } elsif (ref($stuff) && ref($stuff) eq 'SCALAR') {
77 4         6 $html = $$stuff;
78             } else {
79 34         44 $html = $stuff;
80             }
81              
82 41   66     268 $tree ||= do {
83 38         305 my $t = HTML::TreeBuilder::XPath->new;
84 38         7389 $t->parse($html);
85 38         13124 $t;
86             };
87              
88 41         68 my $stash = {};
89 18     18   102 no warnings 'redefine';
  18         31  
  18         19010  
90 41         118 local *process = create_process(0, $tree, $stash, $current);
91 41         87 local *process_first = create_process(1, $tree, $stash, $current);
92              
93 41         60 my $retval;
94             local *result = sub {
95 31     31   46 $retval++;
96 31         58 my @keys = @_;
97              
98 31 50       71 if (@keys == 1) {
    0          
99 31         85 return $stash->{$keys[0]};
100             } elsif (@keys) {
101 0         0 my %res;
102 0         0 @res{@keys} = @{$stash}{@keys};
  0         0  
103 0         0 return \%res;
104             } else {
105 0         0 return $stash;
106             }
107 41         156 };
108              
109 41         257 my $ret = $self->{code}->($tree);
110 37         207 $tree->delete;
111              
112             # check user specified return value
113 37 100       2782 return $ret if $retval;
114              
115 6         186 return $stash;
116             }
117              
118             sub create_process {
119 82     82 0 95 my($first, $tree, $stash, $uri) = @_;
120              
121             sub {
122 41     41   86 my($exp, @attr) = @_;
123              
124 41 100       256 my $xpath = $exp =~ m!^/! ? $exp : HTML::Selector::XPath::selector_to_xpath($exp);
125 40         2991 my @nodes = eval {
126 40         262 local $SIG{__WARN__} = sub { };
127 40         210 $tree->findnodes($xpath);
128             };
129              
130 40 100       68561 if ($@) {
131 3         11 die "'$xpath' doesn't look like a valid XPath expression: $@";
132             }
133              
134 37 50       99 @nodes or return;
135 37 100       94 @nodes = ($nodes[0]) if $first;
136              
137 37         141 while (my($key, $val) = splice(@attr, 0, 2)) {
138 37 100       153 if (!defined $val) {
    100          
139 3 50 33     22 if (ref($key) && ref($key) eq 'CODE') {
140 3         10 for my $node (@nodes) {
141 5         44 local $_ = $node;
142 5         16 $key->($node);
143             }
144             } else {
145 0         0 die "Don't know what to do with $key => undef";
146             }
147             } elsif ($key =~ s!\[\]$!!) {
148 5         22 $stash->{$key} = [ map __get_value($_, $val, $uri), @nodes ];
149             } else {
150 29         80 $stash->{$key} = __get_value($nodes[0], $val, $uri);
151             }
152             }
153              
154 37         501 return;
155 82         446 };
156             }
157              
158             sub __get_value {
159 54     54   108 my($node, $val, $uri) = @_;
160              
161 54 50 66     629 if (ref($val) && ref($val) eq 'CODE') {
    100 66        
    100 66        
    100 100        
    100          
    100          
    50          
162 0         0 local $_ = $node;
163 0         0 return $val->($node);
164             } elsif (blessed($val) && $val->isa('Web::Scraper')) {
165 3         19 return $val->scrape($node, $uri);
166             } elsif ($val =~ s!^@!!) {
167 9         35 my $value = $node->attr($val);
168 9 100 66     131 if ($uri && is_link_element($node, $val)) {
169 5         1807 require URI;
170 5         10951 $value = URI->new_abs($value, $uri);
171             }
172 9         22210 return $value;
173             } elsif (lc($val) eq 'content' || lc($val) eq 'text') {
174 22 100       63 return $node->isTextNode ? $node->string_value : $node->as_text;
175             } elsif (lc($val) eq 'raw' || lc($val) eq 'html') {
176 6 100       17 if ($node->isTextNode) {
177             # xxx is this a bug? as_XML doesn't return encoded output
178 1         7 return HTML::Entities::encode($node->as_XML, q("'<>&));
179             }
180 5         29 my $html = $node->as_XML;
181 5         1172 $html =~ s!^<.*?>!!;
182 5         36 $html =~ s!\s*\n*$!!;
183 5         30 return $html;
184             } elsif (ref($val) eq 'HASH') {
185 3         4 my $values;
186 3         11 for my $key (keys %$val) {
187 5         77 $values->{$key} = __get_value($node, $val->{$key}, $uri);
188             }
189 3         22 return $values;
190             } elsif (ref($val) eq 'ARRAY') {
191 11         13 my $how = $val->[0];
192 11         21 my $value = __get_value($node, $how, $uri);
193 11         227 for my $filter (@$val[1..$#$val]) {
194 13         22 $value = run_filter($value, $filter);
195             }
196 11         49 return $value;
197             } else {
198 0         0 Carp::croak "Unknown value type $val";
199             }
200             }
201              
202             sub run_filter {
203 13     13 0 15 my($value, $filter) = @_;
204              
205             ## sub { s/foo/bar/g } is a valid filter
206             ## sub { DateTime::Format::Foo->parse_string(shift) } is valid too
207 13         14 my $callback;
208             my $module;
209              
210 13 100       20 if (ref($filter) eq 'CODE') {
    50          
211 8         9 $callback = $filter;
212 8         11 $module = "$filter";
213             } elsif (!ref($filter)) {
214 5 50       11 $module = $filter =~ s/^\+// ? $filter : "Web::Scraper::Filter::$filter";
215 5 50       25 unless ($module->isa('Web::Scraper::Filter')) {
216 0 0       0 $module->require or Carp::croak("Loading $module: $@");
217             }
218              
219 5     5   15 $callback = sub { $module->new->filter(shift) };
  5         15  
220             } else {
221 0         0 Carp::croak("Don't know filter type $filter");
222             }
223              
224 13         14 local $_ = $value;
225 13         16 my $retval = eval { $callback->($value) };
  13         168  
226 13 50       38 if ($@) {
227 0         0 Carp::croak("Filter $module had an error: $@");
228             }
229              
230 18     18   113 no warnings 'uninitialized';
  18         30  
  18         4649  
231 13 100 100     77 if (($retval =~ /^\d+$/ and $_ ne $value) or (defined $retval and !$retval)) {
      100        
      66        
232 7         8 $value = $_;
233             } else {
234 6         4 $value = $retval;
235             }
236              
237 13         32 return $value;
238             }
239              
240             sub is_link_element {
241 5     5 0 10 my($node, $attr) = @_;
242 5   50     20 my $link_elements = $HTML::Tagset::linkElements{$node->tag} || [];
243 5         42 for my $elem (@$link_elements) {
244 5 50       31 return 1 if $attr eq $elem;
245             }
246 0         0 return;
247             }
248              
249             sub __stub {
250 54     54   71 my $func = shift;
251             return sub {
252 0     0     croak "Can't call $func() outside scraper block";
253 54         202 };
254             }
255              
256             *process = __stub 'process';
257             *process_first = __stub 'process_first';
258             *result = __stub 'result';
259              
260             1;
261             __END__