spider 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7286f875f41881c9c8c385987c322b9832e5b07c6147aa4910a900e59015927e
4
- data.tar.gz: 145ecd718a34521c17f0f9e939cb66f964ca0a1f93533969caf7d54426b213a8
3
+ metadata.gz: 393d7a240dbf77360ca4aeac52e8f162443903bac06817bbbf12440e597221f8
4
+ data.tar.gz: a26ff2a1f661fcde2712dc11f4a7fc0a9920e2c111bde01ef07b2135f0428c15
5
5
  SHA512:
6
- metadata.gz: ba771c7dbbe3df286475a5586ba2d2b63affe762b7bf504694e6865e39c8e7f5047811ac97285a1edc5d99cd478993fd2be9ee3ae244cc1690975f7ab3f0e779
7
- data.tar.gz: 2bd17f25db36a267b5534ff663b8394e7439a1701970e6d23ea295818732133b23c4bb35f82aa1f6f90022edcaceafc47b3fee3f7902fd9028a2ec8ad697a2a4
6
+ metadata.gz: 72498a31555641b00d448d28cf7a20e652a2824c7891b9f0ca7a04c126d0481f3df98267c876e63f01d1103c56e6a747e448bb4968cdbab9245806dfdf25ca77
7
+ data.tar.gz: 141feb5b9446ad7f40c2c79ddbb1b43e70a93a6347115c5bbc96a2a55b47d0eef096b99f9586d3ca122d93d0e48e04b4d0b4f44b4316475053256f6645a6837c
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.7.0
1
+ 0.8.0
@@ -15,14 +15,14 @@ class IncludedInFile
15
15
  def initialize(filepath)
16
16
  @filepath = filepath
17
17
  # create file if not exists
18
- File.write(@filepath, '') unless File.file?(@filepath)
18
+ File.write(@filepath, "") unless File.file?(@filepath)
19
19
  @urls = File.readlines(@filepath).map(&:chomp)
20
20
  end
21
21
 
22
22
  # Add an item to the file & array of URL.
23
23
  def <<(v)
24
24
  @urls << v.to_s
25
- File.write(@filepath, "#{v}\r\n", File.size(@filepath), mode: 'a')
25
+ File.write(@filepath, "#{v}\r\n", File.size(@filepath), mode: "a")
26
26
  end
27
27
 
28
28
  # True if the item is in the file.
@@ -1,6 +1,6 @@
1
1
  # Use memcached to track cycles.
2
2
 
3
- require 'dalli'
3
+ require "dalli"
4
4
 
5
5
  # A specialized class using memcached to track items stored. It supports
6
6
  # three operations: new, <<, and include? . Together these can be used to
@@ -1,7 +1,7 @@
1
1
  # Use Redis to track cycles.
2
2
 
3
- require 'redis'
4
- require 'json'
3
+ require "redis"
4
+ require "json"
5
5
 
6
6
  # A specialized class using Redis to track items stored. It supports
7
7
  # three operations: new, <<, and include? . Together these can be used to
@@ -1,8 +1,8 @@
1
1
  # Use AmazonSQS to track nodes to visit.
2
2
 
3
- require 'rubygems'
4
- require 'right_aws'
5
- require 'yaml'
3
+ require "rubygems"
4
+ require "right_aws"
5
+ require "yaml"
6
6
 
7
7
  # A specialized class using AmazonSQS to track nodes to walk. It supports
8
8
  # two operations: push and pop . Together these can be used to
@@ -20,7 +20,7 @@ class NextUrlsInSQS
20
20
  # Construct a new NextUrlsInSQS instance. All arguments here are
21
21
  # passed to RightAWS::SqsGen2 (part of the right_aws gem) or used
22
22
  # to set the AmazonSQS queue name (optional).
23
- def initialize(aws_access_key, aws_secret_access_key, queue_name = 'ruby-spider')
23
+ def initialize(aws_access_key, aws_secret_access_key, queue_name = "ruby-spider")
24
24
  @sqs = RightAws::SqsGen2.new(aws_access_key, aws_secret_access_key)
25
25
  @queue = @sqs.queue(queue_name)
26
26
  end
@@ -30,14 +30,14 @@ class NextUrlsInSQS
30
30
  def pop
31
31
  while true
32
32
  message = @queue.pop
33
- return YAML::load(message.to_s) unless message.nil?
33
+ return YAML.load(message.to_s) unless message.nil?
34
34
  sleep 5
35
35
  end
36
36
  end
37
37
 
38
38
  # Put data on the queue. Data is encoded with YAML.
39
39
  def push(a_msg)
40
- encoded_message = YAML::dump(a_msg)
40
+ encoded_message = YAML.dump(a_msg)
41
41
  @queue.push(a_msg)
42
42
  end
43
43
  end
@@ -11,13 +11,13 @@ require "uri"
11
11
 
12
12
  # Based on Perl's WWW::RobotRules module, by Gisle Aas.
13
13
  class RobotRules
14
- def initialize( user_agent )
14
+ def initialize(user_agent)
15
15
  @user_agent = user_agent.scan(/\S+/).first.sub(%r{/.*},
16
16
  "").downcase
17
17
  @rules = Hash.new { |rules, rule| rules[rule] = Array.new }
18
18
  end
19
19
 
20
- def parse( text_uri, robots_data )
20
+ def parse(text_uri, robots_data)
21
21
  uri = URI.parse(text_uri)
22
22
  location = "#{uri.host}:#{uri.port}"
23
23
  @rules.delete(location)
@@ -69,12 +69,12 @@ uri.scheme
69
69
  end
70
70
  end
71
71
 
72
- def allowed?( text_uri )
72
+ def allowed?(text_uri)
73
73
  uri = URI.parse(text_uri)
74
74
  location = "#{uri.host}:#{uri.port}"
75
75
  path = uri.path
76
76
 
77
- return true unless %w{http https}.include?(uri.scheme)
77
+ return true unless %w[http https].include?(uri.scheme)
78
78
 
79
79
  not @rules[location].any? { |rule| path.index(rule) == 0 }
80
80
  end
@@ -1,41 +1,51 @@
1
1
  # Specialized spidering rules.
2
2
 
3
- require File.dirname(__FILE__)+'/robot_rules.rb'
4
- require 'open-uri'
5
- require 'uri'
6
- require 'net/http'
7
- require 'net/https'
3
+ require File.dirname(__FILE__)+"/robot_rules.rb"
4
+ require "open-uri"
5
+ require "uri"
6
+ require "net/http"
7
+ require "net/https"
8
8
 
9
- module Net #:nodoc:
10
- class HTTPResponse #:nodoc:
9
+ module Net # :nodoc:
10
+ class HTTPResponse # :nodoc:
11
11
  def success?; false; end
12
12
  def redirect?; false; end
13
13
  end
14
- class HTTPSuccess #:nodoc:
14
+ class HTTPSuccess # :nodoc:
15
15
  def success?; true; end
16
16
  end
17
- class HTTPRedirection #:nodoc:
17
+ class HTTPRedirection # :nodoc:
18
18
  def redirect?; true; end
19
19
  end
20
20
  end
21
21
 
22
- class NilClass #:nodoc:
23
- def merge(h); h; end
24
- end
25
-
26
22
  class SpiderInstance
27
- def initialize(next_urls, seen = [], rules = nil, robots_seen = []) #:nodoc:
28
- @url_checks = []
29
- @cache = :memory
30
- @callbacks = {}
31
- @next_urls = [next_urls]
32
- @seen = seen
33
- @rules = rules || RobotRules.new("Ruby Spider #{Spider::VERSION}")
34
- @robots_seen = robots_seen
35
- @headers = {}
36
- @setup = nil
37
- @teardown = nil
38
- @interrupted = false
23
+ # Matches an anchor tag's attribute string carrying a rel value that should
24
+ # not be followed (nofollow, sponsored, or ugc), regardless of quoting.
25
+ NOFOLLOW_REL = /rel\s*=\s*["']?[^"'>]*\b(?:nofollow|sponsored|ugc)\b/i
26
+
27
+ # Seconds to wait before each HTTP request. Use to be polite to servers.
28
+ # s.crawl_delay = 1
29
+ #
30
+ # Maximum number of pages to fetch before stopping. nil means no limit.
31
+ # s.max_urls = 100
32
+ attr_writer :crawl_delay, :max_urls
33
+
34
+ def initialize(next_urls, seen = [], rules = nil, robots_seen = []) # :nodoc:
35
+ @url_checks = []
36
+ @cache = :memory
37
+ @callbacks = {}
38
+ @next_urls = [ next_urls ]
39
+ @seen = seen
40
+ @rules = rules || RobotRules.new("Ruby Spider #{Spider::VERSION}")
41
+ @robots_seen = robots_seen
42
+ @headers = {}
43
+ @setup = nil
44
+ @teardown = nil
45
+ @interrupted = false
46
+ @crawl_delay = 0
47
+ @max_urls = nil
48
+ @urls_fetched = 0
39
49
  end
40
50
 
41
51
  # Add a predicate that determines whether to continue down this URL's path.
@@ -70,7 +80,7 @@ class SpiderInstance
70
80
  if cacher.respond_to?(:<<) && cacher.respond_to?(:include?)
71
81
  @seen = cacher
72
82
  else
73
- raise ArgumentError, 'expected something that responds to << and included?'
83
+ raise ArgumentError, "expected something that responds to << and included?"
74
84
  end
75
85
  end
76
86
 
@@ -149,10 +159,10 @@ class SpiderInstance
149
159
  HeaderSetter.new(self)
150
160
  end
151
161
 
152
- def raw_headers #:nodoc:
162
+ def raw_headers # :nodoc:
153
163
  @headers
154
164
  end
155
- def raw_headers=(v) #:nodoc:
165
+ def raw_headers=(v) # :nodoc:
156
166
  @headers = v
157
167
  end
158
168
 
@@ -161,14 +171,14 @@ class SpiderInstance
161
171
  @headers = {}
162
172
  end
163
173
 
164
- def start! #:nodoc:
174
+ def start! # :nodoc:
165
175
  trap("SIGINT") { @interrupted = true }
166
176
  begin
167
177
  next_urls = @next_urls.pop
168
178
  next_urls.each do |prior_url, urls|
169
- urls = [urls] unless urls.kind_of?(Array)
179
+ urls = [ urls ] unless urls.kind_of?(Array)
170
180
  urls.map do |a_url|
171
- [a_url, (URI.parse(a_url) rescue nil)]
181
+ [ a_url, (URI.parse(a_url) rescue nil) ]
172
182
  end.select do |a_url, parsed_url|
173
183
  allowable_url?(a_url, parsed_url)
174
184
  end.each do |a_url, parsed_url|
@@ -180,17 +190,18 @@ class SpiderInstance
180
190
  end
181
191
  end
182
192
  @teardown.call(a_url) unless @teardown.nil?
193
+ @interrupted = true if @max_urls && @urls_fetched >= @max_urls
183
194
  break if @interrupted
184
195
  end
185
196
  end
186
197
  end while !@next_urls.empty? && !@interrupted
187
198
  end
188
199
 
189
- def stop! #:nodoc:
200
+ def stop! # :nodoc:
190
201
  @interrupted = true
191
202
  end
192
203
 
193
- def success_or_failure(code) #:nodoc:
204
+ def success_or_failure(code) # :nodoc:
194
205
  if code > 199 && code < 300
195
206
  :success
196
207
  else
@@ -198,23 +209,23 @@ class SpiderInstance
198
209
  end
199
210
  end
200
211
 
201
- def allowable_url?(a_url, parsed_url) #:nodoc:
212
+ def allowable_url?(a_url, parsed_url) # :nodoc:
202
213
  !parsed_url.nil? && !@seen.include?(parsed_url) && allowed?(a_url, parsed_url) &&
203
- @url_checks.map{|url_check|url_check.call(a_url)}.all?
214
+ @url_checks.map { |url_check|url_check.call(a_url) }.all?
204
215
  end
205
216
 
206
217
  # True if the robots.txt for that URL allows access to it.
207
218
  def allowed?(a_url, parsed_url) # :nodoc:
208
- return false unless ['http','https'].include?(parsed_url.scheme)
219
+ return false unless [ "http", "https" ].include?(parsed_url.scheme)
209
220
  u = "#{parsed_url.scheme}://#{parsed_url.host}:#{parsed_url.port}/robots.txt"
210
221
  parsed_u = URI.parse(u)
211
- return false unless @url_checks.map{|url_check|url_check.call(a_url)}.all?
222
+ return false unless @url_checks.map { |url_check|url_check.call(a_url) }.all?
212
223
  begin
213
224
  unless @robots_seen.include?(u)
214
- #open(u, 'User-Agent' => 'Ruby Spider',
225
+ # open(u, 'User-Agent' => 'Ruby Spider',
215
226
  # 'Accept' => 'text/html,text/xml,application/xml,text/plain', :ssl_verify => false) do |url|
216
227
  # @rules.parse(u, url.read)
217
- #end
228
+ # end
218
229
  get_page(parsed_u) do |r|
219
230
  @rules.parse(u, r.body)
220
231
  end
@@ -223,24 +234,26 @@ class SpiderInstance
223
234
  @rules.allowed?(a_url)
224
235
  rescue OpenURI::HTTPError
225
236
  true # No robots.txt
226
- rescue Exception, Timeout::Error # to keep it from crashing
237
+ rescue StandardError, Timeout::Error # to keep it from crashing
227
238
  false
228
239
  end
229
240
  end
230
241
 
231
- def get_page(parsed_url, &block) #:nodoc:
242
+ def get_page(parsed_url, &block) # :nodoc:
232
243
  @seen << parsed_url
233
244
  begin
245
+ sleep @crawl_delay if @crawl_delay && @crawl_delay > 0
234
246
  http = Net::HTTP.new(parsed_url.host, parsed_url.port)
235
- if parsed_url.scheme == 'https'
247
+ if parsed_url.scheme == "https"
236
248
  http.use_ssl = true
237
249
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
238
250
  end
239
251
  # Uses start because http.finish cannot be called.
240
- r = http.start {|h| h.request(Net::HTTP::Get.new(parsed_url.request_uri, @headers))}
252
+ r = http.start { |h| h.request(Net::HTTP::Get.new(parsed_url.request_uri, @headers)) }
241
253
  if r.redirect?
242
- get_page(URI.parse(construct_complete_url(parsed_url,r['Location'])), &block)
254
+ get_page(URI.parse(construct_complete_url(parsed_url, r["Location"])), &block)
243
255
  else
256
+ @urls_fetched += 1
244
257
  block.call(r)
245
258
  end
246
259
  rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError => e
@@ -249,89 +262,62 @@ class SpiderInstance
249
262
  end
250
263
  end
251
264
 
252
- def do_callbacks(a_url, resp, prior_url) #:nodoc:
253
- cbs = [@callbacks[:every],
265
+ def do_callbacks(a_url, resp, prior_url) # :nodoc:
266
+ cbs = [ @callbacks[:every],
254
267
  resp.success? ? @callbacks[:success] : @callbacks[:failure],
255
- @callbacks[resp.code.to_i]]
268
+ @callbacks[resp.code.to_i] ]
256
269
 
257
270
  cbs.each do |cb|
258
271
  cb.call(a_url, resp, prior_url) if cb
259
272
  end
260
273
  end
261
274
 
262
- def generate_next_urls(a_url, resp) #:nodoc:
275
+ def generate_next_urls(a_url, resp) # :nodoc:
263
276
  # Only scan for links if the content-type is HTML or the URL ends with .html
264
- content_type = resp['Content-Type'] || resp['content-type'] || ''
265
- url_ends_with_html = a_url.downcase.end_with?('.html')
266
-
267
- unless content_type.downcase.include?('text/html') || url_ends_with_html
277
+ content_type = resp["Content-Type"] || resp["content-type"] || ""
278
+ url_ends_with_html = a_url.downcase.end_with?(".html")
279
+
280
+ unless content_type.downcase.include?("text/html") || url_ends_with_html
268
281
  return []
269
282
  end
270
-
283
+
271
284
  web_page = resp.body
272
- base_url = (web_page.scan(/base\s+href="(.*?)"/i).flatten +
273
- [a_url[0,a_url.rindex('/')]])[0]
274
- base_url = remove_trailing_slash(base_url)
275
-
276
- # Extract anchor tags with href attributes, respecting rel="nofollow"
277
- web_page.scan(/<a\s[^>]*href="([^"]*)"[^>]*>/i).flatten.map do |link|
278
- # Get the full anchor tag to check for rel attribute
279
- anchor_match = web_page.match(/<a\s[^>]*href="#{Regexp.escape(link)}"[^>]*>/i)
280
- next nil unless anchor_match
281
-
282
- anchor_tag = anchor_match[0]
283
-
284
- # Check if this link has rel="nofollow" or similar attributes that should be respected
285
- if anchor_tag.match(/rel\s*=\s*["']([^"']*nofollow[^"']*)["']/i) ||
286
- anchor_tag.match(/rel\s*=\s*["']([^"']*sponsored[^"']*)["']/i) ||
287
- anchor_tag.match(/rel\s*=\s*["']([^"']*ugc[^"']*)["']/i)
288
- next nil # Skip links with nofollow, sponsored, or ugc rel attributes
289
- end
290
-
285
+ base_url = (web_page.scan(/base\s+href="(.*?)"/i).flatten + [ a_url ])[0]
286
+
287
+ # Extract each anchor tag once, then pull href and rel from the same tag,
288
+ # respecting rel="nofollow"/"sponsored"/"ugc".
289
+ web_page.scan(/<a\s([^>]*)>/i).flatten.map do |attrs|
290
+ next nil if attrs =~ NOFOLLOW_REL
291
+
292
+ href = attrs[/href\s*=\s*"([^"]*)"/i, 1]
293
+ next nil if href.nil? || href.empty?
294
+
291
295
  begin
292
- parsed_link = URI.parse(link)
293
- if parsed_link.fragment == '#'
296
+ parsed_link = URI.parse(href)
297
+ if parsed_link.fragment == "#"
294
298
  nil
295
299
  else
296
- construct_complete_url(base_url, link, parsed_link)
300
+ construct_complete_url(base_url, href, parsed_link)
297
301
  end
298
- rescue
302
+ rescue StandardError
299
303
  nil
300
304
  end
301
305
  end.compact
302
306
  end
303
307
 
304
- def construct_complete_url(base_url, additional_url, parsed_additional_url = nil) #:nodoc:
308
+ def construct_complete_url(base_url, additional_url, parsed_additional_url = nil) # :nodoc:
305
309
  parsed_additional_url ||= URI.parse(additional_url)
306
- case parsed_additional_url.scheme
307
- when nil
308
- u = base_url.is_a?(URI) ? base_url : URI.parse(base_url)
309
- # Include port if it's not the default port
310
- port_part = (u.port && ((u.scheme == 'http' && u.port != 80) || (u.scheme == 'https' && u.port != 443))) ? ":#{u.port}" : ""
311
- if additional_url[0].chr == '/'
312
- "#{u.scheme}://#{u.host}#{port_part}#{additional_url}"
313
- elsif u.path.nil? || u.path == ''
314
- "#{u.scheme}://#{u.host}#{port_part}/#{additional_url}"
315
- elsif u.path[0].chr == '/'
316
- "#{u.scheme}://#{u.host}#{port_part}#{u.path}/#{additional_url}"
317
- else
318
- "#{u.scheme}://#{u.host}#{port_part}/#{u.path}/#{additional_url}"
319
- end
320
- else
321
- additional_url
322
- end
323
- end
324
-
325
- def remove_trailing_slash(s) #:nodoc:
326
- s.sub(%r{/*$},'')
310
+ return additional_url unless parsed_additional_url.scheme.nil?
311
+ base = base_url.is_a?(URI) ? base_url : URI.parse(base_url.to_s)
312
+ URI.join(base, additional_url).to_s
327
313
  end
328
314
 
329
- class HeaderSetter #:nodoc:
315
+ class HeaderSetter # :nodoc:
330
316
  def initialize(si)
331
317
  @si = si
332
318
  end
333
- def []=(k,v)
334
- @si.raw_headers = @si.raw_headers.merge({k => v})
319
+ def []=(k, v)
320
+ @si.raw_headers = @si.raw_headers.merge({ k => v })
335
321
  end
336
322
  end
337
323
  end
data/lib/spider.rb CHANGED
@@ -1,11 +1,10 @@
1
- require File.dirname(__FILE__)+'/spider/spider_instance'
1
+ require File.dirname(__FILE__)+"/spider/spider_instance"
2
2
 
3
3
  # A spidering library for Ruby. Handles robots.txt, scraping, finding more
4
4
  # links, and doing it all over again.
5
5
  class Spider
6
-
7
6
  VERSION = File.read(
8
- File.expand_path('../VERSION', __dir__)
7
+ File.expand_path("../VERSION", __dir__)
9
8
  ).strip.freeze
10
9
 
11
10
  def self.version
@@ -22,6 +21,12 @@ class Spider
22
21
  # a_url =~ %r{^http://cashcats.biz.*}
23
22
  # end
24
23
  #
24
+ # # Be polite: wait one second between requests.
25
+ # s.crawl_delay = 1
26
+ #
27
+ # # Stop after fetching 100 pages.
28
+ # s.max_urls = 100
29
+ #
25
30
  # s.on 404 do |a_url, resp, prior_url|
26
31
  # puts "URL not found: #{a_url}"
27
32
  # end
@@ -37,7 +42,7 @@ class Spider
37
42
 
38
43
  def self.start_at(a_url, &block)
39
44
  rules = RobotRules.new("Ruby Spider #{Spider::VERSION}")
40
- a_spider = SpiderInstance.new({nil => [a_url]}, [], rules, [])
45
+ a_spider = SpiderInstance.new({ nil => [ a_url ] }, [], rules, [])
41
46
  block.call(a_spider)
42
47
  a_spider.start!
43
48
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spider
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nagro
@@ -45,7 +45,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
45
45
  - !ruby/object:Gem::Version
46
46
  version: '0'
47
47
  requirements: []
48
- rubygems_version: 3.6.9
48
+ rubygems_version: 4.0.14
49
49
  specification_version: 4
50
50
  summary: A Web spidering library
51
51
  test_files: []