File Coverage

blib/lib/Web/Scraper.pm
Criterion Covered Total %
statement 25 27 92.5
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 34 36 94.4


line stmt bran cond sub pod time code
1             package Web::Scraper;
2 5     5   237011 use strict;
  5         14  
  5         329  
3 5     5   29 use warnings;
  5         11  
  5         324  
4 5     5   132 use 5.008001;
  5         22  
  5         210  
5 5     5   28 use Carp;
  5         8  
  5         509  
6 5     5   26 use Scalar::Util qw(blessed);
  5         10  
  5         556  
7 5     5   29 use List::Util qw(first);
  5         8  
  5         927  
8 5     5   5406 use HTML::Entities;
  5         55679  
  5         770  
9 5     5   16709 use HTML::Tagset;
  5         9071  
  5         257  
10 5     5   9225 use HTML::TreeBuilder::XPath;
  0            
  0            
11             use HTML::Selector::XPath;
12             use UNIVERSAL::require;
13              
14             our $VERSION = '0.37';
15              
16             sub import {
17             my $class = shift;
18             my $pkg = caller;
19              
20             no strict 'refs';
21             no warnings 'redefine';
22             *{"$pkg\::scraper"} = _build_scraper($class);
23             *{"$pkg\::process"} = sub { goto &process };
24             *{"$pkg\::process_first"} = sub { goto &process_first };
25             *{"$pkg\::result"} = sub { goto &result };
26             }
27              
28             our $UserAgent;
29              
30             sub __ua {
31             require LWP::UserAgent;
32             $UserAgent ||= LWP::UserAgent->new(agent => __PACKAGE__ . "/" . $VERSION);
33             $UserAgent;
34             }
35              
36             sub user_agent {
37             my $self = shift;
38             $self->{user_agent} = shift if @_;
39             $self->{user_agent} || __ua;
40             }
41              
42             sub define {
43             my($class, $coderef) = @_;
44             bless { code => $coderef }, $class;
45             }
46              
47             sub _build_scraper {
48             my $class = shift;
49             return sub(&) {
50             my($coderef) = @_;
51             bless { code => $coderef }, $class;
52             };
53             }
54              
55             sub scrape {
56             my $self = shift;
57             my($stuff, $current) = @_;
58              
59             my($html, $tree);
60              
61             if (blessed($stuff) && $stuff->isa('URI')) {
62             my $ua = $self->user_agent;
63             my $res = $ua->get($stuff);
64             return $self->scrape($res, $stuff->as_string);
65             } elsif (blessed($stuff) && $stuff->isa('HTTP::Response')) {
66             if ($stuff->is_success) {
67             $html = $stuff->decoded_content;
68             } else {
69             croak "GET " . $stuff->request->uri . " failed: ", $stuff->status_line;
70             }
71             $current ||= $stuff->request->uri;
72             } elsif (blessed($stuff) && $stuff->isa('HTML::Element')) {
73             $tree = $stuff->clone;
74             } elsif (ref($stuff) && ref($stuff) eq 'SCALAR') {
75             $html = $$stuff;
76             } else {
77             $html = $stuff;
78             }
79              
80             $tree ||= $self->build_tree($html);
81              
82             my $stash = {};
83             no warnings 'redefine';
84             local *process = create_process(0, $tree, $stash, $current);
85             local *process_first = create_process(1, $tree, $stash, $current);
86              
87             my $retval;
88             local *result = sub {
89             $retval++;
90             my @keys = @_;
91              
92             if (@keys == 1) {
93             return $stash->{$keys[0]};
94             } elsif (@keys) {
95             my %res;
96             @res{@keys} = @{$stash}{@keys};
97             return \%res;
98             } else {
99             return $stash;
100             }
101             };
102              
103             my $ret = $self->{code}->($tree);
104             $tree->delete;
105              
106             # check user specified return value
107             return $ret if $retval;
108              
109             return $stash;
110             }
111              
112             sub build_tree {
113             my($self, $html) = @_;
114              
115             my $t = HTML::TreeBuilder::XPath->new;
116             $t->store_comments(1) if ($t->can('store_comments'));
117             $t->ignore_unknown(0);
118             $t->parse($html);
119             $t->eof;
120             $t;
121             }
122              
123             sub create_process {
124             my($first, $tree, $stash, $uri) = @_;
125              
126             sub {
127             my($exp, @attr) = @_;
128              
129             my $xpath = $exp =~ m!^(?:/|id\()! ? $exp : HTML::Selector::XPath::selector_to_xpath($exp);
130             my @nodes = eval {
131             local $SIG{__WARN__} = sub { };
132             $tree->findnodes($xpath);
133             };
134              
135             if ($@) {
136             die "'$xpath' doesn't look like a valid XPath expression: $@";
137             }
138              
139             @nodes or return;
140             @nodes = ($nodes[0]) if $first;
141              
142             while (my($key, $val) = splice(@attr, 0, 2)) {
143             if (!defined $val) {
144             if (ref($key) && ref($key) eq 'CODE') {
145             for my $node (@nodes) {
146             local $_ = $node;
147             $key->($node);
148             }
149             } else {
150             die "Don't know what to do with $key => undef";
151             }
152             } elsif ($key =~ s!\[\]$!!) {
153             $stash->{$key} = [ map __get_value($_, $val, $uri), @nodes ];
154             } else {
155             $stash->{$key} = __get_value($nodes[0], $val, $uri);
156             }
157             }
158              
159             return;
160             };
161             }
162              
163             sub __get_value {
164             my($node, $val, $uri) = @_;
165              
166             if (ref($val) && ref($val) eq 'CODE') {
167             local $_ = $node;
168             return $val->($node);
169             } elsif (blessed($val) && $val->isa('Web::Scraper')) {
170             return $val->scrape($node, $uri);
171             } elsif ($val =~ s!^@!!) {
172             my $value = $node->attr($val);
173             if ($uri && is_link_element($node, $val)) {
174             require URI;
175             $value = URI->new_abs($value, $uri);
176             }
177             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             return $node->isTextNode
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             if ($node->isTextNode) {
191             if ($HTML::TreeBuilder::XPath::VERSION < 0.09) {
192             return HTML::Entities::encode($node->as_XML, q("'<>&));
193             } else {
194             return $node->as_XML;
195             }
196             }
197             my $html = $node->as_XML;
198             $html =~ s!^<.*?>!!;
199             $html =~ s!\s*\n*$!!;
200             return $html;
201             } elsif (ref($val) eq 'HASH') {
202             my $values;
203             for my $key (keys %$val) {
204             $values->{$key} = __get_value($node, $val->{$key}, $uri);
205             }
206             return $values;
207             } elsif (ref($val) eq 'ARRAY') {
208             my $how = $val->[0];
209             my $value = __get_value($node, $how, $uri);
210             for my $filter (@$val[1..$#$val]) {
211             $value = run_filter($value, $filter);
212             }
213             return $value;
214             } else {
215             Carp::croak "Unknown value type $val";
216             }
217             }
218              
219             sub run_filter {
220             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             my $callback;
225             my $module;
226              
227             if (ref($filter) eq 'CODE') {
228             $callback = $filter;
229             $module = "$filter";
230             } elsif (!ref($filter)) {
231             $module = $filter =~ s/^\+// ? $filter : "Web::Scraper::Filter::$filter";
232             unless ($module->isa('Web::Scraper::Filter')) {
233             $module->require or Carp::croak("Loading $module: $@");
234             }
235             $callback = sub { $module->new->filter(shift) };
236             } elsif (blessed($filter) && $filter->can('filter')) {
237             $callback = sub { $filter->filter(shift) };
238             } else {
239             Carp::croak("Don't know filter type $filter");
240             }
241              
242             local $_ = $value;
243             my $retval = eval { $callback->($value) };
244             if ($@) {
245             Carp::croak("Filter $module had an error: $@");
246             }
247              
248             no warnings 'uninitialized';
249             # sub { s/foo/bar/ } returns number or PL_sv_no which is stringified to ''
250             if (($retval =~ /^\d+$/ and $_ ne $value) or (defined($retval) and $retval eq '')) {
251             $value = $_;
252             } else {
253             $value = $retval;
254             }
255              
256             return $value;
257             }
258              
259             sub is_link_element {
260             my($node, $attr) = @_;
261             my $link_elements = $HTML::Tagset::linkElements{$node->tag} || [];
262             for my $elem (@$link_elements) {
263             return 1 if $attr eq $elem;
264             }
265             return;
266             }
267              
268             sub __stub {
269             my $func = shift;
270             return sub {
271             croak "Can't call $func() outside scraper block";
272             };
273             }
274              
275             *process = __stub 'process';
276             *process_first = __stub 'process_first';
277             *result = __stub 'result';
278              
279             1;
280             __END__