fetch_util 0.3.1 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/lib/fetch_util/assets/extract.js +1 -1
- data/lib/fetch_util/browser/interaction_helpers/consent_helpers.rb +187 -104
- data/lib/fetch_util/browser/interaction_helpers/dom_interaction.rb +14 -2
- data/lib/fetch_util/browser/navigation/headers_and_readiness.rb +0 -7
- data/lib/fetch_util/browser/navigation/navigator_patch.rb +3 -1
- data/lib/fetch_util/browser/site_stabilization/community_and_marketplace.rb +28 -27
- data/lib/fetch_util/browser/site_stabilization/social_platforms.rb +25 -32
- data/lib/fetch_util/browser/stabilization/page_flow.rb +93 -11
- data/lib/fetch_util/browser.rb +55 -16
- data/lib/fetch_util/extractor.rb +27 -3
- data/lib/fetch_util/fetcher.rb +270 -144
- data/lib/fetch_util/parallel_fetcher.rb +15 -20
- data/lib/fetch_util/raw_docs_fallback.rb +8 -21
- data/lib/fetch_util/regulatory/http_client.rb +111 -43
- data/lib/fetch_util/version.rb +1 -1
- data/lib/fetch_util.rb +12 -2
- metadata +2 -2
|
@@ -38,11 +38,11 @@ module FetchUtil
|
|
|
38
38
|
work = Array(urls).compact.map(&:to_s).reject(&:empty?)
|
|
39
39
|
return [] if work.empty?
|
|
40
40
|
|
|
41
|
-
jobs = Queue.new
|
|
42
|
-
failures = Queue.new
|
|
43
|
-
work.each_with_index { |url, index| jobs << [index, url] }
|
|
44
41
|
results = Array.new(work.length)
|
|
45
42
|
worker_count = [@concurrency, work.length].min
|
|
43
|
+
failures = []
|
|
44
|
+
next_index = 0
|
|
45
|
+
mutex = Mutex.new
|
|
46
46
|
|
|
47
47
|
threads = Array.new(worker_count) do
|
|
48
48
|
Thread.new do
|
|
@@ -50,44 +50,39 @@ module FetchUtil
|
|
|
50
50
|
|
|
51
51
|
begin
|
|
52
52
|
loop do
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
index = mutex.synchronize do
|
|
54
|
+
if next_index < work.length
|
|
55
|
+
current = next_index
|
|
56
|
+
next_index += 1
|
|
57
|
+
current
|
|
58
|
+
end
|
|
57
59
|
end
|
|
60
|
+
break if index.nil?
|
|
61
|
+
|
|
62
|
+
url = work[index]
|
|
58
63
|
|
|
59
64
|
begin
|
|
60
65
|
results[index] = fetcher.fetch(url)
|
|
61
66
|
rescue StandardError => e
|
|
62
|
-
failures << Failure.new(index: index, url: url, error: e)
|
|
67
|
+
mutex.synchronize { failures << Failure.new(index: index, url: url, error: e) }
|
|
63
68
|
end
|
|
64
69
|
end
|
|
65
70
|
ensure
|
|
66
71
|
fetcher.quit if fetcher.respond_to?(:quit)
|
|
67
72
|
end
|
|
68
73
|
rescue StandardError => e
|
|
69
|
-
failures << Failure.new(index: nil, url: nil, error: e)
|
|
74
|
+
mutex.synchronize { failures << Failure.new(index: nil, url: nil, error: e) }
|
|
70
75
|
end
|
|
71
76
|
end
|
|
72
77
|
|
|
73
78
|
threads.each(&:join)
|
|
74
|
-
raise_for_failures(
|
|
79
|
+
raise_for_failures(failures, results)
|
|
75
80
|
|
|
76
81
|
results
|
|
77
82
|
end
|
|
78
83
|
|
|
79
84
|
private
|
|
80
85
|
|
|
81
|
-
def drain_queue(queue)
|
|
82
|
-
items = []
|
|
83
|
-
loop do
|
|
84
|
-
items << queue.pop(true)
|
|
85
|
-
rescue ThreadError
|
|
86
|
-
break
|
|
87
|
-
end
|
|
88
|
-
items
|
|
89
|
-
end
|
|
90
|
-
|
|
91
86
|
def raise_for_failures(failures, results)
|
|
92
87
|
return if failures.empty?
|
|
93
88
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "cgi"
|
|
4
|
-
require "net/http"
|
|
5
4
|
require "nokogiri"
|
|
6
5
|
require "uri"
|
|
7
6
|
|
|
7
|
+
require_relative "regulatory/http_client"
|
|
8
|
+
|
|
8
9
|
module FetchUtil
|
|
9
10
|
class RawDocsFallback
|
|
10
11
|
DEFAULT_HEADERS = {
|
|
@@ -47,8 +48,9 @@ module FetchUtil
|
|
|
47
48
|
].freeze
|
|
48
49
|
PRUNED_TEXT_PATTERN = /\A(?:on this page|table of contents|edit this page|copy page|copy item path|search|settings|help|expand description)\z/i
|
|
49
50
|
|
|
50
|
-
def initialize(timeout: 20)
|
|
51
|
+
def initialize(timeout: 20, http_client: nil)
|
|
51
52
|
@timeout = timeout.to_i
|
|
53
|
+
@http_client = http_client || FetchUtil::HttpRedirectClient.new(timeout: @timeout, headers: DEFAULT_HEADERS)
|
|
52
54
|
end
|
|
53
55
|
|
|
54
56
|
def fetch(url)
|
|
@@ -272,26 +274,11 @@ module FetchUtil
|
|
|
272
274
|
%(concat(#{parts.join(%q(, "'", ))}))
|
|
273
275
|
end
|
|
274
276
|
|
|
275
|
-
def fetch_html(url
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
uri = URI.parse(url)
|
|
279
|
-
request = Net::HTTP::Get.new(uri)
|
|
280
|
-
DEFAULT_HEADERS.each { |key, value| request[key] = value }
|
|
281
|
-
|
|
282
|
-
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: @timeout, read_timeout: @timeout) do |http|
|
|
283
|
-
http.request(request)
|
|
284
|
-
end
|
|
277
|
+
def fetch_html(url)
|
|
278
|
+
response = @http_client.get(url)
|
|
279
|
+
raise Error, "HTTP #{response.status}" unless response.status.between?(200, 299)
|
|
285
280
|
|
|
286
|
-
|
|
287
|
-
when Net::HTTPSuccess
|
|
288
|
-
[uri.to_s, response.body]
|
|
289
|
-
when Net::HTTPRedirection
|
|
290
|
-
location = URI.join(uri, response["location"]).to_s
|
|
291
|
-
fetch_html(location, limit: limit - 1)
|
|
292
|
-
else
|
|
293
|
-
raise Error, "HTTP #{response.code}"
|
|
294
|
-
end
|
|
281
|
+
[response.url, response.body]
|
|
295
282
|
end
|
|
296
283
|
end
|
|
297
284
|
end
|
|
@@ -1,70 +1,138 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "net/http"
|
|
4
|
+
require "uri"
|
|
4
5
|
|
|
5
6
|
module FetchUtil
|
|
6
|
-
class
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
class HttpRedirectClient
|
|
8
|
+
REDIRECT_LIMIT = 5
|
|
9
|
+
TRANSIENT_ERRORS = [EOFError, IOError, SocketError, SystemCallError, Timeout::Error].freeze
|
|
10
|
+
Response = Struct.new(:url, :status, :headers, :body, :redirects, keyword_init: true)
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
def initialize(timeout:, headers: {})
|
|
13
|
+
@timeout = timeout.to_f
|
|
14
|
+
@headers = headers.reject { |_key, value| value.to_s.empty? }
|
|
15
|
+
end
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
def get(url, limit: REDIRECT_LIMIT)
|
|
18
|
+
@connections = {}
|
|
19
|
+
fetch(parse_http_uri(url), limit, [])
|
|
20
|
+
ensure
|
|
21
|
+
close_connections
|
|
22
|
+
end
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
private
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
attr_reader :timeout, :headers
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
def fetch(uri, limit, redirects)
|
|
29
|
+
response = request(uri)
|
|
30
|
+
return build_response(uri, response, redirects: redirects) unless response.is_a?(Net::HTTPRedirection)
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
raise FetchUtil::Error, "too many redirects for #{uri}" if limit <= 0
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
location = response["location"].to_s.strip
|
|
35
|
+
return build_response(uri, response, redirects: redirects) if location.empty?
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
redirect_response = build_response(uri, response)
|
|
38
|
+
fetch(uri.merge(location), limit - 1, redirects + [redirect_response])
|
|
39
|
+
end
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
http
|
|
41
|
-
http.read_timeout = timeout
|
|
41
|
+
def request(uri)
|
|
42
|
+
attempts = 0
|
|
43
|
+
begin
|
|
44
|
+
http = connection_for(uri)
|
|
42
45
|
request = Net::HTTP::Get.new(uri.request_uri.empty? ? "/" : uri.request_uri)
|
|
43
|
-
request[
|
|
44
|
-
request["User-Agent"] = user_agent unless user_agent.empty?
|
|
46
|
+
headers.each { |key, value| request[key] = value }
|
|
45
47
|
http.request(request)
|
|
48
|
+
rescue *TRANSIENT_ERRORS
|
|
49
|
+
close_connection(uri)
|
|
50
|
+
attempts += 1
|
|
51
|
+
retry if attempts <= 1
|
|
52
|
+
raise
|
|
46
53
|
end
|
|
54
|
+
end
|
|
47
55
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
def connection_for(uri)
|
|
57
|
+
key = [uri.scheme, uri.host, uri.port]
|
|
58
|
+
@connections[key] ||= Net::HTTP.start(
|
|
59
|
+
uri.host,
|
|
60
|
+
uri.port,
|
|
61
|
+
use_ssl: uri.scheme == "https",
|
|
62
|
+
open_timeout: timeout,
|
|
63
|
+
read_timeout: timeout
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def close_connection(uri)
|
|
68
|
+
key = [uri.scheme, uri.host, uri.port]
|
|
69
|
+
@connections.delete(key)&.finish
|
|
70
|
+
rescue IOError
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def close_connections
|
|
75
|
+
@connections&.each_value do |http|
|
|
76
|
+
http.finish if http.started?
|
|
77
|
+
rescue IOError
|
|
78
|
+
nil
|
|
56
79
|
end
|
|
80
|
+
@connections = nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def build_response(uri, response, redirects: [])
|
|
84
|
+
Response.new(
|
|
85
|
+
url: uri.to_s,
|
|
86
|
+
status: response.code.to_i,
|
|
87
|
+
headers: response.to_hash.transform_keys(&:downcase),
|
|
88
|
+
body: response.body.to_s,
|
|
89
|
+
redirects: redirects
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def parse_http_uri(url)
|
|
94
|
+
uri = URI.parse(url.to_s)
|
|
95
|
+
raise URI::InvalidURIError, "unsupported url: #{url}" unless uri.is_a?(URI::HTTP) && uri.host
|
|
96
|
+
|
|
97
|
+
uri
|
|
98
|
+
end
|
|
99
|
+
end
|
|
57
100
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
101
|
+
class Regulatory
|
|
102
|
+
class HttpClient
|
|
103
|
+
DEFAULT_ACCEPT = "text/html,application/json,text/plain,*/*"
|
|
104
|
+
|
|
105
|
+
def initialize(timeout:, user_agent:, redirect_client: nil)
|
|
106
|
+
@user_agent = user_agent.to_s.strip
|
|
107
|
+
@redirect_client = redirect_client || FetchUtil::HttpRedirectClient.new(timeout: timeout, headers: request_headers)
|
|
108
|
+
end
|
|
63
109
|
|
|
64
|
-
|
|
110
|
+
def get(url, limit: FetchUtil::HttpRedirectClient::REDIRECT_LIMIT)
|
|
111
|
+
build_response(redirect_client.get(url, limit: limit))
|
|
65
112
|
rescue URI::InvalidURIError
|
|
66
113
|
raise ArgumentError, "unsupported url: #{url}"
|
|
67
114
|
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
attr_reader :redirect_client, :user_agent
|
|
119
|
+
|
|
120
|
+
def build_response(response)
|
|
121
|
+
FetchUtil::Regulatory::Response.new(
|
|
122
|
+
url: response.url,
|
|
123
|
+
status: response.status,
|
|
124
|
+
headers: response.headers,
|
|
125
|
+
body: response.body,
|
|
126
|
+
redirects: response.redirects.map { |redirect| build_response(redirect) }
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def request_headers
|
|
131
|
+
{
|
|
132
|
+
"Accept" => DEFAULT_ACCEPT,
|
|
133
|
+
"User-Agent" => user_agent
|
|
134
|
+
}
|
|
135
|
+
end
|
|
68
136
|
end
|
|
69
137
|
end
|
|
70
138
|
end
|
data/lib/fetch_util/version.rb
CHANGED
data/lib/fetch_util.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "addressable/uri"
|
|
3
4
|
require "uri"
|
|
4
5
|
|
|
5
6
|
require_relative "fetch_util/version"
|
|
@@ -88,12 +89,21 @@ module FetchUtil
|
|
|
88
89
|
text.gsub(/\u00A0/, " ").gsub(/\s+/, " ").strip
|
|
89
90
|
end
|
|
90
91
|
|
|
92
|
+
def normalize_url(url)
|
|
93
|
+
return url if url.nil? || url.to_s.empty?
|
|
94
|
+
|
|
95
|
+
Addressable::URI.parse(url.to_s).normalize.to_s
|
|
96
|
+
rescue Addressable::URI::InvalidURIError, URI::InvalidURIError
|
|
97
|
+
url.to_s
|
|
98
|
+
end
|
|
99
|
+
|
|
91
100
|
def strip_www_host(url)
|
|
92
|
-
URI.parse(url.to_s)
|
|
101
|
+
uri = url.is_a?(URI::Generic) ? url : URI.parse(url.to_s)
|
|
102
|
+
uri.host.to_s.downcase.sub(/\Awww\./, "")
|
|
93
103
|
end
|
|
94
104
|
|
|
95
105
|
def docs_like_url?(value)
|
|
96
|
-
uri = value.is_a?(URI::Generic) ? value : URI.parse(value.to_s.strip)
|
|
106
|
+
uri = value.is_a?(URI::Generic) ? value : URI.parse(normalize_url(value.to_s.strip))
|
|
97
107
|
return false unless uri.is_a?(URI::HTTP) && uri.host
|
|
98
108
|
|
|
99
109
|
host = strip_www_host(uri)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fetch_util
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- hmdne
|
|
@@ -140,7 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
140
140
|
- !ruby/object:Gem::Version
|
|
141
141
|
version: '0'
|
|
142
142
|
requirements: []
|
|
143
|
-
rubygems_version: 4.0.
|
|
143
|
+
rubygems_version: 4.0.15
|
|
144
144
|
specification_version: 4
|
|
145
145
|
summary: AI for fetching in Ruby
|
|
146
146
|
test_files: []
|