micro_spider 0.1.16

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f9363e70b57c95de9256ea2549cf2ee76c2669c0
4
+ data.tar.gz: 7f8b0bc18fde686058c2b426f84c05b26ba3a1b3
5
+ SHA512:
6
+ metadata.gz: eb6a2ca107f788c95b4244b06de2ac8b7c94e983e0306dbcce71872cee37dd5946784cb900681fdd9d0ee35f6e82c2c6f7abbfed4916fabbdcc97a1ee27c849b
7
+ data.tar.gz: 06ea26fcfd3b53edbb461927772a50d4320525e89f9d7c19e250cc93ba33937362dd60d7d5467dd5d7a0c8950fa67e50c0a7d2df6f5fd5c81a51ca1a9e78c9cb
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2013 zires
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,4 @@
1
+ tiny-spider
2
+ ===========
3
+
4
+ web spider
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ require 'yard'
9
+ YARD::Rake::YardocTask.new
10
+
11
+ require 'rake/testtask'
12
+ Rake::TestTask.new(:test) do |t|
13
+ t.libs << 'lib'
14
+ t.libs << 'test'
15
+ t.pattern = 'test/**/*_test.rb'
16
+ t.verbose = true
17
+ end
18
+
19
+ task :default => :test
@@ -0,0 +1,155 @@
1
+ require 'capybara'
2
+ require 'capybara-webkit'
3
+ require 'capybara/dsl'
4
+
5
+ Capybara.run_server = false
6
+ Capybara.current_driver = :webkit
7
+
8
+ require 'logger'
9
+ require 'spider_core'
10
+
11
+ class MicroSpider
12
+
13
+ include Capybara::DSL
14
+ include SpiderCore::Behavior
15
+ include SpiderCore::FieldDSL
16
+ include SpiderCore::FollowDSL
17
+ include SpiderCore::PaginationDSL
18
+
19
+ attr_reader :excretion, :paths, :delay, :current_location
20
+ attr_accessor :logger, :actions, :recipe, :skip_set_entrance
21
+
22
+ def initialize(excretion = nil)
23
+ @paths = []
24
+ @actions = []
25
+ @excretion = excretion || { status: 'inprogress', results: [] }
26
+ @logger = Logger.new(STDOUT)
27
+ end
28
+
29
+ # The seconds between each two request.
30
+ #
31
+ # @param sec [Float]
32
+ def delay=(sec)
33
+ raise ArgumentError, 'Delay sec can not be a negative number.' if sec.to_f < 0
34
+ @delay = sec.to_f
35
+ end
36
+
37
+ # Visit the path.
38
+ #
39
+ # @param path [String] the path to visit, can be absolute path or relative path.
40
+ # @example Visit a path
41
+ # spider = TinySpider.new
42
+ # spider.visit('/example')
43
+ # spider.visit('http://google.com')
44
+ #
45
+ def visit(path)
46
+ sleep_or_not
47
+ logger.info "Begin to visit #{path}."
48
+ super(path)
49
+ @current_location = {entrance: path}
50
+ logger.info "Current location is #{path}."
51
+ end
52
+
53
+ def click(locator, opts = {})
54
+ actions << lambda {
55
+ path = find_link(locator, opts)[:href]
56
+ visit(path)
57
+ }
58
+ end
59
+
60
+ def learn(recipe = nil, &block)
61
+ if block_given?
62
+ instance_eval(&block)
63
+ @recipe = block
64
+ elsif recipe.is_a?(Proc)
65
+ instance_eval(&recipe)
66
+ @recipe = recipe
67
+ elsif recipe.is_a?(String)
68
+ instance_eval(recipe)
69
+ @recipe = recipe
70
+ else
71
+ self
72
+ end
73
+ end
74
+
75
+ def site(url)
76
+ return if @site
77
+ Capybara.app_host = @excretion[:site] = @site = url
78
+ end
79
+
80
+ def entrance(*path_or_paths)
81
+ return if @skip_set_entrance
82
+ @paths += path_or_paths
83
+ end
84
+
85
+ def entrance_on_path(path, pattern, kind: :css, **opts, &block)
86
+ return if @skip_set_entrance
87
+ visit(path)
88
+ entrances = scan_all(kind, pattern, opts).map do |element|
89
+ block_given? ? yield(element) : element[:href]
90
+ end
91
+ @paths += entrances.to_a
92
+ end
93
+
94
+ def crawl(&block)
95
+ return excretion if completed?
96
+
97
+ @paths.compact!
98
+ path = @paths.shift
99
+ if path.nil?
100
+ excretion[:status] = 'completed'
101
+ return excretion
102
+ end
103
+
104
+ visit(path)
105
+ execute_actions
106
+ yield(@current_location) if block_given?
107
+ excretion[:results] << @current_location
108
+
109
+ @skip_set_entrance = true
110
+ learn(@recipe)
111
+ crawl(&block)
112
+
113
+ excretion
114
+ end
115
+
116
+ def create_action(name, &block)
117
+ action = proc { actions << lambda { block.call(current_location) } }
118
+ metaclass.send :define_method, name, &action
119
+ end
120
+
121
+ def execute_actions
122
+ actions.delete_if { |action| action.call }
123
+ end
124
+
125
+ def spawn
126
+ spider = self.clone
127
+ spider.instance_variable_set(:@paths, [])
128
+ spider.instance_variable_set(:@actions, [])
129
+ spider.instance_variable_set(:@excretion, { status: 'inprogress', results: [] })
130
+ spider.skip_set_entrance = false
131
+ spider
132
+ end
133
+
134
+ def results
135
+ excretion[:results]
136
+ end
137
+
138
+ def completed?
139
+ excretion[:status] == 'completed'
140
+ end
141
+
142
+ def metaclass
143
+ class << self; self; end
144
+ end
145
+
146
+ protected
147
+ def sleep_or_not
148
+ if delay && delay > 0
149
+ logger.info "Nedd sleep #{delay} sec."
150
+ sleep(delay)
151
+ logger.info 'Wakeup'
152
+ end
153
+ end
154
+
155
+ end
@@ -0,0 +1,27 @@
1
+ module SpiderCore
2
+ module Behavior
3
+
4
+ protected
5
+
6
+ def scan_all(kind, pattern, **opts)
7
+ if pattern.is_a?(String)
8
+ elements = all(kind, pattern).lazy
9
+ if opts[:limit] && opts[:limit].to_i > 0
10
+ elements = elements.take(opts[:limit].to_i)
11
+ end
12
+ return elements
13
+ elsif pattern.is_a?(Regexp)
14
+ html.scan(pattern).lazy
15
+ end
16
+ end
17
+
18
+ def scan_first(kind, pattern)
19
+ if pattern.is_a?(String)
20
+ first(kind, pattern)
21
+ elsif pattern.is_a?(Regexp)
22
+ html[pattern, 1]
23
+ end
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,84 @@
1
+ module SpiderCore
2
+ module FieldDSL
3
+
4
+ # Get a field on current page.
5
+ #
6
+ # @param display [String] display name
7
+ def field(display, pattern, opts = {}, &block)
8
+ kind = opts[:kind] || :css
9
+ actions << lambda {
10
+ action_for(:field, {display: display, pattern: pattern, kind: kind}, opts, &block)
11
+ }
12
+ end
13
+
14
+ def css_field(display, pattern, opts = {}, &block)
15
+ field(display, pattern, opts.merge(kind: :css), &block)
16
+ end
17
+
18
+ def xpath_field(display, pattern, opts = {}, &block)
19
+ field(display, pattern, opts.merge(kind: :xpath), &block)
20
+ end
21
+
22
+ def fields(display, pattern, opts = {}, &block)
23
+ kind = opts[:kind] || :css
24
+ actions << lambda {
25
+ action_for(:fields, {display: display, pattern: pattern, kind: kind}, opts, &block)
26
+ }
27
+ end
28
+
29
+ def css_fields(display, pattern, opts = {}, &block)
30
+ fields(display, pattern, opts.merge(kind: :css), &block)
31
+ end
32
+
33
+ def xpath_fields(display, pattern, opts = {}, &block)
34
+ fields(display, pattern, opts.merge(kind: :xpath), &block)
35
+ end
36
+
37
+ protected
38
+ def handle_element(element)
39
+ if element && element.respond_to?(:text)
40
+ element.text
41
+ else
42
+ element
43
+ end
44
+ end
45
+
46
+ def handle_elements(elements, &block)
47
+ if elements.respond_to?(:map) && block_given?
48
+ elements.map { |element| yield(element) }.force
49
+ elsif elements.respond_to?(:map)
50
+ elements.map { |element| handle_element(element) }.force
51
+ elsif block_given?
52
+ yield(elements)
53
+ else
54
+ handle_element(elements)
55
+ end
56
+ end
57
+
58
+ def action_for(action, action_opts = {}, opts = {}, &block)
59
+ begin
60
+ logger.info "Start to get `#{action_opts[:pattern]}` displayed `#{action_opts[:display]}`."
61
+
62
+ elements = case action
63
+ when :field
64
+ scan_first(action_opts[:kind], action_opts[:pattern])
65
+ when :fields
66
+ scan_all(action_opts[:kind], action_opts[:pattern], opts).lazy
67
+ else
68
+ raise 'Unknow action.'
69
+ end
70
+
71
+ make_field_result( action_opts[:display], handle_elements(elements, &block) )
72
+ rescue Exception => err
73
+ logger.fatal("Caught exception when get `#{action_opts[:pattern]}`.")
74
+ logger.fatal(err)
75
+ end
76
+ end
77
+
78
+ def make_field_result(display, field)
79
+ current_location[:field] ||= []
80
+ current_location[:field] << {display => field}
81
+ end
82
+
83
+ end
84
+ end
@@ -0,0 +1,22 @@
1
+ module SpiderCore
2
+ module FollowDSL
3
+
4
+ attr_accessor :skip_followers
5
+
6
+ def follow(pattern, kind: :css, **opts, &block)
7
+ return unless block_given?
8
+ actions << lambda {
9
+ spider = self.spawn
10
+ spider.learn(&block)
11
+ scan_all(kind, pattern, opts).each do |element|
12
+ next if skip_followers && skip_followers.include?(element[:href])
13
+ spider.skip_set_entrance = false
14
+ spider.entrance(element[:href])
15
+ end
16
+ current_location[:follow] ||= []
17
+ current_location[:follow] << spider.crawl[:results]
18
+ }
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ module SpiderCore
2
+ module PaginationDSL
3
+
4
+ attr_accessor :next_page, :skip_pages
5
+
6
+ def keep_eyes_on_next_page(pattern, kind: :css)
7
+ actions << lambda {
8
+ @next_page = first(kind, pattern)[:href] rescue nil
9
+ @paths.unshift(@next_page) if @next_page
10
+ }
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module SpiderCore
2
+ VERSION = "0.1.16"
3
+ end
@@ -0,0 +1,4 @@
1
+ require 'spider_core/behavior'
2
+ require 'spider_core/follow_dsl'
3
+ require 'spider_core/field_dsl'
4
+ require 'spider_core/pagination_dsl'
@@ -0,0 +1,106 @@
1
+ require 'test_helper'
2
+
3
+ class MicroSpiderTest < MiniTest::Unit::TestCase
4
+
5
+ def setup
6
+ @spider = MicroSpider.new
7
+ end
8
+
9
+ def test_spider_can_visit_path_with_some_delays
10
+ @spider.delay = 5
11
+ now = Time.now
12
+ @spider.visit('/')
13
+ @spider.visit('/')
14
+ assert_equal 5, @spider.instance_variable_get(:@delay)
15
+ assert (Time.now - now) > 5
16
+ end
17
+
18
+ def test_spider_can_follow_lots_of_links
19
+ @spider.entrance('/')
20
+ @spider.follow('.links a') do
21
+ field :name, '#name'
22
+ end
23
+ excretion = @spider.crawl
24
+ excretion[:results].first[:follow].first.each do |f|
25
+ case f[:entrance]
26
+ when '/a'
27
+ assert_equal 'This is a', f[:field].first[:name]
28
+ when '/b'
29
+ assert_equal 'This is b', f[:field].first[:name]
30
+ when '/c'
31
+ assert_equal 'This is c', f[:field].first[:name]
32
+ when '/d'
33
+ assert_equal 'This is d', f[:field].first[:name]
34
+ end
35
+ end
36
+ end
37
+
38
+ def test_spider_can_nest_follow_lots_of_links
39
+ @spider.entrance('/')
40
+ @spider.follow('.links a') do
41
+ follow('.links a') do
42
+ field :name, '#name'
43
+ end
44
+ end
45
+ excretion = @spider.crawl
46
+ excretion[:results].first[:follow].first.each do |f|
47
+ refute_empty f[:follow].first
48
+ f[:follow].first.each do |ff|
49
+ case ff[:entrance]
50
+ when '/a'
51
+ assert_equal 'This is a', ff[:field].first[:name]
52
+ when '/b'
53
+ assert_equal 'This is b', ff[:field].first[:name]
54
+ when '/c'
55
+ assert_equal 'This is c', ff[:field].first[:name]
56
+ when '/d'
57
+ assert_equal 'This is d', ff[:field].first[:name]
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ def test_spider_can_keep_eyes_on_next_page
64
+ @spider.entrance('/page/1')
65
+ @spider.learn do
66
+ keep_eyes_on_next_page('.pages a.next_page')
67
+ field(:current_page, '#current_page')
68
+ end
69
+ excretion = @spider.crawl
70
+ excretion[:results].each do |f|
71
+ f[:entrance] =~ /\/page\/(\d)/
72
+ assert_equal "Current Page #{$1}", f[:field].first[:current_page]
73
+ end
74
+ end
75
+
76
+ def test_spider_can_create_custom_action
77
+ @spider.create_action(:save) do |result|
78
+ result[:save] = 'saved'
79
+ end
80
+ @spider.learn do
81
+ entrance '/'
82
+ field :name, '#name'
83
+ save
84
+ end
85
+ excretion = @spider.crawl
86
+ assert_equal 'saved', excretion[:results].first[:save]
87
+ end
88
+
89
+ def test_spider_can_create_custom_action_reached_by_spawn
90
+ @spider.create_action(:save) do |result|
91
+ result[:save] = 'saved'
92
+ end
93
+ @spider.learn do
94
+ entrance '/'
95
+ field :name, '#name'
96
+ save
97
+ follow '.links a' do
98
+ field :name, '#name'
99
+ save
100
+ end
101
+ end
102
+ excretion = @spider.crawl
103
+ assert_equal 'saved', excretion[:results].first[:follow].first[0][:save]
104
+ end
105
+
106
+ end
@@ -0,0 +1,103 @@
1
+ begin
2
+ Bundler.setup(:default, :development)
3
+ rescue Bundler::BundlerError => e
4
+ $stderr.puts e.message
5
+ $stderr.puts "Run `bundle install` to install missing gems"
6
+ exit e.status_code
7
+ end
8
+
9
+ require 'sinatra/base'
10
+ require 'test/unit'
11
+ require 'pry'
12
+
13
+ # Enable turn if it is available
14
+ begin
15
+ require 'turn'
16
+ rescue LoadError
17
+ end
18
+
19
+ require 'micro_spider'
20
+
21
+ class MyApp < Sinatra::Base
22
+
23
+ get '/' do
24
+ erb <<-ERB
25
+ <div id="name">Home</div>
26
+ <div class='links'>
27
+ <a href='/a'>A</a>
28
+ <a href='/b'>B</a>
29
+ <a href='/c'>C</a>
30
+ <a href='/d'>D</a>
31
+ </div>
32
+ ERB
33
+ end
34
+
35
+ get '/a' do
36
+ erb <<-ERB
37
+ <div id='name'>This is a</div>
38
+ <div class='links'>
39
+ <a href='/a'>A</a>
40
+ <a href='/b'>B</a>
41
+ <a href='/c'>C</a>
42
+ <a href='/d'>D</a>
43
+ </div>
44
+ ERB
45
+ end
46
+
47
+ get '/b' do
48
+ erb <<-ERB
49
+ <div id='name'>This is b</div>
50
+ <div class='links'>
51
+ <a href='/a'>A</a>
52
+ <a href='/b'>B</a>
53
+ <a href='/c'>C</a>
54
+ <a href='/d'>D</a>
55
+ </div>
56
+ ERB
57
+ end
58
+
59
+
60
+ get '/c' do
61
+ erb <<-ERB
62
+ <div id='name'>This is c</div>
63
+ <div class='links'>
64
+ <a href='/a'>A</a>
65
+ <a href='/b'>B</a>
66
+ <a href='/c'>C</a>
67
+ <a href='/d'>D</a>
68
+ </div>
69
+ ERB
70
+ end
71
+
72
+
73
+ get '/d' do
74
+ erb <<-ERB
75
+ <div id='name'>This is d</div>
76
+ <div class='links'>
77
+ <a href='/a'>A</a>
78
+ <a href='/b'>B</a>
79
+ <a href='/c'>C</a>
80
+ <a href='/d'>D</a>
81
+ </div>
82
+ ERB
83
+ end
84
+
85
+ get '/page/:page' do
86
+ @current_page = params[:page]
87
+ erb <<-ERB
88
+ <div id='current_page'>Current Page <%= @current_page %></div>
89
+ <div class='pages'>
90
+ <% next_page = @current_page.to_i < 3 ? @current_page.to_i + 1 : nil %>
91
+ <% if next_page %>
92
+ <a href='/page/<%= next_page %>' class='next_page'>next page</a>
93
+ <% end %>
94
+ </div>
95
+ ERB
96
+ end
97
+
98
+
99
+ end
100
+
101
+ Capybara.use_default_driver
102
+ Capybara.app = MyApp
103
+
metadata ADDED
@@ -0,0 +1,156 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: micro_spider
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.16
5
+ platform: ruby
6
+ authors:
7
+ - zires
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-07-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: capybara
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: capybara-webkit
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: yard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: turn
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: sinatra
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: A DSL to write web spider. Depend on capybara and capybara-webkit.
112
+ email:
113
+ - zshuaibin@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - lib/micro_spider.rb
119
+ - lib/spider_core/behavior.rb
120
+ - lib/spider_core/field_dsl.rb
121
+ - lib/spider_core/follow_dsl.rb
122
+ - lib/spider_core/pagination_dsl.rb
123
+ - lib/spider_core/version.rb
124
+ - lib/spider_core.rb
125
+ - MIT-LICENSE
126
+ - Rakefile
127
+ - README.md
128
+ - test/micro_spider_test.rb
129
+ - test/test_helper.rb
130
+ homepage: https://github.com/zires/micro-spider
131
+ licenses: []
132
+ metadata: {}
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - '>='
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ requirements: []
148
+ rubyforge_project:
149
+ rubygems_version: 2.0.0.rc.2
150
+ signing_key:
151
+ specification_version: 4
152
+ summary: A DSL to write web spider.
153
+ test_files:
154
+ - test/micro_spider_test.rb
155
+ - test/test_helper.rb
156
+ has_rdoc: