require 'spidr/rules' require 'spidr/page' require 'spidr/spidr' require 'net/http' module Spidr class Agent # URL schemes to visit SCHEMES = ['http', 'https'] # Proxy to use attr_accessor :proxy # User-Agent to use attr_accessor :user_agent # Referer to use attr_accessor :referer # Delay in between fetching pages attr_accessor :delay # History containing visited URLs attr_reader :history # List of unreachable URLs attr_reader :failures # Queue of URLs to visit attr_reader :queue # # Creates a new Agent object with the given _options_ and _block_. # If a _block_ is given, it will be passed the newly created # Agent object. # # _options_ may contain the following keys: # :proxy:: The proxy to use while spidering. # :user_agent:: The User-Agent string to send. # :referer:: The referer URL to send. # :delay:: Duration in seconds to pause between spidering each # link. Defaults to 0. # :host:: The host-name to visit. # :hosts:: An +Array+ of host patterns to visit. # :ignore_hosts:: An +Array+ of host patterns to not visit. # :ports:: An +Array+ of port patterns to visit. # :ignore_ports:: An +Array+ of port patterns to not visit. # :links:: An +Array+ of link patterns to visit. # :ignore_links:: An +Array+ of link patterns to not visit. # :exts:: An +Array+ of File extension patterns to visit. # :ignore_exts:: An +Array+ of File extension patterns to not # visit. # def initialize(options={},&block) @proxy = (options[:proxy] || Spidr.proxy) @user_agent = (options[:user_agent] || Spidr.user_agent) @referer = options[:referer] @host_rules = Rules.new( :accept => options[:hosts], :reject => options[:ignore_hosts] ) @port_rules = Rules.new( :accept => options[:ports], :reject => options[:ignore_ports] ) @link_rules = Rules.new( :accept => options[:links], :reject => options[:ignore_links] ) @ext_rules = Rules.new( :accept => options[:exts], :reject => options[:ignore_exts] ) @every_url_blocks = [] @every_failed_url_blocks = [] @urls_like_blocks = Hash.new { |hash,key| hash[key] = [] } @every_page_blocks = [] @delay = (options[:delay] || 0) @history = [] @failures = [] @queue = [] @paused = true if options[:host] visit_hosts_like(options[:host]) end block.call(self) if block end # # Creates a new Agent object with the given _options_ and will begin # spidering at the specified _url_. If a _block_ is given it will be # passed the newly created Agent object, before the agent begins # spidering. # def self.start_at(url,options={},&block) self.new(options) do |spider| block.call(spider) if block spider.start_at(url) end end # # Creates a new Agent object with the given _options_ and will begin # spidering the specified host _name_. If a _block_ is given it will be # passed the newly created Agent object, before the agent begins # spidering. # def self.host(name,options={},&block) self.new(options.merge(:host => name)) do |spider| block.call(spider) if block spider.start_at("http://#{name}/") end end # # Creates a new Agent object with the given _options_ and will begin # spidering the host of the specified _url_. If a _block_ is given it # will be passed the newly created Agent object, before the agent # begins spidering. # def self.site(url,options={},&block) url = URI(url.to_s) return self.new(options.merge(:host => url.host)) do |spider| block.call(spider) if block spider.start_at(url) end end # # Returns the +Array+ of host patterns to visit. # def visit_hosts @host_rules.accept end # # Adds the given _pattern_ to the visit_hosts. If a _block_ is given, # it will be added to the visit_hosts. # def visit_hosts_like(pattern=nil,&block) if pattern visit_hosts << pattern elsif block visit_hosts << block end return self end # # Returns the +Array+ of URL host patterns to not visit. # def ignore_hosts @host_rules.reject end # # Adds the given _pattern_ to the ignore_hosts. If a _block_ is given, # it will be added to the ignore_hosts. # def ignore_hosts_like(pattern=nil,&block) if pattern ignore_hosts << pattern elsif block ignore_hosts << block end return self end # # Returns the +Array+ of URL port patterns to visit. # def visit_ports @port_rules.accept end # # Adds the given _pattern_ to the visit_ports. If a _block_ is given, # it will be added to the visit_ports. # def visit_ports_like(pattern=nil,&block) if pattern visit_ports << pattern elsif block visit_ports << block end return self end # # Returns the +Array+ of URL port patterns to not visit. # def ignore_ports @port_rules.reject end # # Adds the given _pattern_ to the ignore_hosts. If a _block_ is given, # it will be added to the ignore_hosts. # def ignore_ports_like(pattern=nil,&block) if pattern ignore_ports << pattern elsif block ignore_ports << block end return self end # # Returns the +Array+ of link patterns to visit. # def visit_links @link_rules.accept end # # Adds the given _pattern_ to the visit_links. If a _block_ is given, # it will be added to the visit_links. # def visit_links_like(pattern=nil,&block) if pattern visit_links << pattern elsif block visit_links << block end return self end # # Returns the +Array+ of link patterns to not visit. # def ignore_links @link_rules.reject end # # Adds the given _pattern_ to the ignore_links. If a _block_ is given, # it will be added to the ignore_links. # def ignore_links_like(pattern=nil,&block) if pattern ignore_links << pattern elsif block ignore_links << block end return self end # # Returns the +Array+ of URL extension patterns to visit. # def visit_exts @ext_rules.accept end # # Adds the given _pattern_ to the visit_exts. If a _block_ is given, # it will be added to the visit_exts. # def visit_exts_like(pattern=nil,&block) if pattern visit_exts << pattern elsif block visit_exts << block end return self end # # Returns the +Array+ of URL extension patterns to not visit. # def ignore_exts @ext_rules.reject end # # Adds the given _pattern_ to the ignore_exts. If a _block_ is given, # it will be added to the ignore_exts. # def ignore_exts_like(pattern=nil,&block) if pattern ignore_exts << pattern elsif block ignore_exts << block end return self end # # For every URL that the agent visits it will be passed to the # specified _block_. # def every_url(&block) @every_url_blocks << block return self end # # For every URL that the agent is unable to visit, it will be passed # to the specified _block_. # def every_failed_url(&block) @every_failed_url_blocks << block return self end # # For every URL that the agent visits and matches the specified # _pattern_, it will be passed to the specified _block_. # def urls_like(pattern,&block) @urls_like_blocks[pattern] << block return self end # # For every Page that the agent visits, pass the page to the # specified _block_. # def every_page(&block) @every_page_blocks << block return self end # # For every Page that the agent visits, pass the headers to the given # _block_. # def all_headers(&block) every_page { |page| block.call(page.headers) } end # # Clears the history of the agent. # def clear @queue.clear @history.clear @failures.clear return self end # # Clear the history and start spidering at the specified _url_. # def start_at(url) clear enqueue(url) return continue! end # # Start spidering until the queue becomes empty or the agent is # paused. # def run until (@queue.empty? || @paused == true) visit_page(dequeue) end return self end # # Continue spidering. # def continue! @paused = false return run end # # Returns +true+ if the agent is still spidering, returns +false+ # otherwise. # def running? @paused == false end # # Returns +true+ if the agent is paused, returns +false+ otherwise. # def paused? @paused == true end # # Pauses the agent, causing spidering to temporarily stop. # def pause! @paused = true return self end # # Sets the history of links that were previously visited to the # specified _new_history_. # # agent.history = ['http://tenderlovemaking.com/2009/05/06/ann-nokogiri-130rc1-has-been-released/'] # def history=(new_history) @history = new_history.map do |url| unless url.kind_of?(URI) URI(url.to_s) else url end end end alias visited_urls history # # Returns the +Array+ of visited URLs. # def visited_links @history.map { |uri| uri.to_s } end # # Return the +Array+ of hosts that were visited. # def visited_hosts @history.map { |uri| uri.host }.uniq end # # Returns +true+ if the specified _url_ was visited, returns +false+ # otherwise. # def visited?(url) url = URI(url) unless url.kind_of?(URI) return @history.include?(url) end # # Returns +true+ if the specified _url_ was unable to be visited, # returns +false+ otherwise. # def failed?(url) url = URI(url) unless url.kind_of?(URI) return @failures.include?(url) end alias pending_urls queue # # Creates a new Page object from the specified _url_. If a _block_ is # given, it will be passed the newly created Page object. # def get_page(url,&block) host = url.host port = url.port unless url.path.empty? path = url.path else path = '/' end proxy_host = @proxy[:host] proxy_port = @proxy[:port] proxy_user = @proxy[:user] proxy_password = @proxy[:password] begin Net::HTTP::Proxy(proxy_host,proxy_port,proxy_user,proxy_password).start(host,port) do |sess| headers = {} headers['User-Agent'] = @user_agent if @user_agent headers['Referer'] = @referer if @referer new_page = Page.new(url,sess.get(path,headers)) block.call(new_page) if block return new_page end rescue SystemCallError, Net::HTTPBadResponse failed(url) return nil end end # # Returns the agent represented as a Hash containing the agents # +history+ and +queue+ information. # def to_hash {:history => @history, :queue => @queue} end # # Sets the queue of links to visit to the specified _new_queue_. # # agent.queue = ['http://www.vimeo.com/', 'http://www.reddit.com/'] # def queue=(new_queue) @queue = new_queue.map do |url| unless url.kind_of?(URI) URI(url.to_s) else url end end end # # Returns +true+ if the specified _url_ is queued for visiting, returns # +false+ otherwise. # def queued?(url) @queue.include?(url) end # # Enqueues the specified _url_ for visiting, only if it passes all the # agent's rules for visiting a given URL. Returns +true+ if the _url_ # was successfully enqueued, returns +false+ otherwise. # def enqueue(url) link = url.to_s url = URI(link) if (!(queued?(url)) && visit?(url)) @every_url_blocks.each { |block| block.call(url) } @urls_like_blocks.each do |pattern,blocks| if ((pattern.kind_of?(Regexp) && link =~ pattern) || pattern == link || pattern == url) blocks.each { |url_block| url_block.call(url) } end end @queue << url return true end return false end protected # # Dequeues a URL that will later be visited. # def dequeue @queue.shift end # # Returns +true+ if the specified _url_ should be visited, based on # it's scheme, returns +false+ otherwise. # def visit_scheme?(url) if url.scheme return SCHEMES.include?(url.scheme) else return true end end # # Returns +true+ if the specified _url_ should be visited, based on # the host of the _url_, returns +false+ otherwise. # def visit_host?(url) @host_rules.accept?(url.host) end # # Returns +true+ if the specified _url_ should be visited, based on # the port of the _url_, returns +false+ otherwise. # def visit_port?(url) @port_rules.accept?(url.port) end # # Returns +true+ if the specified _url_ should be visited, based on # the pattern of the _url_, returns +false+ otherwise. # def visit_link?(url) @link_rules.accept?(url.to_s) end # # Returns +true+ if the specified _url_ should be visited, based on # the file extension of the _url_, returns +false+ otherwise. # def visit_ext?(url) @ext_rules.accept?(File.extname(url.path)[1..-1]) end # # Returns +true+ if the specified URL should be visited, returns # +false+ otherwise. # def visit?(url) (!(visited?(url)) && visit_scheme?(url) && visit_host?(url) && visit_port?(url) && visit_link?(url) && visit_ext?(url)) end # # Visits the spedified _url_ and enqueus it's links for visiting. If a # _block_ is given, it will be passed a newly created Page object # for the specified _url_. # def visit_page(url,&block) get_page(url) do |page| @history << page.url page.urls.each { |next_url| enqueue(next_url) } @every_page_blocks.each { |page_block| page_block.call(page) } block.call(page) if block end end # # Adds the specified _url_ to the failures list. # def failed(url) url = URI(url.to_s) unless url.kind_of?(URI) @every_failed_url_blocks.each { |block| block.call(url) } @failures << url return true end end end