Fast high-level web crawling Ruby framework
Vessel is a Ruby high-level web crawling framework. It can be used in a wide range of scenarios, like data mining, monitoring or historical archival. Pages are fetched by a pluggable driver: Ferrum (a real Chrome, the default) or Mechanize (plain HTTP, no browser). For automated testing we recommend Cuprite.
Add this to your Gemfile:
gem "vessel"The default :ferrum driver needs Chrome or Chromium installed and available in PATH.
The :mechanize driver is optional and ships as a sibling dependency, so add it
to your Gemfile when you want to use it:
gem "mechanize"
gem "vessel"In order to show you how Vessel works we are going to crawl together famous quotes website:
…ruby QuotesToScrapeCom.run(delay: 2, max_threads: 1)
### domain
```ruby
class MyScraper handler`, so different
entry points can be parsed by different methods. Without a handler `:parse` is
used:
```ruby
class MyScraper :parse_products,
"https://example.com/news" => :parse_news
endIf start_urls is empty the crawler is still started once with a stub response,
which is handy when the first urls are computed in parse itself.
class MyScraper "text/plain",
"Referer" => "https://example.com"
endcookie sets one cookie, cookies takes an array of them. domain defaults to
the crawler's domain:
class MyScraper "humor"
end| Option | Description |
|---|---|
url |
Absolute or relative url, resolved against the current page |
handler |
Method to call with the response, :parse by default, callback is an alias |
data |
Arbitrary hash carried over to the response, deep-copied |
delay |
Overrides the crawler delay for this request |
headers |
Overrides the crawler headers for this request |
cookies |
Overrides the crawler cookies for this request |
once |
true by default, the same url is not visited twice, pass false to allow it |
encode |
true by default, the url is decoded and encoded again before it's joined |
You can yield a single request, an array of them, or a hash of the extracted
fields, but not both in one yield.
The response is delegated to the crawler, so inside a handler you can call directly:
at_css, css, at_xpath, xpath - search the pageurl, data, attempt, size, body, raw - the current url, the data
attached to the request, which attempt it is, the page size in bytes, the
Nokogiri document and the raw htmlabsolute_url, join_url, url_encode, url_decode (aliased as
uri_encode, uri_decode) - url helpersresponse and page are available as well, page being the driver's native
object, Ferrum::Page or Mechanize::Page. response.status and
response.headers give you the response status and headers,
response.sync_body re-reads the html after a pause when the page needs a
moment to render.
By default the selectors run against the live page, which for the Ferrum driver
means Chrome does the querying. Setting Vessel.page_snapshot = true makes them
run against the Nokogiri document of the page's html instead, which is much
faster when you extract a lot of nodes and don't need to interact with the page.
Keep in mind that the nodes you get back come from the driver, Ferrum::Node or
Nokogiri::XML::Node, and the two don't have the same api. Read the attributes
with node[:href], that one works everywhere, node.attribute(:href) only works
on the live Chrome page.
Instead of building a hash by hand a handler can declare fields and yield the
collected fields object:
class MyScraper :writer }` stores the field under another name
Fields can be normalized in one place by their name, put such declarations into
`config/fields` of a generated project:
```ruby
Vessel::Cargo::FieldType.add(:price) { |value| value.to_s.gsub(/[^\d.]/, "").to_f }Every field :price is then passed through that block, typing: false skips it.
Everything a handler yields that is not a request goes through the middleware
pipeline, which runs in its own thread pool. A middleware is a class with a
call(hash, fields) method, it gets the hash returned by the previous
middleware plus the original fields object, and returns the hash for the next
one:
class Sanitize < Vessel::Middleware
def call(hash, fields)
hash.transform_values { |v| v.is_a?(String) ? v.strip : v }
end
end
class Save < Vessel::Middleware
def call(hash, fields)
raise Vessel::Middleware::InvalidItemError if hash[:text].to_s.empty?
DB[:quotes].insert(hash)
hash
end
end
class MyScraper < Vessel::Cargo
middleware "Sanitize", "Save"
endMiddleware is declared by name so that classes can be defined anywhere in the
project, the crawler settings are available as settings. Raising
Vessel::Middleware::InvalidItemError silently rejects the item, any other
error is logged and counted as a rejection too.
A block passed to .run replaces the whole pipeline, which is the shortest way
to collect the items in a script:
QuotesToScrapeCom.run { |item| quotes << item }Override any of these in the crawler to hook into the run:
class MyScraper < Vessel::Cargo
def before_start; end # before anything is scheduled
def before(stats); end # with the initial stats
def after_change(counter, stats) # on every stats counter change
end
def info(stats); end # every 4 seconds, logs the stats by default
def after(stats); end # when the engine is done
def before_stop; end # at exit
def on_error(request, error); end # a request failed, re-raises by default
endThe stats hash holds req_enqueued, res_dequeued, res_handled,
item_pipelined, item_processed, item_sent, item_rejected and idling.
Vessel logs to $stdout at the debug level. Pass your own logger to change
that:
Vessel::Logger.instance = Logger.new("log/vessel.log", level: Logger::INFO)A single-file script is enough for a small crawler, for a bigger one Vessel can generate a project:
$ vessel new myproject
$ cd myproject
$ bundle install
$ vessel generate example.commyproject
├── Gemfile
├── config
│ ├── boot.rb # loads everything below
│ ├── environments
│ │ ├── dev/dev.rb # ApplicationCrawler for VESSEL_ENV=dev
│ │ └── prod/prod.rb # ApplicationCrawler for VESSEL_ENV=prod
│ ├── fields # FieldType declarations
│ └── middleware # middleware classes
├── crawlers # one crawler per site
├── lib
│ ├── helpers
│ └── loader.rb
└── logCrawlers inherit from ApplicationCrawler, which is defined per environment, so
the settings can differ between development and production, VESSEL_ENV
selects the environment and defaults to dev.
| Command | Description |
|---|---|
vessel new NAME |
Generate a new project |
vessel generate DOMAIN |
Generate crawlers/DOMAIN.rb |
vessel list |
List the crawlers of the project |
vessel settings DOMAIN |
Show the settings of a crawler |
vessel start DOMAIN |
Run a crawler |
vessel parse DOMAIN URL HANDLER --data=key:value |
Fetch one url and call one handler, for debugging |
vessel version |
Print the version |
The gem is available as open source under the terms of the MIT License.
No open issues yet, or sync has not completed.