grubby 1.2.1 → 3.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/Gemfile +3 -0
- data/README.md +165 -121
- data/Rakefile +0 -13
- data/grubby.gemspec +14 -18
- data/lib/grubby/core_ext/uri.rb +14 -11
- data/lib/grubby/json_parser.rb +3 -27
- data/lib/grubby/json_scraper.rb +9 -2
- data/lib/grubby/mechanize/download.rb +1 -1
- data/lib/grubby/mechanize/fetch_with_retry.rb +2 -2
- data/lib/grubby/mechanize/file.rb +3 -2
- data/lib/grubby/mechanize/link.rb +9 -6
- data/lib/grubby/mechanize/page.rb +4 -2
- data/lib/grubby/mechanize/parser.rb +9 -9
- data/lib/grubby/page_scraper.rb +9 -2
- data/lib/grubby/scraper.rb +95 -69
- data/lib/grubby/version.rb +1 -1
- data/lib/grubby.rb +93 -58
- metadata +19 -108
- data/.gitignore +0 -9
- data/.travis.yml +0 -5
- data/lib/grubby/log.rb +0 -5
data/lib/grubby.rb
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
require "
|
|
2
|
-
|
|
1
|
+
require "pathname"
|
|
2
|
+
|
|
3
3
|
require "gorge"
|
|
4
4
|
require "mechanize"
|
|
5
|
-
require "mini_sanity"
|
|
6
|
-
require "pleasant_path"
|
|
7
5
|
require "ryoba"
|
|
8
6
|
|
|
9
7
|
require_relative "grubby/version"
|
|
10
|
-
require_relative "grubby/log"
|
|
11
8
|
|
|
12
9
|
require_relative "grubby/core_ext/string"
|
|
13
10
|
require_relative "grubby/core_ext/uri"
|
|
@@ -23,22 +20,37 @@ class Grubby < Mechanize
|
|
|
23
20
|
|
|
24
21
|
VERSION = GRUBBY_VERSION
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
class << self
|
|
24
|
+
# Logger used by Grubby.
|
|
25
|
+
#
|
|
26
|
+
# @return [Logger]
|
|
27
|
+
def logger
|
|
28
|
+
@logger ||= Logger.new($stderr).tap do |logger|
|
|
29
|
+
logger.formatter = -> (severity, time, progname, msg) do
|
|
30
|
+
"[#{time.strftime "%Y-%m-%d %H:%M:%S"}] #{severity} #{msg}\n"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
attr_writer :logger
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The minimum amount of time enforced between requests, in seconds.
|
|
39
|
+
# If the value is a Range, a random number within the Range is chosen
|
|
40
|
+
# for each request.
|
|
29
41
|
#
|
|
30
42
|
# @return [Integer, Float, Range<Integer>, Range<Float>]
|
|
31
43
|
attr_accessor :time_between_requests
|
|
32
44
|
|
|
33
45
|
# Journal file used to ensure only-once processing of resources by
|
|
34
|
-
# {
|
|
46
|
+
# {fulfill} across multiple program runs.
|
|
35
47
|
#
|
|
36
48
|
# @return [Pathname, nil]
|
|
37
49
|
attr_reader :journal
|
|
38
50
|
|
|
39
51
|
# @param journal [Pathname, String]
|
|
40
52
|
# Optional journal file used to ensure only-once processing of
|
|
41
|
-
# resources by {
|
|
53
|
+
# resources by {fulfill} across multiple program runs
|
|
42
54
|
def initialize(journal = nil)
|
|
43
55
|
super()
|
|
44
56
|
|
|
@@ -74,26 +86,32 @@ class Grubby < Mechanize
|
|
|
74
86
|
end
|
|
75
87
|
|
|
76
88
|
# Sets the journal file used to ensure only-once processing of
|
|
77
|
-
# resources by {
|
|
89
|
+
# resources by {fulfill} across multiple program runs. Setting the
|
|
78
90
|
# journal file will clear the in-memory list of previously-processed
|
|
79
91
|
# resources, and, if the journal file exists, load the list from file.
|
|
80
92
|
#
|
|
81
93
|
# @param path [Pathname, String, nil]
|
|
82
94
|
# @return [Pathname]
|
|
83
95
|
def journal=(path)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
if path
|
|
97
|
+
@journal = Pathname(path)
|
|
98
|
+
@journal.dirname.mkpath
|
|
99
|
+
require "csv"
|
|
100
|
+
@fulfilled = CSV.open(@journal, "a+") do |csv|
|
|
101
|
+
csv.each.map{|row| FulfilledEntry.new(*row) }.to_set
|
|
90
102
|
end
|
|
103
|
+
else
|
|
104
|
+
@journal = nil
|
|
105
|
+
@fulfilled = Set.new
|
|
106
|
+
end
|
|
107
|
+
|
|
91
108
|
@journal
|
|
92
109
|
end
|
|
93
110
|
|
|
94
|
-
# Calls +#head+ and returns true if
|
|
95
|
-
#
|
|
96
|
-
# do not
|
|
111
|
+
# Calls +#head+ and returns true if a response code "200" is received,
|
|
112
|
+
# false otherwise. Unlike +#head+, error response codes (e.g. "404",
|
|
113
|
+
# "500") do not result in a +Mechanize::ResponseCodeError+ being
|
|
114
|
+
# raised.
|
|
97
115
|
#
|
|
98
116
|
# @param uri [URI, String]
|
|
99
117
|
# @return [Boolean]
|
|
@@ -106,7 +124,7 @@ class Grubby < Mechanize
|
|
|
106
124
|
end
|
|
107
125
|
|
|
108
126
|
# Calls +#get+ with each of +mirror_uris+ until a successful
|
|
109
|
-
# ("200 OK") response is
|
|
127
|
+
# ("200 OK") response is received, and returns that +#get+ result.
|
|
110
128
|
# Rescues and logs +Mechanize::ResponseCodeError+ failures for all but
|
|
111
129
|
# the last mirror.
|
|
112
130
|
#
|
|
@@ -114,13 +132,13 @@ class Grubby < Mechanize
|
|
|
114
132
|
# grubby = Grubby.new
|
|
115
133
|
#
|
|
116
134
|
# urls = [
|
|
117
|
-
# "
|
|
118
|
-
# "
|
|
119
|
-
# "
|
|
120
|
-
# "
|
|
135
|
+
# "https://httpstat.us/404",
|
|
136
|
+
# "https://httpstat.us/500",
|
|
137
|
+
# "https://httpstat.us/200?foo",
|
|
138
|
+
# "https://httpstat.us/200?bar",
|
|
121
139
|
# ]
|
|
122
140
|
#
|
|
123
|
-
# grubby.get_mirrored(urls).uri # == URI("
|
|
141
|
+
# grubby.get_mirrored(urls).uri # == URI("https://httpstat.us/200?foo")
|
|
124
142
|
#
|
|
125
143
|
# grubby.get_mirrored(urls.take(2)) # raise Mechanize::ResponseCodeError
|
|
126
144
|
#
|
|
@@ -137,84 +155,101 @@ class Grubby < Mechanize
|
|
|
137
155
|
if i >= mirror_uris.length
|
|
138
156
|
raise
|
|
139
157
|
else
|
|
140
|
-
|
|
141
|
-
|
|
158
|
+
Grubby.logger.debug("Mirror failed (code #{e.response_code}): #{mirror_uris[i - 1]}")
|
|
159
|
+
Grubby.logger.debug("Try mirror: #{mirror_uris[i]}")
|
|
142
160
|
retry
|
|
143
161
|
end
|
|
144
162
|
end
|
|
145
163
|
end
|
|
146
164
|
|
|
147
165
|
# Ensures only-once processing of the resource indicated by +uri+ for
|
|
148
|
-
# the specified +purpose+.
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
166
|
+
# the specified +purpose+. The given block is executed and the result
|
|
167
|
+
# is returned if and only if the Grubby instance has not recorded a
|
|
168
|
+
# previous call to +fulfill+ for the same resource and purpose.
|
|
169
|
+
#
|
|
170
|
+
# Note that the resource is identified by both its URI and its content
|
|
171
|
+
# hash. The latter prevents superfluous and rearranged URI query
|
|
172
|
+
# string parameters from interfering with only-once processing.
|
|
173
|
+
#
|
|
174
|
+
# If {journal} is set, and if the block does not raise an exception,
|
|
175
|
+
# the resource and purpose are logged to the journal file. This
|
|
176
|
+
# enables only-once processing across multiple program runs. It also
|
|
177
|
+
# provides a means to resume batch processing after an unexpected
|
|
178
|
+
# termination.
|
|
153
179
|
#
|
|
154
180
|
# @example
|
|
155
181
|
# grubby = Grubby.new
|
|
156
182
|
#
|
|
157
|
-
# grubby.
|
|
158
|
-
#
|
|
183
|
+
# grubby.fulfill("https://example.com/posts") do |page|
|
|
184
|
+
# "first time"
|
|
159
185
|
# end
|
|
186
|
+
# # == "first time"
|
|
160
187
|
#
|
|
161
|
-
# grubby.
|
|
162
|
-
#
|
|
188
|
+
# grubby.fulfill("https://example.com/posts") do |page|
|
|
189
|
+
# "already seen" # not evaluated
|
|
163
190
|
# end
|
|
191
|
+
# # == nil
|
|
164
192
|
#
|
|
165
|
-
# grubby.
|
|
166
|
-
#
|
|
193
|
+
# grubby.fulfill("https://example.com/posts?page=1") do |page|
|
|
194
|
+
# "already seen content hash" # not evaluated
|
|
167
195
|
# end
|
|
196
|
+
# # == nil
|
|
197
|
+
#
|
|
198
|
+
# grubby.fulfill("https://example.com/posts", "again!") do |page|
|
|
199
|
+
# "already seen, but new purpose"
|
|
200
|
+
# end
|
|
201
|
+
# # == "already seen, but new purpose"
|
|
168
202
|
#
|
|
169
203
|
# @param uri [URI, String]
|
|
170
204
|
# @param purpose [String]
|
|
171
|
-
# @yield [resource]
|
|
172
205
|
# @yieldparam resource [Mechanize::Page, Mechanize::File, Mechanize::Download, ...]
|
|
173
|
-
# @
|
|
174
|
-
#
|
|
206
|
+
# @yieldreturn [Object]
|
|
207
|
+
# @return [Object, nil]
|
|
175
208
|
# @raise [Mechanize::ResponseCodeError]
|
|
176
209
|
# if fetching the resource results in error (see +Mechanize#get+)
|
|
177
|
-
def
|
|
210
|
+
def fulfill(uri, purpose = "")
|
|
178
211
|
series = []
|
|
179
212
|
|
|
180
213
|
uri = uri.to_absolute_uri
|
|
181
|
-
return
|
|
214
|
+
return unless add_fulfilled(uri, purpose, series)
|
|
182
215
|
|
|
183
216
|
normalized_uri = normalize_uri(uri)
|
|
184
|
-
return
|
|
217
|
+
return unless add_fulfilled(normalized_uri, purpose, series)
|
|
185
218
|
|
|
186
|
-
|
|
219
|
+
Grubby.logger.info("Fetch #{normalized_uri}")
|
|
187
220
|
resource = get(normalized_uri)
|
|
188
|
-
|
|
189
|
-
|
|
221
|
+
unprocessed = add_fulfilled(resource.uri, purpose, series) &
|
|
222
|
+
add_fulfilled("content hash: #{resource.content_hash}", purpose, series)
|
|
190
223
|
|
|
191
|
-
yield resource
|
|
224
|
+
result = yield resource if unprocessed
|
|
192
225
|
|
|
193
226
|
CSV.open(journal, "a") do |csv|
|
|
194
|
-
series.each{|
|
|
227
|
+
series.each{|entry| csv << entry }
|
|
195
228
|
end if journal
|
|
196
229
|
|
|
197
|
-
|
|
230
|
+
result
|
|
198
231
|
end
|
|
199
232
|
|
|
200
233
|
|
|
201
234
|
private
|
|
202
235
|
|
|
203
236
|
# @!visibility private
|
|
204
|
-
|
|
237
|
+
FulfilledEntry = Struct.new(:purpose, :target)
|
|
205
238
|
|
|
206
|
-
def
|
|
207
|
-
series <<
|
|
208
|
-
if series.uniq
|
|
209
|
-
seen_info = series.length > 1 ? "seen #{series.last.target}" : "seen"
|
|
210
|
-
$log.info("Skip #{series.first.target} (#{seen_info})")
|
|
239
|
+
def add_fulfilled(target, purpose, series)
|
|
240
|
+
series << FulfilledEntry.new(purpose, target.to_s)
|
|
241
|
+
if (series.uniq!) || @fulfilled.add?(series.last)
|
|
211
242
|
true
|
|
243
|
+
else
|
|
244
|
+
Grubby.logger.info("Skip #{series.first.target}" \
|
|
245
|
+
" (seen#{" #{series.last.target}" unless series.length == 1})")
|
|
246
|
+
false
|
|
212
247
|
end
|
|
213
248
|
end
|
|
214
249
|
|
|
215
250
|
def normalize_uri(uri)
|
|
216
251
|
uri = uri.dup
|
|
217
|
-
|
|
252
|
+
Grubby.logger.warn("Ignore ##{uri.fragment} in #{uri}") if uri.fragment
|
|
218
253
|
uri.fragment = nil
|
|
219
254
|
uri.path = uri.path.chomp("/")
|
|
220
255
|
uri
|
metadata
CHANGED
|
@@ -1,55 +1,54 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: grubby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jonathan Hefner
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
13
|
+
name: cgi
|
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
18
|
+
version: '0'
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
25
|
+
version: '0'
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
|
-
name:
|
|
27
|
+
name: csv
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
30
29
|
requirements:
|
|
31
|
-
- - "
|
|
30
|
+
- - ">="
|
|
32
31
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
32
|
+
version: '0'
|
|
34
33
|
type: :runtime
|
|
35
34
|
prerelease: false
|
|
36
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
36
|
requirements:
|
|
38
|
-
- - "
|
|
37
|
+
- - ">="
|
|
39
38
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
39
|
+
version: '0'
|
|
41
40
|
- !ruby/object:Gem::Dependency
|
|
42
41
|
name: gorge
|
|
43
42
|
requirement: !ruby/object:Gem::Requirement
|
|
44
43
|
requirements:
|
|
45
|
-
- - "
|
|
44
|
+
- - ">="
|
|
46
45
|
- !ruby/object:Gem::Version
|
|
47
46
|
version: '1.0'
|
|
48
47
|
type: :runtime
|
|
49
48
|
prerelease: false
|
|
50
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
50
|
requirements:
|
|
52
|
-
- - "
|
|
51
|
+
- - ">="
|
|
53
52
|
- !ruby/object:Gem::Version
|
|
54
53
|
version: '1.0'
|
|
55
54
|
- !ruby/object:Gem::Dependency
|
|
@@ -66,113 +65,26 @@ dependencies:
|
|
|
66
65
|
- - "~>"
|
|
67
66
|
- !ruby/object:Gem::Version
|
|
68
67
|
version: '2.7'
|
|
69
|
-
- !ruby/object:Gem::Dependency
|
|
70
|
-
name: mini_sanity
|
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
|
72
|
-
requirements:
|
|
73
|
-
- - "~>"
|
|
74
|
-
- !ruby/object:Gem::Version
|
|
75
|
-
version: '1.0'
|
|
76
|
-
type: :runtime
|
|
77
|
-
prerelease: false
|
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
-
requirements:
|
|
80
|
-
- - "~>"
|
|
81
|
-
- !ruby/object:Gem::Version
|
|
82
|
-
version: '1.0'
|
|
83
|
-
- !ruby/object:Gem::Dependency
|
|
84
|
-
name: pleasant_path
|
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
|
86
|
-
requirements:
|
|
87
|
-
- - "~>"
|
|
88
|
-
- !ruby/object:Gem::Version
|
|
89
|
-
version: '1.1'
|
|
90
|
-
type: :runtime
|
|
91
|
-
prerelease: false
|
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
-
requirements:
|
|
94
|
-
- - "~>"
|
|
95
|
-
- !ruby/object:Gem::Version
|
|
96
|
-
version: '1.1'
|
|
97
68
|
- !ruby/object:Gem::Dependency
|
|
98
69
|
name: ryoba
|
|
99
70
|
requirement: !ruby/object:Gem::Requirement
|
|
100
71
|
requirements:
|
|
101
|
-
- - "
|
|
72
|
+
- - ">="
|
|
102
73
|
- !ruby/object:Gem::Version
|
|
103
74
|
version: '1.0'
|
|
104
75
|
type: :runtime
|
|
105
76
|
prerelease: false
|
|
106
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
78
|
requirements:
|
|
108
|
-
- - "
|
|
79
|
+
- - ">="
|
|
109
80
|
- !ruby/object:Gem::Version
|
|
110
81
|
version: '1.0'
|
|
111
|
-
- !ruby/object:Gem::Dependency
|
|
112
|
-
name: bundler
|
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
|
114
|
-
requirements:
|
|
115
|
-
- - "~>"
|
|
116
|
-
- !ruby/object:Gem::Version
|
|
117
|
-
version: '1.15'
|
|
118
|
-
type: :development
|
|
119
|
-
prerelease: false
|
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
-
requirements:
|
|
122
|
-
- - "~>"
|
|
123
|
-
- !ruby/object:Gem::Version
|
|
124
|
-
version: '1.15'
|
|
125
|
-
- !ruby/object:Gem::Dependency
|
|
126
|
-
name: rake
|
|
127
|
-
requirement: !ruby/object:Gem::Requirement
|
|
128
|
-
requirements:
|
|
129
|
-
- - "~>"
|
|
130
|
-
- !ruby/object:Gem::Version
|
|
131
|
-
version: '10.0'
|
|
132
|
-
type: :development
|
|
133
|
-
prerelease: false
|
|
134
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
-
requirements:
|
|
136
|
-
- - "~>"
|
|
137
|
-
- !ruby/object:Gem::Version
|
|
138
|
-
version: '10.0'
|
|
139
|
-
- !ruby/object:Gem::Dependency
|
|
140
|
-
name: minitest
|
|
141
|
-
requirement: !ruby/object:Gem::Requirement
|
|
142
|
-
requirements:
|
|
143
|
-
- - "~>"
|
|
144
|
-
- !ruby/object:Gem::Version
|
|
145
|
-
version: '5.0'
|
|
146
|
-
type: :development
|
|
147
|
-
prerelease: false
|
|
148
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
149
|
-
requirements:
|
|
150
|
-
- - "~>"
|
|
151
|
-
- !ruby/object:Gem::Version
|
|
152
|
-
version: '5.0'
|
|
153
|
-
- !ruby/object:Gem::Dependency
|
|
154
|
-
name: yard
|
|
155
|
-
requirement: !ruby/object:Gem::Requirement
|
|
156
|
-
requirements:
|
|
157
|
-
- - "~>"
|
|
158
|
-
- !ruby/object:Gem::Version
|
|
159
|
-
version: '0.9'
|
|
160
|
-
type: :development
|
|
161
|
-
prerelease: false
|
|
162
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
163
|
-
requirements:
|
|
164
|
-
- - "~>"
|
|
165
|
-
- !ruby/object:Gem::Version
|
|
166
|
-
version: '0.9'
|
|
167
|
-
description:
|
|
168
82
|
email:
|
|
169
83
|
- jonathan@hefner.pro
|
|
170
84
|
executables: []
|
|
171
85
|
extensions: []
|
|
172
86
|
extra_rdoc_files: []
|
|
173
87
|
files:
|
|
174
|
-
- ".gitignore"
|
|
175
|
-
- ".travis.yml"
|
|
176
88
|
- CHANGELOG.md
|
|
177
89
|
- Gemfile
|
|
178
90
|
- LICENSE.txt
|
|
@@ -184,7 +96,6 @@ files:
|
|
|
184
96
|
- lib/grubby/core_ext/uri.rb
|
|
185
97
|
- lib/grubby/json_parser.rb
|
|
186
98
|
- lib/grubby/json_scraper.rb
|
|
187
|
-
- lib/grubby/log.rb
|
|
188
99
|
- lib/grubby/mechanize/download.rb
|
|
189
100
|
- lib/grubby/mechanize/fetch_with_retry.rb
|
|
190
101
|
- lib/grubby/mechanize/file.rb
|
|
@@ -197,8 +108,9 @@ files:
|
|
|
197
108
|
homepage: https://github.com/jonathanhefner/grubby
|
|
198
109
|
licenses:
|
|
199
110
|
- MIT
|
|
200
|
-
metadata:
|
|
201
|
-
|
|
111
|
+
metadata:
|
|
112
|
+
source_code_uri: https://github.com/jonathanhefner/grubby
|
|
113
|
+
changelog_uri: https://github.com/jonathanhefner/grubby/blob/master/CHANGELOG.md
|
|
202
114
|
rdoc_options: []
|
|
203
115
|
require_paths:
|
|
204
116
|
- lib
|
|
@@ -206,15 +118,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
206
118
|
requirements:
|
|
207
119
|
- - ">="
|
|
208
120
|
- !ruby/object:Gem::Version
|
|
209
|
-
version: '
|
|
121
|
+
version: '3.4'
|
|
210
122
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
211
123
|
requirements:
|
|
212
124
|
- - ">="
|
|
213
125
|
- !ruby/object:Gem::Version
|
|
214
126
|
version: '0'
|
|
215
127
|
requirements: []
|
|
216
|
-
rubygems_version:
|
|
217
|
-
signing_key:
|
|
128
|
+
rubygems_version: 4.0.10
|
|
218
129
|
specification_version: 4
|
|
219
130
|
summary: Fail-fast web scraping
|
|
220
131
|
test_files: []
|
data/.gitignore
DELETED
data/.travis.yml
DELETED