Grell
Grell is a generic crawler for the web written in Ruby. It can be used to gather data, test pages in a given domain, etc.
Installation
Add this line to your application's Gemfile:
gem 'grell'
And then execute:
$ bundle
Or install it yourself as:
$ gem install grell
Grell uses PhantomJS as a browser, you will need to download and install it in your system. Check for instructions in http://phantomjs.org/ Grell has been tested with PhantomJS v2.1.x
Usage
Crawling an entire site
The main entry point of the library is Grell::Crawler#start_crawling. Grell will yield to your code with each page it finds:
require 'grell'
crawler = Grell::Crawler.new
crawler.start_crawling('http://www.google.com') do |page|
#Grell will keep iterating this block which each unique page it finds
puts "yes we crawled #{page.url}"
puts "status: #{page.status}"
puts "headers: #{page.headers}"
puts "body: #{page.body}"
puts "We crawled it at #{page.timestamp}"
puts "We found #{page.links.size} links"
puts "page id and parent_id #{page.id}, #{page.parent_id}"
end
Grell keeps a list of pages previously crawled and do not visit the same page twice. This list is indexed by the complete url, including query parameters.
Re-retrieving a page
If you want Grell to revisit a page and return the data to you again, return the symbol :retry in your block for the start_crawling method. For instance
require 'grell'
crawler = Grell::Crawler.new
crawler.start_crawling('http://www.google.com') do |current_page|
if current_page.status == 500 && current_page.retries == 0
crawler.manager.restart
:retry
end
end
Pages' id
Each page has an unique id, accessed by the property id
. Also each page stores the id of the page from which we found this page, accessed by the property parent_id
.
The page object generated by accessing the first URL passed to the start_crawling(the root) has a parent_id
equal to nil
and an id
equal to 0.
Using this information it is possible to construct a directed graph.
Restart and quit
Grell can be restarted. The current list of visited and yet-to-visit pages list are not modified when restarting
but the browser is destroyed and recreated, all cookies and local storage are lost. After restarting, crawling is resumed with a
new browser.
To destroy the crawler, call the quit
method. This will free the memory taken in Ruby and destroys the PhantomJS process.
require 'grell'
crawler = Grell::Crawler.new
crawler.manager.restart # restarts the browser
crawler.manager.quit # quits and destroys the crawler
Options
The Grell:Crawler
class can be passed options to customize its behavior:
-
logger
: Sets the logger object, for instanceRails.logger
. Default:Logger.new(STDOUT)
-
on_periodic_restart
: Sets periodic restarts of the crawler each certain number of visits. Default: 100 pages. -
allowlist
: Sets a allowlist filter for URLs to be visited. Default: all URLs are allowlisted. -
denylist
: Sets a denylist filter for URLs to be avoided. Default: no URL is denylisted. -
add_match_block
: Block evaluated to consider if a given page should be part of the pages to be visited. Default: add unique URLs. -
evaluate_in_each_page
: Javascript block to be evaluated on each page visited. Default: Nothing evaluated.
Grell by default will follow all the links it finds in the site being crawled. It will never follow links linking outside your site. If you want to further limit the amount of links crawled, you can use allowlisting, denylisting or manual filtering. Below further details on these and other options.
Automatically restarting PhantomJS
If you are doing a long crawling it is possible that phantomJS gets into an inconsistent state or it starts leaking memory.
The crawler can be restarted manually by calling crawler.manager.restart
or automatically by using the
on_periodic_restart
configuration key as follows:
require 'grell'
crawler = Grell::Crawler.new(on_periodic_restart: { do: my_restart_procedure, each: 200 })
crawler.start_crawling('http://www.google.com') do |current_page|
...
endd
This code will setup the crawler to be restarted every 200 pages being crawled and to call my_restart_procedure
between restarts. A restart will destroy the cookies so for instance this custom block can be used to relogin.
Allowlisting
require 'grell'
crawler = Grell::Crawler.new(allowlist: [/games\/.*/, '/fun'])
crawler.start_crawling('http://www.google.com')
Grell here will only follow links to games and '/fun' and ignore all other links. You can provide a regexp, strings (if any part of the string match is allowlisted) or an array with regexps and/or strings.
Denylisting
require 'grell'
crawler = Grell::Crawler.new(denylist: /games\/.*/)
crawler.start_crawling('http://www.google.com')
Similar to allowlisting. But now Grell will follow every other link in this site which does not go to /games/...
If you call both allowlist and denylist then both will apply, a link has to fullfill both conditions to survive. If you do not call any, then all links on this site will be crawled. Think of these methods as filters.
Manual link filtering
If you have a more complex use-case, you can modify the list of links manually. Grell yields the page to you before it adds the links to the list of links to visit. So you can modify in your block of code "page.links" to add and delete links to instruct Grell to add them to the list of links to visit next.
Custom URL Comparison
By default, Grell will detect new URLs to visit by comparing the full URL
with the URLs of the discovered and visited links. This functionality can
be changed by passing a block of code to Grells start_crawling
method.
In the below example, the path of the URLs (instead of the full URL) will
be compared.
require 'grell'
add_match_block = Proc.new do |collection_page, page|
collection_page.path == page.path
end
crawler = Grell::Crawler.new(add_match_block: add_match_block)
crawler.start_crawling('http://www.google.com') do |current_page|
...
end
Evaluate script
You can evalute a JavaScript snippet in each page before extracting links by passing the snippet to the 'evaluate_in_each_page' option:
require 'grell'
crawler = Grell::Crawler.new(evaluate_in_each_page: "typeof jQuery !== 'undefined' && $('.dropdown').addClass('open');")
Errors
When there is an error in the page or an internal error in the crawler (Javascript crashed the browser, etc). Grell will return with status 404 and the headers will have the following keys:
- grellStatus: 'Error'
- errorClass: The class of the error which broke this page.
- errorMessage: A descriptive message with the information Grell could gather about the error.
Tests
Run the tests with
bundle exec rake ci
Contributors
Grell is (c) Medidata Solutions Worldwide and owned by its major contributors: