micro_spider 0.1.16

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 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: