File Coverage

blib/lib/Novel/Robot/Browser.pm
Criterion Covered Total %
statement 73 172 42.4
branch 9 84 10.7
condition 12 75 16.0
subroutine 17 21 80.9
pod 3 8 37.5
total 114 360 31.6


line stmt bran cond sub pod time code
1             # ABSTRACT: get/post url, return unicode content, auto detect CJK charset
2             package Novel::Robot::Browser;
3            
4 3     3   18 use strict;
  3         7  
  3         115  
5 3     3   12 use warnings;
  3         6  
  3         163  
6 3     3   15 use utf8;
  3         6  
  3         19  
7            
8             our $VERSION = 0.22;
9            
10             #use Data::Dumper;
11            
12 3     3   6810 use Encode::Detect::CJK qw/detect/;
  3         161180  
  3         280  
13 3     3   60 use Encode;
  3         6  
  3         287  
14 3     3   2176 use File::Slurp qw/slurp/;
  3         108225  
  3         289  
15 3     3   1856 use HTTP::CookieJar;
  3         108960  
  3         188  
16 3     3   2559 use HTTP::Tiny;
  3         120932  
  3         201  
17 3     3   2103 use IO::Uncompress::Gunzip qw(gunzip);
  3         137339  
  3         327  
18             #use Parallel::ForkManager;
19 3     3   2100 use Term::ProgressBar;
  3         230792  
  3         185  
20 3     3   33 use URI::Escape;
  3         7  
  3         309  
21 3     3   22 use URI;
  3         7  
  3         7901  
22            
23             our $DEFAULT_URL_CONTENT = '';
24             our %DEFAULT_HEADER = (
25             'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
26             'Accept-Charset' => 'gb2312,utf-8;q=0.7,*;q=0.7',
27             'Accept-Encoding' => "gzip",
28             'Accept-Language' => 'zh,zh-cn;q=0.8,en-us;q=0.5,en;q=0.3',
29             'Connection' => 'keep-alive',
30             #'User-Agent' => 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0',
31             'User-Agent' => 'User-Agent: MQQBrowser/26 Mozilla/5.0 (Linux; U; Android 2.3.7; zh-cn; MB200 Build/GRJ22; CyanogenMod-7) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1',
32             'DNT' => 1,
33             );
34            
35             sub new {
36 2     2 0 13 my ( $self, %opt ) = @_;
37 2   50     38 $opt{retry} ||= 5;
38 2   50     8 $opt{max_process_num} ||= 5;
39 2   33     18 $opt{browser} ||= _init_browser( $opt{browser_headers} );
40 2   50     18 $opt{use_chrome} ||= 0;
41 2         18 bless {%opt}, __PACKAGE__;
42             }
43            
44             sub _init_browser {
45 2     2   8 my ( $headers ) = @_;
46            
47 2   50     14 $headers ||= {};
48 2         39 my %h = ( %DEFAULT_HEADER, %$headers );
49            
50             #my $cookie_jar = Novel::Robot::Browser::CookieJar->new();
51 2         27 my $cookie_jar = HTTP::CookieJar->new;
52            
53 2         34 my $http = HTTP::Tiny->new(
54             default_headers => \%h,
55             cookie_jar => $cookie_jar,
56             );
57            
58 2         321 return $http;
59             }
60            
61             sub request_url_whole {
62 0     0 1 0 my ( $self, $url, %o ) = @_;
63            
64 0         0 my $html = $self->request_url( $url, $o{post_data} );
65            
66 0   0     0 my $info = $o{info_sub}->( \$html ) || {};
67 0   0     0 my $data_list = $o{item_list} || $o{item_list_sub}->( \$html ) || [];
68            
69 0         0 my $i = 1;
70 0 0 0     0 unless ( $o{stop_sub} and $o{stop_sub}->( $info, $data_list, $i, %o ) or defined $o{item_list}) {
      0        
71 0 0 0     0 $data_list = [] if ( $o{min_page_num} and $o{min_page_num} > 1 );
72 0 0       0 my $page_list = exists $o{page_list_sub} ? $o{page_list_sub}->( \$html ) : undef;
73 0         0 while ( 1 ) {
74 0         0 $i++;
75             my $u =
76             $page_list ? $page_list->[ $i - 2 ] :
77 0 0       0 ( exists $o{next_page_sub} ? $o{next_page_sub}->( $url, $i, \$html ) : undef );
    0          
78 0 0       0 last unless ( $u );
79 0 0 0     0 next if ( $o{min_page_num} and $i < $o{min_page_num} );
80 0 0 0     0 last if ( $o{max_page_num} and $i > $o{max_page_num} );
81            
82            
83 0 0       0 my ( $u_url, $u_post_data ) = ref( $u ) eq 'HASH' ? @{$u}{qw/url post_data/} : ( $u, undef );
  0         0  
84 0         0 my $c = $self->request_url( $u_url, $u_post_data );
85 0         0 my $fs = $o{item_list_sub}->( \$c );
86 0 0       0 last unless ( $fs );
87            
88 0         0 push @$data_list, @$fs;
89 0 0 0     0 last if ( $o{stop_sub} and $o{stop_sub}->( $info, $data_list, $i, %o ) );
90             }
91             } ## end unless ( $o{stop_sub} and ...)
92            
93             #lofter倒序
94 0 0       0 if ( $o{reverse_item_list} ){
95 0         0 $data_list = [ reverse @$data_list ];
96 0         0 my $max_id = $data_list->[0]{id};
97 0 0       0 if($max_id){
98 0         0 $_->{id} = $max_id - $_->{id} +1 for(@$data_list);
99             }
100             }
101 0 0 0     0 $info->{item_num} = ( $#$data_list >= 0 and exists $data_list->[-1]{id} ) ? $data_list->[-1]{id} : ( scalar( @$data_list ) || $i );
      0        
102            
103 0 0       0 if ( $o{item_sub} ) {
104 0         0 my $item_id = 0;
105 0 0       0 print "\n\n" if ( $o{progress} );
106 0         0 my $progress;
107 0 0       0 $progress = Term::ProgressBar->new( { count => scalar(@$data_list) } ) if ( $o{progress} );
108            
109 0         0 for my $i ( 0 .. $#$data_list ) {
110 0         0 my $r = $data_list->[$i];
111 0   0     0 $r->{id} //= ++$item_id;
112 0         0 $r->{url} = URI->new_abs( $r->{url}, $url )->as_string;
113 0 0       0 next unless ( $self->is_item_in_range( $r->{id}, $o{min_item_num}, $o{max_item_num} ) );
114 0 0       0 if(exists $o{back_index}){
115 0 0       0 last if($i + $o{back_index} > $#$data_list);
116             }
117            
118 0 0       0 if($r->{url}){
119 0         0 my $c = $self->request_url( $r->{url}, $r->{post_data} );
120 0         0 my $temp_r = $o{item_sub}->( \$c );
121 0   0     0 $r->{$_} //= $temp_r->{$_} for keys(%$temp_r);
122             }else{
123 0         0 $r = $o{item_sub}->( $r );
124             }
125            
126 0         0 my $next_url = URI->new_abs( $data_list->[$i+1]->{url}, $url )->as_string;
127 0         0 while($r->{next_url}){
128 0         0 $r->{next_url} = URI->new_abs( $r->{next_url}, $url )->as_string;
129 0 0       0 if($r->{next_url} ne $next_url){
130 0         0 my $c = $self->request_url( $r->{next_url}, $r->{post_data} );
131 0         0 my $temp_r = $o{item_sub}->( \$c );
132 0         0 $r->{content} .= "\n\n".$temp_r->{content};
133 0 0       0 last unless(exists $temp_r->{next_url});
134 0         0 $r->{next_url} = $temp_r->{next_url};
135             }else{
136 0         0 last;
137             }
138             }
139            
140 0 0       0 $progress->update( $item_id ) if ( $o{progress} );
141             }
142            
143 0 0       0 $progress->update( scalar(@$data_list) ) if ( $o{progress} );
144             }
145 0 0       0 print "\n\n" if ( $o{progress} );
146 0         0 return ( $info, $data_list );
147             } ## end sub request_url_whole
148            
149             sub is_item_in_range {
150 6     6 0 23 my ( $self, $id, $min, $max ) = @_;
151 6 50       18 return 1 unless ( $id );
152 6 50 33     35 return 0 if ( $min and $id < $min );
153 6 50 33     16 return 0 if ( $max and $id > $max );
154 6         26 return 1;
155             }
156            
157             sub is_list_overflow {
158 0     0 0 0 my ( $self, $r, $max ) = @_;
159            
160 0 0       0 return unless ( $max );
161            
162 0         0 my $item_num = scalar( @$r );
163 0   0     0 my $id = $r->[-1]{id} // $item_num;
164            
165 0 0       0 return if ( $id < $max );
166            
167 0         0 $#{$r} = $max - 1;
  0         0  
168 0         0 return 1;
169             }
170            
171             sub request_url {
172 2     2 1 2280 my ( $self, $url, $form ) = @_;
173 2 50       6 return $DEFAULT_URL_CONTENT unless ( $url );
174            
175 2         4 my $c;
176 2         8 for my $i ( 1 .. $self->{retry} ) {
177 2         5 eval { $c = $self->request_url_simple( $url, $form ); };
  2         7  
178 2 50       12 last if ( $c );
179 0         0 sleep 2;
180             }
181            
182 2   33     20 return $c || $DEFAULT_URL_CONTENT;
183             }
184            
185             sub format_post_content {
186 0     0 0 0 my ( $self, $form ) = @_;
187            
188 0 0       0 return $form unless ( ref( $form ) eq 'HASH' );
189            
190 0         0 my @params;
191 0         0 while ( my ( $k, $v ) = each %$form ) {
192 0         0 push @params, uri_escape( $k ) . "=" . uri_escape( $v );
193             }
194            
195 0         0 my $post_str = join( "&", @params );
196 0         0 return $post_str;
197             }
198            
199             sub request_url_simple {
200 2     2 0 6 my ( $self, $url, $form ) = @_;
201            
202 2         4 my $res;
203 2 50       9 if ( $form ) {
    50          
204             $res = $self->{browser}->request(
205 0         0 'POST', $url,
206             { content => $self->format_post_content( $form ),
207             headers => {
208             'content-type' => 'application/x-www-form-urlencoded',
209             },
210             } );
211             } elsif ( $self->{use_chrome} ) {
212 0         0 $res->{content} = `chrome --no-sandbox --user-data-dir --headless --disable-gpu --dump-dom "$url" 2>/dev/null`;
213 0         0 $res->{success} = 1;
214             } else {
215 2         90 $res = $self->{browser}->get( $url );
216             }
217 2 50       1174787 return $DEFAULT_URL_CONTENT unless ( $res->{success} );
218            
219 2         5 my $html;
220 2         5 my $content = $res->{content};
221 2 50 33     16 if ( $res->{headers}{'content-encoding'}
222             and $res->{headers}{'content-encoding'} eq 'gzip' ) {
223 2         19 gunzip \$content => \$html, MultiStream => 1, Append => 1;
224             }
225            
226 2   33     6658 my $charset = detect( $html || $content );
227 2   33     1409545 my $r = decode( $charset, $html || $content, Encode::FB_XMLCREF );
228            
229 2   33     13746 return $r || $DEFAULT_URL_CONTENT;
230             } ## end sub request_url_simple
231            
232             sub read_moz_cookie {
233 0     0 1   my ( $self, $cookie, $dom ) = @_;
234            
235 0           my @segment;
236 0 0 0       if ( -f $cookie and $cookie =~ /\.sqlite$/ ) { # firefox sqlite3
    0 0        
237 0           my $sqlite3_cookie =
238             `sqlite3 "$cookie" "select host,isSecure,path,isHttpOnly,expiry,name,value from moz_cookies where baseDomain='$dom'"`;
239 0           @segment = map { [ split /\|/ ] } split /\n/, $sqlite3_cookie;
  0            
240             } elsif ( -f $cookie and $cookie =~ /\.txt$/ ) { # Netscape HTTP Cookie File
241 0           my @ck = slurp( $cookie );
242 0 0         @segment = grep { $_->[0] and $_->[0] =~ /(^|\.)\Q$dom\E$/ } map { [ split /\s+/ ] } @ck;
  0            
  0            
243             } else { # cookie string : name1=value1; name2=value2
244 0           my @ck = split /;\s*/, $cookie;
245 0           @segment = map { my @c = split /=/; [ $dom, undef, '/', undef, 0, $c[0], $c[1] ] } @ck;
  0            
  0            
246             }
247            
248            
249 0 0         @segment = grep { defined $_->[6] and $_->[6] =~ /\S/ } @segment;
  0            
250            
251 0           my @jar = map { "$_->[5]=$_->[6]; Domain=$_->[0]; Path=$_->[2]; Expiry=$_->[4]" } @segment;
  0            
252 0           $self->{browser}{cookie_jar}->load_cookies( @jar );
253            
254 0           $cookie = join( "; ", map { "$_->[5]=$_->[6]" } @segment );
  0            
255            
256             #$self->{browser}{cookie_jar}{cookie} = $cookie;
257            
258 0           return $cookie;
259            
260             } ## end sub read_moz_cookie
261            
262             1;