vessel 0.2.0 → 0.3.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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -0
  3. data/README.md +418 -34
  4. data/bin/console +16 -0
  5. data/bin/vessel +8 -0
  6. data/lib/vessel/cargo/callbacks.rb +27 -0
  7. data/lib/vessel/cargo/field.rb +41 -0
  8. data/lib/vessel/cargo/field_type.rb +17 -0
  9. data/lib/vessel/cargo/fieldable.rb +27 -0
  10. data/lib/vessel/cargo/fields.rb +54 -0
  11. data/lib/vessel/cargo/settings.rb +113 -0
  12. data/lib/vessel/cargo.rb +38 -78
  13. data/lib/vessel/cli.rb +61 -2
  14. data/lib/vessel/driver/ferrum/driver.rb +43 -0
  15. data/lib/vessel/driver/ferrum/page.rb +64 -0
  16. data/lib/vessel/driver/mechanize/driver.rb +35 -0
  17. data/lib/vessel/driver/mechanize/page.rb +79 -0
  18. data/lib/vessel/driver/page.rb +59 -0
  19. data/lib/vessel/driver/registry.rb +26 -0
  20. data/lib/vessel/driver.rb +133 -0
  21. data/lib/vessel/engine/stats.rb +51 -0
  22. data/lib/vessel/engine.rb +106 -27
  23. data/lib/vessel/loader.rb +36 -0
  24. data/lib/vessel/logger.rb +25 -0
  25. data/lib/vessel/middleware.rb +7 -9
  26. data/lib/vessel/middleware_scheduler.rb +51 -0
  27. data/lib/vessel/proxy.rb +41 -0
  28. data/lib/vessel/request.rb +28 -11
  29. data/lib/vessel/response.rb +106 -0
  30. data/lib/vessel/scheduler.rb +19 -34
  31. data/lib/vessel/skeleton/Gemfile +5 -0
  32. data/lib/vessel/skeleton/config/boot.rb +17 -0
  33. data/lib/vessel/skeleton/config/environments/dev/dev.rb +5 -0
  34. data/lib/vessel/skeleton/config/environments/prod/prod.rb +7 -0
  35. data/lib/vessel/skeleton/config/fields/.keep +0 -0
  36. data/lib/vessel/skeleton/config/middleware/debug.rb +15 -0
  37. data/lib/vessel/skeleton/crawlers/.keep +0 -0
  38. data/lib/vessel/skeleton/lib/helpers/.keep +0 -0
  39. data/lib/vessel/skeleton/lib/loader.rb +4 -0
  40. data/lib/vessel/skeleton/log/.keep +0 -0
  41. data/lib/vessel/templates/template.erb +9 -0
  42. data/lib/vessel/templator.rb +32 -0
  43. data/lib/vessel/util.rb +16 -0
  44. data/lib/vessel/version.rb +1 -1
  45. data/lib/vessel.rb +35 -5
  46. metadata +96 -29
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require "nokogiri"
5
+ require "addressable/uri"
6
+
7
+ module Vessel
8
+ class Response
9
+ extend Forwardable
10
+
11
+ attr_accessor :rejected
12
+ attr_reader :url, :data, :handler, :page, :attempt, :cookies
13
+
14
+ delegate %i[xpath css at_xpath at_css] => :page_or_body
15
+
16
+ def initialize(handler:, url: nil, data: nil, page: nil, attempt: 1)
17
+ @url = Addressable::URI.parse(url) if url
18
+ @data = data
19
+ @handler = handler
20
+ @attempt = attempt
21
+ @driven_page = page
22
+ @page = @driven_page&.page
23
+ @rejected = false
24
+ @cookies = @driven_page&.cookies&.map do |c|
25
+ {
26
+ name: c.name, value: c.value, domain: c.domain, path: c.path,
27
+ httponly: c.httponly?, secure: c.secure?, expires: c.expires
28
+ }
29
+ end
30
+ end
31
+
32
+ alias callback handler
33
+
34
+ def absolute_url(relative, encode: true)
35
+ if encode
36
+ relative = url_decode(relative)
37
+ relative = url_encode(relative)
38
+ end
39
+ join_url(url.to_s, relative)
40
+ end
41
+
42
+ def join_url(head, tail)
43
+ Addressable::URI.join(head, tail).to_s
44
+ end
45
+
46
+ def url_encode(uri)
47
+ Addressable::URI.encode(uri)
48
+ end
49
+ alias uri_encode url_encode
50
+
51
+ def url_decode(uri)
52
+ Addressable::URI.unencode(uri)
53
+ end
54
+ alias uri_decode url_decode
55
+
56
+ def body
57
+ @body ||= Nokogiri::HTML(raw)
58
+ end
59
+
60
+ def raw
61
+ @raw ||= @driven_page&.body
62
+ end
63
+
64
+ def sync_body(seconds = 2)
65
+ sleep(seconds)
66
+ @raw = @body = nil
67
+ body
68
+ end
69
+
70
+ def close
71
+ @driven_page&.close
72
+ @driven_page = @page = nil
73
+ end
74
+
75
+ def stub?
76
+ !url
77
+ end
78
+
79
+ def status
80
+ @driven_page&.status
81
+ end
82
+
83
+ def headers
84
+ @driven_page&.headers
85
+ end
86
+
87
+ def size
88
+ @driven_page&.size
89
+ end
90
+
91
+ def ==(other)
92
+ url == other.url &&
93
+ data == other.data &&
94
+ handler == other.handler &&
95
+ page == other.page &&
96
+ attempt == other.attempt &&
97
+ cookies == other.cookies
98
+ end
99
+
100
+ private
101
+
102
+ def page_or_body
103
+ Vessel.page_snapshot ? body : page
104
+ end
105
+ end
106
+ end
@@ -6,37 +6,37 @@ require "concurrent-ruby"
6
6
  module Vessel
7
7
  class Scheduler
8
8
  extend Forwardable
9
+
9
10
  delegate %i[scheduled_task_count completed_task_count queue_length] => :pool
10
11
 
11
- attr_reader :browser, :queue, :delay, :headers
12
+ attr_reader :driver, :queue, :settings
12
13
 
13
14
  def initialize(queue, settings)
14
15
  @queue = queue
15
- @min_threads, @max_threads, @delay, @headers =
16
- settings.values_at(:min_threads, :max_threads, :delay, :headers)
17
-
18
- options = settings[:ferrum]
19
- options.merge!(timeout: settings[:timeout]) if settings[:timeout]
20
- @browser = Ferrum::Browser.new(**options)
21
-
22
- if settings[:intercept]
23
- @browser.network.intercept
24
- @browser.on(:request, &settings[:intercept])
25
- end
16
+ @settings = settings
17
+ @driver = Driver::Registry.instance.build(settings)
26
18
  end
27
19
 
28
20
  def post(*requests)
29
- requests.map do |request|
30
- Concurrent::Promises.future_on(pool, queue, request) do |queue, request|
31
- queue << goto(request)
32
- end
21
+ requests.flatten.map do |request|
22
+ Concurrent::Future.execute(executor: pool,
23
+ args: [request]) { |r| queue << driver.go_to(r) }
33
24
  end
34
25
  end
35
26
 
36
27
  def stop
37
28
  pool.shutdown
38
29
  pool.kill unless pool.wait_for_termination(30)
39
- browser.quit
30
+ driver.stop
31
+ end
32
+
33
+ def force_kill
34
+ pool.send(:ns_kill_execution)
35
+ driver.stop
36
+ end
37
+
38
+ def idle?
39
+ queue_length.zero? && scheduled_task_count == completed_task_count
40
40
  end
41
41
 
42
42
  private
@@ -44,24 +44,9 @@ module Vessel
44
44
  def pool
45
45
  @pool ||= Concurrent::ThreadPoolExecutor.new(
46
46
  max_queue: 0,
47
- min_threads: @min_threads,
48
- max_threads: @max_threads
47
+ min_threads: settings[:min_threads],
48
+ max_threads: settings[:max_threads]
49
49
  )
50
50
  end
51
-
52
- def goto(request)
53
- return [nil, request] if request.stub?
54
-
55
- page = browser.create_page
56
- page.headers.set(headers) if headers
57
- # Delay is set between requests when we don't want to bombard server with
58
- # requests so it requires crawler to be single threaded. Otherwise doesn't
59
- # make sense.
60
- sleep(delay) if @max_threads == 1 && delay > 0
61
- page.goto(request.url)
62
- [page, request]
63
- rescue => e
64
- e
65
- end
66
51
  end
67
52
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "vessel"
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(Dir.pwd) unless $LOAD_PATH.include?(Dir.pwd)
4
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
5
+
6
+ require "bundler/setup"
7
+ Bundler.require(:default)
8
+ Bundler.require(Vessel.env.to_sym)
9
+
10
+ require "lib/loader"
11
+ Vessel.loader = ApplicationLoader.new
12
+
13
+ Dir["config/environments/*.rb"].each { |f| require f }
14
+ Dir["config/environments/#{Vessel.env}/*.rb"].each { |f| require f }
15
+ Dir["config/middleware/*.rb"].each { |f| require f }
16
+ Dir["lib/helpers/*.rb"].each { |f| require f }
17
+ Dir["config/fields/*.rb"].each { |f| require f }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationCrawler < Vessel::Cargo
4
+ middleware "Debug"
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationCrawler < Vessel::Cargo
4
+ delay 4..6
5
+ threads max: 1
6
+ middleware "Debug"
7
+ end
File without changes
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Debug < Vessel::Middleware
4
+ def call(hash, _fields)
5
+ puts deep_sort(hash)
6
+ hash
7
+ end
8
+
9
+ private
10
+
11
+ def deep_sort(hash)
12
+ sorted = hash.sort.to_h
13
+ sorted.transform_values { |v| v.is_a?(Hash) ? deep_sort(v) : v }
14
+ end
15
+ end
File without changes
File without changes
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationLoader < Vessel::Loader
4
+ end
File without changes
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= @class_name %> < ApplicationCrawler
4
+ domain "<%= @domain %>"
5
+ start_urls "https://<%= @domain %>"
6
+
7
+ def parse
8
+ end
9
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vessel
4
+ class Templator
5
+ TEMPLATES = "templates/*.erb"
6
+
7
+ def initialize(domain, options)
8
+ @domain = domain
9
+ @options = options
10
+ @class_name = @domain.gsub("-", ".dash.").split(".").map(&:capitalize).join
11
+ end
12
+
13
+ def build
14
+ abort "Template #{template_path} not found" unless File.file?(template_path)
15
+
16
+ file = File.read(template_path)
17
+ evaluated = ERB.new(file).result(binding)
18
+ Logger.debug("Templator: creating file #{store_path}")
19
+ File.write(store_path, evaluated)
20
+ end
21
+
22
+ private
23
+
24
+ def template_path
25
+ File.expand_path("templates/template.erb", __dir__)
26
+ end
27
+
28
+ def store_path
29
+ "crawlers/#{@domain}.rb"
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vessel
4
+ module Util
5
+ def deep_clone(value)
6
+ case value
7
+ when Hash
8
+ value.inject({}) { |b, (k, v)| b.merge(k => deep_clone(v)) }
9
+ when Array
10
+ value.inject([]) { |b, v| b << deep_clone(v) }
11
+ else
12
+ value
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vessel
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/vessel.rb CHANGED
@@ -1,14 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ module Vessel
4
+ class Error < StandardError; end
5
+
6
+ class NotImplementedError < Error; end
7
+
8
+ class << self
9
+ attr_writer :loader, :page_snapshot
10
+
11
+ def boot
12
+ require File.expand_path("config/boot")
13
+ end
14
+
15
+ def env
16
+ ENV.fetch("VESSEL_ENV", "dev")
17
+ end
18
+
19
+ def page_snapshot
20
+ !!@page_snapshot
21
+ end
22
+
23
+ def loader
24
+ @loader ||= Loader.new
25
+ end
26
+ end
27
+ end
28
+
3
29
  require "concurrent-ruby"
30
+ require "vessel/cli"
4
31
  require "vessel/engine"
5
32
  require "vessel/middleware"
33
+ require "vessel/middleware_scheduler"
6
34
  require "vessel/scheduler"
7
35
  require "vessel/request"
36
+ require "vessel/response"
8
37
  require "vessel/version"
9
38
  require "vessel/cargo"
10
-
11
- module Vessel
12
- class Error < StandardError; end
13
- class NotImplementedError < Error; end
14
- end
39
+ require "vessel/driver"
40
+ require "vessel/driver/ferrum/driver"
41
+ require "vessel/driver/mechanize/driver"
42
+ require "vessel/templator"
43
+ require "vessel/loader"
44
+ require "vessel/logger"
metadata CHANGED
@@ -1,108 +1,176 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vessel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Vorotilin
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2021-03-09 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: addressable
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.5'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.5'
26
+ - !ruby/object:Gem::Dependency
27
+ name: concurrent-ruby
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.1'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.1'
13
40
  - !ruby/object:Gem::Dependency
14
41
  name: ferrum
15
42
  requirement: !ruby/object:Gem::Requirement
16
43
  requirements:
17
- - - ">="
44
+ - - "~>"
18
45
  - !ruby/object:Gem::Version
19
- version: '0.8'
46
+ version: '0.18'
20
47
  type: :runtime
21
48
  prerelease: false
22
49
  version_requirements: !ruby/object:Gem::Requirement
23
50
  requirements:
24
- - - ">="
51
+ - - "~>"
25
52
  - !ruby/object:Gem::Version
26
- version: '0.8'
53
+ version: '0.18'
27
54
  - !ruby/object:Gem::Dependency
28
- name: thor
55
+ name: logger
29
56
  requirement: !ruby/object:Gem::Requirement
30
57
  requirements:
31
58
  - - "~>"
32
59
  - !ruby/object:Gem::Version
33
- version: '1.0'
60
+ version: '1.5'
34
61
  type: :runtime
35
62
  prerelease: false
36
63
  version_requirements: !ruby/object:Gem::Requirement
37
64
  requirements:
38
65
  - - "~>"
39
66
  - !ruby/object:Gem::Version
40
- version: '1.0'
67
+ version: '1.5'
41
68
  - !ruby/object:Gem::Dependency
42
- name: bundler
69
+ name: mechanize
43
70
  requirement: !ruby/object:Gem::Requirement
44
71
  requirements:
45
72
  - - "~>"
46
73
  - !ruby/object:Gem::Version
47
- version: '2.0'
48
- type: :development
74
+ version: '2.14'
75
+ type: :runtime
49
76
  prerelease: false
50
77
  version_requirements: !ruby/object:Gem::Requirement
51
78
  requirements:
52
79
  - - "~>"
53
80
  - !ruby/object:Gem::Version
54
- version: '2.0'
81
+ version: '2.14'
55
82
  - !ruby/object:Gem::Dependency
56
- name: rake
83
+ name: nokogiri
57
84
  requirement: !ruby/object:Gem::Requirement
58
85
  requirements:
59
86
  - - "~>"
60
87
  - !ruby/object:Gem::Version
61
- version: '12.3'
62
- type: :development
88
+ version: '1.18'
89
+ type: :runtime
63
90
  prerelease: false
64
91
  version_requirements: !ruby/object:Gem::Requirement
65
92
  requirements:
66
93
  - - "~>"
67
94
  - !ruby/object:Gem::Version
68
- version: '12.3'
95
+ version: '1.18'
69
96
  - !ruby/object:Gem::Dependency
70
- name: rspec
97
+ name: thor
71
98
  requirement: !ruby/object:Gem::Requirement
72
99
  requirements:
73
100
  - - "~>"
74
101
  - !ruby/object:Gem::Version
75
- version: '3.8'
76
- type: :development
102
+ version: '1.5'
103
+ type: :runtime
77
104
  prerelease: false
78
105
  version_requirements: !ruby/object:Gem::Requirement
79
106
  requirements:
80
107
  - - "~>"
81
108
  - !ruby/object:Gem::Version
82
- version: '3.8'
109
+ version: '1.5'
83
110
  description: Vessel is a high-level web crawling framework, used to crawl websites
84
111
  and extract structured data from their pages
85
112
  email:
86
113
  - d.vorotilin@gmail.com
87
- executables: []
114
+ executables:
115
+ - vessel
88
116
  extensions: []
89
117
  extra_rdoc_files: []
90
118
  files:
119
+ - CHANGELOG.md
91
120
  - LICENSE
92
121
  - README.md
122
+ - bin/console
123
+ - bin/vessel
93
124
  - lib/vessel.rb
94
125
  - lib/vessel/cargo.rb
126
+ - lib/vessel/cargo/callbacks.rb
127
+ - lib/vessel/cargo/field.rb
128
+ - lib/vessel/cargo/field_type.rb
129
+ - lib/vessel/cargo/fieldable.rb
130
+ - lib/vessel/cargo/fields.rb
131
+ - lib/vessel/cargo/settings.rb
95
132
  - lib/vessel/cli.rb
133
+ - lib/vessel/driver.rb
134
+ - lib/vessel/driver/ferrum/driver.rb
135
+ - lib/vessel/driver/ferrum/page.rb
136
+ - lib/vessel/driver/mechanize/driver.rb
137
+ - lib/vessel/driver/mechanize/page.rb
138
+ - lib/vessel/driver/page.rb
139
+ - lib/vessel/driver/registry.rb
96
140
  - lib/vessel/engine.rb
141
+ - lib/vessel/engine/stats.rb
142
+ - lib/vessel/loader.rb
143
+ - lib/vessel/logger.rb
97
144
  - lib/vessel/middleware.rb
145
+ - lib/vessel/middleware_scheduler.rb
146
+ - lib/vessel/proxy.rb
98
147
  - lib/vessel/request.rb
148
+ - lib/vessel/response.rb
99
149
  - lib/vessel/scheduler.rb
150
+ - lib/vessel/skeleton/Gemfile
151
+ - lib/vessel/skeleton/config/boot.rb
152
+ - lib/vessel/skeleton/config/environments/dev/dev.rb
153
+ - lib/vessel/skeleton/config/environments/prod/prod.rb
154
+ - lib/vessel/skeleton/config/fields/.keep
155
+ - lib/vessel/skeleton/config/middleware/debug.rb
156
+ - lib/vessel/skeleton/crawlers/.keep
157
+ - lib/vessel/skeleton/lib/helpers/.keep
158
+ - lib/vessel/skeleton/lib/loader.rb
159
+ - lib/vessel/skeleton/log/.keep
160
+ - lib/vessel/templates/template.erb
161
+ - lib/vessel/templator.rb
162
+ - lib/vessel/util.rb
100
163
  - lib/vessel/version.rb
101
- homepage: https://github.com/route/vessel
164
+ homepage: https://github.com/rubycdp/vessel
102
165
  licenses:
103
166
  - MIT
104
- metadata: {}
105
- post_install_message:
167
+ metadata:
168
+ homepage_uri: https://vessel.rubycdp.com/
169
+ bug_tracker_uri: https://github.com/rubycdp/vessel/issues
170
+ changelog_uri: https://github.com/rubycdp/vessel/blob/main/CHANGELOG.md
171
+ documentation_uri: https://github.com/rubycdp/vessel/blob/main/README.md
172
+ source_code_uri: https://github.com/rubycdp/vessel
173
+ rubygems_mfa_required: 'true'
106
174
  rdoc_options: []
107
175
  require_paths:
108
176
  - lib
@@ -110,15 +178,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
110
178
  requirements:
111
179
  - - ">="
112
180
  - !ruby/object:Gem::Version
113
- version: 2.3.0
181
+ version: '3.1'
114
182
  required_rubygems_version: !ruby/object:Gem::Requirement
115
183
  requirements:
116
184
  - - ">="
117
185
  - !ruby/object:Gem::Version
118
186
  version: '0'
119
187
  requirements: []
120
- rubygems_version: 3.1.4
121
- signing_key:
188
+ rubygems_version: 4.0.6
122
189
  specification_version: 4
123
190
  summary: High-level web crawling framework
124
191
  test_files: []