monkeyshines 0.0.2
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.
- data/.document +4 -0
- data/.gitignore +43 -0
- data/LICENSE +20 -0
- data/LICENSE.textile +20 -0
- data/README.textile +125 -0
- data/Rakefile +105 -0
- data/VERSION +1 -0
- data/examples/.gitignore +4 -0
- data/examples/bulk_urls/scrape_bulk_urls.rb +64 -0
- data/examples/rename_tree/rename_hdp_tree.rb +151 -0
- data/examples/rename_tree/rename_ripd_tree.rb +82 -0
- data/examples/rss_feeds/scrape_rss_feeds.rb +52 -0
- data/examples/shorturls/README.textile +111 -0
- data/examples/shorturls/bulkdump_shorturls.rb +46 -0
- data/examples/shorturls/bulkload_shorturls.rb +45 -0
- data/examples/shorturls/extract_urls.rb +12 -0
- data/examples/shorturls/multiplex_shorturl_cache.rb +32 -0
- data/examples/shorturls/old/multidump_and_fix_shorturls.rb +66 -0
- data/examples/shorturls/old/shorturl_stats.rb +81 -0
- data/examples/shorturls/scrape_shorturls.rb +112 -0
- data/examples/shorturls/shorturl_request.rb +29 -0
- data/examples/shorturls/shorturl_sequence.rb +121 -0
- data/examples/shorturls/shorturl_start_tyrant.sh +16 -0
- data/examples/shorturls/start_shorturl_cache.sh +2 -0
- data/lib/monkeyshines.rb +31 -0
- data/lib/monkeyshines/extensions.rb +16 -0
- data/lib/monkeyshines/fetcher.rb +10 -0
- data/lib/monkeyshines/fetcher/authed_http_fetcher.rb +35 -0
- data/lib/monkeyshines/fetcher/base.rb +44 -0
- data/lib/monkeyshines/fetcher/fake_fetcher.rb +19 -0
- data/lib/monkeyshines/fetcher/http_fetcher.rb +127 -0
- data/lib/monkeyshines/fetcher/http_head_fetcher.rb +23 -0
- data/lib/monkeyshines/monitor.rb +7 -0
- data/lib/monkeyshines/monitor/chunked_store.rb +23 -0
- data/lib/monkeyshines/monitor/periodic_logger.rb +33 -0
- data/lib/monkeyshines/monitor/periodic_monitor.rb +65 -0
- data/lib/monkeyshines/options.rb +59 -0
- data/lib/monkeyshines/recursive_runner.rb +26 -0
- data/lib/monkeyshines/repository/base.rb +57 -0
- data/lib/monkeyshines/repository/s3.rb +169 -0
- data/lib/monkeyshines/request_stream.rb +11 -0
- data/lib/monkeyshines/request_stream/base.rb +32 -0
- data/lib/monkeyshines/request_stream/edamame_queue.rb +54 -0
- data/lib/monkeyshines/request_stream/klass_request_stream.rb +39 -0
- data/lib/monkeyshines/request_stream/simple_request_stream.rb +22 -0
- data/lib/monkeyshines/runner.rb +161 -0
- data/lib/monkeyshines/runner_core/options.rb +5 -0
- data/lib/monkeyshines/runner_core/parsing_runner.rb +29 -0
- data/lib/monkeyshines/scrape_job/old_paginated.rb +343 -0
- data/lib/monkeyshines/scrape_job/recursive.rb +9 -0
- data/lib/monkeyshines/scrape_request.rb +136 -0
- data/lib/monkeyshines/scrape_request/paginated.rb +290 -0
- data/lib/monkeyshines/scrape_request/raw_json_contents.rb +16 -0
- data/lib/monkeyshines/scrape_request/signed_url.rb +86 -0
- data/lib/monkeyshines/store.rb +14 -0
- data/lib/monkeyshines/store/base.rb +29 -0
- data/lib/monkeyshines/store/chunked_flat_file_store.rb +37 -0
- data/lib/monkeyshines/store/conditional_store.rb +57 -0
- data/lib/monkeyshines/store/factory.rb +8 -0
- data/lib/monkeyshines/store/flat_file_store.rb +84 -0
- data/lib/monkeyshines/store/key_store.rb +51 -0
- data/lib/monkeyshines/store/null_store.rb +15 -0
- data/lib/monkeyshines/store/read_thru_store.rb +22 -0
- data/lib/monkeyshines/store/tokyo_tdb_key_store.rb +33 -0
- data/lib/monkeyshines/store/tyrant_rdb_key_store.rb +56 -0
- data/lib/monkeyshines/store/tyrant_tdb_key_store.rb +20 -0
- data/lib/monkeyshines/utils/factory_module.rb +106 -0
- data/lib/monkeyshines/utils/filename_pattern.rb +134 -0
- data/lib/monkeyshines/utils/logger.rb +15 -0
- data/lib/monkeyshines/utils/trollop-1.14/FAQ.txt +84 -0
- data/lib/monkeyshines/utils/trollop-1.14/History.txt +101 -0
- data/lib/monkeyshines/utils/trollop-1.14/Manifest.txt +7 -0
- data/lib/monkeyshines/utils/trollop-1.14/README.txt +40 -0
- data/lib/monkeyshines/utils/trollop-1.14/Rakefile +36 -0
- data/lib/monkeyshines/utils/trollop-1.14/lib/trollop.rb +744 -0
- data/lib/monkeyshines/utils/trollop-1.14/test/test_trollop.rb +1048 -0
- data/lib/monkeyshines/utils/trollop.rb +744 -0
- data/lib/monkeyshines/utils/union_interval.rb +52 -0
- data/lib/monkeyshines/utils/uri.rb +70 -0
- data/lib/monkeyshines/utils/uuid.rb +32 -0
- data/monkeyshines.gemspec +147 -0
- data/scrape_from_file.rb +44 -0
- data/spec/monkeyshines_spec.rb +7 -0
- data/spec/spec_helper.rb +9 -0
- metadata +183 -0
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'right_aws'
|
2
|
+
module Monkeyshines
|
3
|
+
module Store
|
4
|
+
|
5
|
+
#
|
6
|
+
# Large portions lifted from Thoughtbot's Paperclip gem.
|
7
|
+
#
|
8
|
+
# Amazon's S3 file hosting service is a scalable, easy place to store files for
|
9
|
+
# distribution. You can find out more about it at http://aws.amazon.com/s3
|
10
|
+
# There are a few S3-specific options for has_attached_file:
|
11
|
+
# * +s3_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
|
12
|
+
# to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon
|
13
|
+
# gives you. You can 'environment-space' this just like you do to your
|
14
|
+
# database.yml file, so different environments can use different accounts:
|
15
|
+
# development:
|
16
|
+
# access_key_id: 123...
|
17
|
+
# secret_access_key: 123...
|
18
|
+
# test:
|
19
|
+
# access_key_id: abc...
|
20
|
+
# secret_access_key: abc...
|
21
|
+
# production:
|
22
|
+
# access_key_id: 456...
|
23
|
+
# secret_access_key: 456...
|
24
|
+
# This is not required, however, and the file may simply look like this:
|
25
|
+
# access_key_id: 456...
|
26
|
+
# secret_access_key: 456...
|
27
|
+
# In which case, those access keys will be used in all environments. You can also
|
28
|
+
# put your bucket name in this file, instead of adding it to the code directly.
|
29
|
+
# This is useful when you want the same account but a different bucket for
|
30
|
+
# development versus production.
|
31
|
+
# * +s3_permissions+: This is a String that should be one of the "canned" access
|
32
|
+
# policies that S3 provides (more information can be found here:
|
33
|
+
# http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies)
|
34
|
+
# The default for Paperclip is "public-read".
|
35
|
+
# * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
|
36
|
+
# 'http' or 'https'. Defaults to 'http' when your :s3_permissions are 'public-read' (the
|
37
|
+
# default), and 'https' when your :s3_permissions are anything else.
|
38
|
+
# * +s3_headers+: A hash of headers such as {'Expires' => 1.year.from_now.httpdate}
|
39
|
+
# * +bucket+: This is the name of the S3 bucket that will store your files. Remember
|
40
|
+
# that the bucket must be unique across all of Amazon S3. If the bucket does not exist
|
41
|
+
# Paperclip will attempt to create it. The bucket name will not be interpolated.
|
42
|
+
# You can define the bucket as a Proc if you want to determine it's name at runtime.
|
43
|
+
# Paperclip will call that Proc with attachment as the only argument.
|
44
|
+
# * +s3_host_alias+: The fully-qualified domain name (FQDN) that is the alias to the
|
45
|
+
# S3 domain of your bucket. Used with the :s3_alias_url url interpolation. See the
|
46
|
+
# link in the +url+ entry for more information about S3 domains and buckets.
|
47
|
+
# * +url+: There are three options for the S3 url. You can choose to have the bucket's name
|
48
|
+
# placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket).
|
49
|
+
# Lastly, you can specify a CNAME (which requires the CNAME to be specified as
|
50
|
+
# :s3_alias_url. You can read more about CNAMEs and S3 at
|
51
|
+
# http://docs.amazonwebservices.com/AmazonS3/latest/index.html?VirtualHosting.html
|
52
|
+
# Normally, this won't matter in the slightest and you can leave the default (which is
|
53
|
+
# path-style, or :s3_path_url). But in some cases paths don't work and you need to use
|
54
|
+
# the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
|
55
|
+
# NOTE: If you use a CNAME for use with CloudFront, you can NOT specify https as your
|
56
|
+
# :s3_protocol; This is *not supported* by S3/CloudFront. Finally, when using the host
|
57
|
+
# alias, the :bucket parameter is ignored, as the hostname is used as the bucket name
|
58
|
+
# by S3.
|
59
|
+
# * +path+: This is the key under the bucket in which the file will be stored. The
|
60
|
+
# URL will be constructed from the bucket and the path. This is what you will want
|
61
|
+
# to interpolate. Keys should be unique, like filenames, and despite the fact that
|
62
|
+
# S3 (strictly speaking) does not support directories, you can still use a / to
|
63
|
+
# separate parts of your file name.
|
64
|
+
class S3Repository < Monkeyshines::Repository::Base
|
65
|
+
attr_reader :bucket_name, :s3_host_alias, :s3_protocol
|
66
|
+
|
67
|
+
def initialize options={}
|
68
|
+
@s3_credentials = parse_credentials(@options[:s3_credentials])
|
69
|
+
@bucket = @options[:bucket] || @s3_credentials[:bucket]
|
70
|
+
@bucket = @bucket.call(self) if @bucket.is_a?(Proc)
|
71
|
+
@s3_options = @options[:s3_options] || {}
|
72
|
+
@s3_permissions = @options[:s3_permissions] || 'public-read'
|
73
|
+
@s3_protocol = @options[:s3_protocol] || (@s3_permissions == 'public-read' ? 'http' : 'https')
|
74
|
+
@s3_headers = @options[:s3_headers] || {}
|
75
|
+
@s3_host_alias = @options[:s3_host_alias]
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Implementation of Monkeyshines::Repository
|
80
|
+
#
|
81
|
+
|
82
|
+
def exists?(filename)
|
83
|
+
s3_bucket.key(filename) ? true : false
|
84
|
+
end
|
85
|
+
|
86
|
+
def md5(filename)
|
87
|
+
end
|
88
|
+
|
89
|
+
#
|
90
|
+
# s3 interface
|
91
|
+
#
|
92
|
+
|
93
|
+
# Use with Monkeyshines::Utils::FilenamePattern to generate urls to s3 files.
|
94
|
+
# Ex:
|
95
|
+
# s3_url = FilenamePattern.new(":s3_path_url_base/path/to/file.ext", s3_repo.filename_pattern_tokens)
|
96
|
+
#
|
97
|
+
def filename_pattern_tokens
|
98
|
+
{ :s3_domain_url_base => "#{s3_protocol}://#{bucket_name}.s3.amazonaws.com",
|
99
|
+
:s3_alias_url_base => "#{s3_protocol}://#{s3_host_alias}",
|
100
|
+
:s3_path_url_base => "#{s3_protocol}://s3.amazonaws.com/#{bucket_name}", }
|
101
|
+
end
|
102
|
+
|
103
|
+
def s3
|
104
|
+
@s3 ||= RightAws::S3.new(@s3_credentials[:access_key_id],
|
105
|
+
@s3_credentials[:secret_access_key],
|
106
|
+
@s3_options)
|
107
|
+
end
|
108
|
+
|
109
|
+
def s3_bucket
|
110
|
+
@s3_bucket ||= s3.bucket(@bucket, true, @s3_permissions)
|
111
|
+
end
|
112
|
+
|
113
|
+
def parse_credentials creds
|
114
|
+
find_credentials(creds)
|
115
|
+
end
|
116
|
+
|
117
|
+
def find_credentials creds
|
118
|
+
case creds
|
119
|
+
when File
|
120
|
+
YAML.load_file(creds.path)
|
121
|
+
when String
|
122
|
+
YAML.load_file(creds)
|
123
|
+
when Hash
|
124
|
+
creds
|
125
|
+
else
|
126
|
+
raise ArgumentError, "Credentials are not a path, file, or hash."
|
127
|
+
end
|
128
|
+
end
|
129
|
+
private :find_credentials
|
130
|
+
|
131
|
+
# # Returns representation of the data of the file assigned to the given
|
132
|
+
# # style, in the format most representative of the current storage.
|
133
|
+
# def to_file path
|
134
|
+
# @queued_for_write[path] || s3_bucket.key(path(style))
|
135
|
+
# end
|
136
|
+
# alias_method :to_io, :to_file
|
137
|
+
#
|
138
|
+
# def flush_writes #:nodoc:
|
139
|
+
# @queued_for_write.each do |style, file|
|
140
|
+
# begin
|
141
|
+
# log("saving #{path(style)}")
|
142
|
+
# key = s3_bucket.key(path(style))
|
143
|
+
# key.data = file
|
144
|
+
# key.put(nil, @s3_permissions, {'Content-type' => instance_read(:content_type)}.merge(@s3_headers))
|
145
|
+
# rescue RightAws::AwsError => e
|
146
|
+
# raise
|
147
|
+
# end
|
148
|
+
# end
|
149
|
+
# @queued_for_write = {}
|
150
|
+
# end
|
151
|
+
#
|
152
|
+
# def flush_deletes #:nodoc:
|
153
|
+
# @queued_for_delete.each do |path|
|
154
|
+
# begin
|
155
|
+
# log("deleting #{path}")
|
156
|
+
# if file = s3_bucket.key(path)
|
157
|
+
# file.delete
|
158
|
+
# end
|
159
|
+
# rescue RightAws::AwsError
|
160
|
+
# # Ignore this.
|
161
|
+
# end
|
162
|
+
# end
|
163
|
+
# @queued_for_delete = []
|
164
|
+
# end
|
165
|
+
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Monkeyshines
|
2
|
+
module RequestStream
|
3
|
+
extend FactoryModule
|
4
|
+
autoload :Base, 'monkeyshines/request_stream/base'
|
5
|
+
autoload :KlassRequestStream, 'monkeyshines/request_stream/klass_request_stream'
|
6
|
+
autoload :KlassHashRequestStream, 'monkeyshines/request_stream/klass_request_stream'
|
7
|
+
autoload :SimpleRequestStream, 'monkeyshines/request_stream/simple_request_stream'
|
8
|
+
autoload :BeanstalkQueue, 'monkeyshines/request_stream/beanstalk_queue'
|
9
|
+
autoload :EdamameQueue, 'monkeyshines/request_stream/edamame_queue'
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Monkeyshines
|
2
|
+
module RequestStream
|
3
|
+
|
4
|
+
#
|
5
|
+
# RequestStream::Base
|
6
|
+
#
|
7
|
+
#
|
8
|
+
class Base
|
9
|
+
attr_accessor :options
|
10
|
+
Base::DEFAULT_OPTIONS = {}
|
11
|
+
def initialize _options={}
|
12
|
+
self.options = Base::DEFAULT_OPTIONS.deep_merge(_options)
|
13
|
+
Log.debug "New #{self.class} as #{options.inspect}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def each *args, &block
|
17
|
+
self.request_store.each(*args) do |*raw_req_args|
|
18
|
+
req = request_from_raw(*raw_req_args)
|
19
|
+
yield req
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def put *args
|
24
|
+
request_store.put *args
|
25
|
+
end
|
26
|
+
|
27
|
+
def skip! *args
|
28
|
+
request_store.skip! *args
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Monkeyshines
|
2
|
+
module RequestStream
|
3
|
+
#
|
4
|
+
# Watch for jobs in an Edamame priority queue
|
5
|
+
# (http://mrflip.github.com/edamame)
|
6
|
+
#
|
7
|
+
class EdamameQueue < Edamame::Broker
|
8
|
+
# How long to wait for tasks
|
9
|
+
cattr_accessor :queue_request_timeout
|
10
|
+
self.queue_request_timeout = 5 * 60 # seconds
|
11
|
+
# priority for search jobs if not otherwise given
|
12
|
+
QUEUE_PRIORITY = 65536
|
13
|
+
|
14
|
+
def initialize _options
|
15
|
+
tube = Monkeyshines::CONFIG[:handle].to_s.gsub(/_/, '-')
|
16
|
+
super _options.deep_merge( :tube => tube )
|
17
|
+
if _options[:queue_request_timeout]
|
18
|
+
Log.info "Setting timeout to #{_options[:queue_request_timeout]}"
|
19
|
+
self.queue_request_timeout = _options[:queue_request_timeout]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# def each klass, &block
|
24
|
+
# work(queue_request_timeout, klass) do |job|
|
25
|
+
# job.each_request(&block)
|
26
|
+
# end
|
27
|
+
# Log.info [queue, queue.beanstalk_stats]
|
28
|
+
# end
|
29
|
+
|
30
|
+
def each &block
|
31
|
+
work(queue_request_timeout) do |job|
|
32
|
+
yield job.obj['type'], job.obj
|
33
|
+
end
|
34
|
+
Log.info [queue, queue.beanstalk_stats]
|
35
|
+
end
|
36
|
+
|
37
|
+
def req_to_job req, job_options={}
|
38
|
+
obj_hash = req.to_hash.merge(
|
39
|
+
'type' => req.class.to_s,
|
40
|
+
'key' => [req.class.to_s, req.key].join('-') )
|
41
|
+
Edamame::Job.from_hash(job_options.merge("obj" => obj_hash,
|
42
|
+
'priority' => (66000 + 1000*req.req_generation),
|
43
|
+
'tube' => tube ))
|
44
|
+
end
|
45
|
+
|
46
|
+
def put job, *args
|
47
|
+
job_options = args.extract_options!
|
48
|
+
job = req_to_job(job, job_options) unless job.is_a?(Beanstalk::Job) || job.is_a?(Edamame::Job)
|
49
|
+
# p [self.class, job.key, job.obj,job.scheduling, job_options, args]
|
50
|
+
super job, *args
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Monkeyshines
|
2
|
+
module RequestStream
|
3
|
+
|
4
|
+
#
|
5
|
+
# KlassRequestStream is an abstract factory for requests -- the first arg
|
6
|
+
# gives the request type
|
7
|
+
#
|
8
|
+
class KlassRequestStream < Base
|
9
|
+
attr_accessor :request_store
|
10
|
+
attr_accessor :klass_scope
|
11
|
+
KlassRequestStream::DEFAULT_OPTIONS = {
|
12
|
+
:store => { :type => :flat_file_store },
|
13
|
+
:klass_scope => Kernel,
|
14
|
+
}
|
15
|
+
def initialize _options={}
|
16
|
+
super KlassRequestStream::DEFAULT_OPTIONS.deep_merge(_options)
|
17
|
+
self.request_store = Monkeyshines::Store.create(options.merge(options[:store]))
|
18
|
+
self.klass_scope = options[:klass_scope]
|
19
|
+
end
|
20
|
+
#
|
21
|
+
# use the first arg as a klass name
|
22
|
+
# to create a scrape request using rest of args
|
23
|
+
#
|
24
|
+
def request_from_raw klass_name, *raw_req_args
|
25
|
+
klass = FactoryModule.get_class(klass_scope, klass_name)
|
26
|
+
klass.new(*raw_req_args)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class KlassHashRequestStream < KlassRequestStream
|
31
|
+
def request_from_raw klass_name, hsh
|
32
|
+
klass = FactoryModule.get_class(klass_scope, klass_name)
|
33
|
+
klass.from_hash(hsh)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Monkeyshines
|
2
|
+
module RequestStream
|
3
|
+
|
4
|
+
#
|
5
|
+
# SimpleRequestStream generates an instance of options[:klass] from each element of its store
|
6
|
+
#
|
7
|
+
class SimpleRequestStream < KlassRequestStream
|
8
|
+
attr_accessor :klass
|
9
|
+
SimpleRequestStream::DEFAULT_OPTIONS = {
|
10
|
+
:klass => Monkeyshines::ScrapeRequest,
|
11
|
+
}
|
12
|
+
def initialize _options={}
|
13
|
+
super SimpleRequestStream::DEFAULT_OPTIONS.merge(_options)
|
14
|
+
self.klass = options[:klass]
|
15
|
+
end
|
16
|
+
def request_from_raw *raw_req_args
|
17
|
+
klass.new(*raw_req_args)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'monkeyshines/runner_core/options'
|
3
|
+
|
4
|
+
module Monkeyshines
|
5
|
+
class Runner
|
6
|
+
attr_accessor :options
|
7
|
+
attr_accessor :fetcher
|
8
|
+
attr_accessor :source
|
9
|
+
attr_accessor :dest
|
10
|
+
attr_accessor :periodic_log
|
11
|
+
attr_accessor :sleep_time, :force_fetch
|
12
|
+
|
13
|
+
DEFAULT_OPTIONS = {
|
14
|
+
:source => { :type => :simple_request_stream, },
|
15
|
+
:dest => { :type => :flat_file_store, :filemode => 'w'},
|
16
|
+
:fetcher => { :type => :http_fetcher, },
|
17
|
+
:log => { :dest => nil, :iters => 100, :time => 30 },
|
18
|
+
:skip => nil,
|
19
|
+
:sleep_time => 0.5,
|
20
|
+
:force_fetch => false,
|
21
|
+
}
|
22
|
+
|
23
|
+
#
|
24
|
+
# Assembles a MonkeyshinesRunner from the given plan.
|
25
|
+
#
|
26
|
+
# options_hashes is a hash tree of options to build each particular
|
27
|
+
# component. The options are deep merged with the class and global defaults.
|
28
|
+
#
|
29
|
+
# The options for each of :fetcher, :request_stream and :dest are passed to
|
30
|
+
# the Fetcher, RequestStream and Store factories respectively
|
31
|
+
#
|
32
|
+
def initialize *options_hashes
|
33
|
+
prepare_options(*options_hashes)
|
34
|
+
setup_main_log
|
35
|
+
self.source = create_source
|
36
|
+
self.fetcher = create_fetcher
|
37
|
+
self.dest = create_dest
|
38
|
+
self.sleep_time = options[:sleep_time]
|
39
|
+
self.force_fetch = options[:force_fetch]
|
40
|
+
end
|
41
|
+
|
42
|
+
def create_source
|
43
|
+
Monkeyshines::RequestStream.create(options[:source])
|
44
|
+
end
|
45
|
+
|
46
|
+
def create_dest
|
47
|
+
Monkeyshines::Store.create(options[:dest])
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_fetcher
|
51
|
+
Monkeyshines::Fetcher.create(options[:fetcher])
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Deep merges:
|
56
|
+
# * the DEFAULT_OPTIONS in runner.rb,
|
57
|
+
# * the global Monkeyshines::CONFIG loaded from disk
|
58
|
+
# * the options passed in as arguments
|
59
|
+
#
|
60
|
+
# Options appearing later win out.
|
61
|
+
#
|
62
|
+
def prepare_options *options_hashes
|
63
|
+
self.options = Hash.deep_sum(
|
64
|
+
Monkeyshines::Runner::DEFAULT_OPTIONS,
|
65
|
+
Monkeyshines::CONFIG,
|
66
|
+
*options_hashes
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# * For each entry in #source,
|
72
|
+
# ** create scrape_request(s)
|
73
|
+
# ** fetch request (...if appropriate)
|
74
|
+
# ** store result (...if fetch was successful)
|
75
|
+
# ** do logging
|
76
|
+
#
|
77
|
+
def run
|
78
|
+
Log.info "Beginning scrape itself"
|
79
|
+
before_scrape()
|
80
|
+
each_request do |req|
|
81
|
+
next unless req
|
82
|
+
before_fetch(req)
|
83
|
+
fetch_and_store(req)
|
84
|
+
after_fetch(req)
|
85
|
+
sleep sleep_time
|
86
|
+
req
|
87
|
+
end
|
88
|
+
after_scrape()
|
89
|
+
end
|
90
|
+
|
91
|
+
#
|
92
|
+
# before_scrape filter chain.
|
93
|
+
#
|
94
|
+
def before_scrape
|
95
|
+
source.skip!(options[:skip].to_i) if options[:skip]
|
96
|
+
end
|
97
|
+
|
98
|
+
#
|
99
|
+
# enumerates requests
|
100
|
+
#
|
101
|
+
def each_request &block
|
102
|
+
source.each(&block)
|
103
|
+
end
|
104
|
+
|
105
|
+
#
|
106
|
+
# before_scrape filter chain.
|
107
|
+
#
|
108
|
+
def before_fetch req
|
109
|
+
end
|
110
|
+
|
111
|
+
#
|
112
|
+
# Fetch and store result
|
113
|
+
#
|
114
|
+
#
|
115
|
+
def fetch_and_store req
|
116
|
+
# some stores (eg.conditional) only call fetcher if url key is missing.
|
117
|
+
dest.set(req.url, force_fetch) do
|
118
|
+
response = fetcher.get(req) # do the url fetch
|
119
|
+
return unless response.healthy? # don't store bad fetches
|
120
|
+
[response.scraped_at, response] # timestamp for bookkeeper, result for dest
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
#
|
125
|
+
# after_fetch
|
126
|
+
#
|
127
|
+
def after_fetch req
|
128
|
+
periodic_log.periodically{ self.log_line(req) }
|
129
|
+
end
|
130
|
+
|
131
|
+
#
|
132
|
+
# after_scrape
|
133
|
+
#
|
134
|
+
def after_scrape
|
135
|
+
dest.close
|
136
|
+
fetcher.close
|
137
|
+
end
|
138
|
+
|
139
|
+
#
|
140
|
+
# Logging
|
141
|
+
#
|
142
|
+
def setup_main_log
|
143
|
+
unless options[:log][:dest].blank?
|
144
|
+
log_file = "%s/log/%s" % [WORK_DIR, options[:log][:dest]]
|
145
|
+
FileUtils.mkdir_p(File.dirname(log_file))
|
146
|
+
$stdout = $stderr = File.open( log_file+"-console.log", "a" )
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def periodic_log
|
151
|
+
@periodic_log ||= Monkeyshines::Monitor::PeriodicLogger.new(options[:log])
|
152
|
+
end
|
153
|
+
|
154
|
+
def log_line result
|
155
|
+
result_log_line = result.blank? ? ['-','-','-'] : [result.response_code, result.url, result.contents.to_s[0..80]]
|
156
|
+
[ dest.log_line, result_log_line ].flatten
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
end
|
161
|
+
end
|