fetch_util 0.3.0 → 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 +37 -0
- data/Rakefile +5 -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/gitlab_repo.rb +32 -0
- data/lib/fetch_util/browser/site_stabilization/social_platforms.rb +25 -32
- data/lib/fetch_util/browser/site_stabilization.rb +2 -0
- data/lib/fetch_util/browser/stabilization/page_flow.rb +93 -10
- data/lib/fetch_util/browser.rb +55 -16
- data/lib/fetch_util/cli.rb +1 -0
- data/lib/fetch_util/extractor.rb +27 -3
- data/lib/fetch_util/fetcher.rb +696 -67
- data/lib/fetch_util/parallel_fetcher.rb +15 -20
- data/lib/fetch_util/raw_docs_fallback.rb +48 -24
- data/lib/fetch_util/regulatory/http_client.rb +111 -43
- data/lib/fetch_util/result.rb +100 -3
- data/lib/fetch_util/version.rb +1 -1
- data/lib/fetch_util.rb +12 -2
- metadata +3 -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 = {
|
|
@@ -20,6 +21,7 @@ module FetchUtil
|
|
|
20
21
|
"nav",
|
|
21
22
|
"aside",
|
|
22
23
|
"footer",
|
|
24
|
+
".discord",
|
|
23
25
|
".toc",
|
|
24
26
|
".sidebar",
|
|
25
27
|
".breadcrumbs",
|
|
@@ -29,17 +31,26 @@ module FetchUtil
|
|
|
29
31
|
"button"
|
|
30
32
|
].freeze
|
|
31
33
|
DOCS_ROOT_SELECTORS = [
|
|
34
|
+
"main .sl-markdown-content",
|
|
35
|
+
"main [data-pagefind-body]",
|
|
36
|
+
".sl-markdown-content",
|
|
37
|
+
"[data-pagefind-body]",
|
|
32
38
|
"main article",
|
|
33
39
|
"main",
|
|
34
40
|
"article",
|
|
35
41
|
"[role='main']",
|
|
42
|
+
"[class*='content']",
|
|
43
|
+
"[id*='content']",
|
|
44
|
+
"[class*='article']",
|
|
45
|
+
"[id*='article']",
|
|
36
46
|
".content",
|
|
37
47
|
".resource-container"
|
|
38
48
|
].freeze
|
|
39
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
|
|
40
50
|
|
|
41
|
-
def initialize(timeout: 20)
|
|
51
|
+
def initialize(timeout: 20, http_client: nil)
|
|
42
52
|
@timeout = timeout.to_i
|
|
53
|
+
@http_client = http_client || FetchUtil::HttpRedirectClient.new(timeout: @timeout, headers: DEFAULT_HEADERS)
|
|
43
54
|
end
|
|
44
55
|
|
|
45
56
|
def fetch(url)
|
|
@@ -58,7 +69,9 @@ module FetchUtil
|
|
|
58
69
|
return nil unless root
|
|
59
70
|
|
|
60
71
|
prune!(root)
|
|
61
|
-
title =
|
|
72
|
+
title = [fragment_title(document, final_url), first_heading(root), meta_title(document), document.title]
|
|
73
|
+
.map { |candidate| clean_text(candidate) }
|
|
74
|
+
.find { |candidate| !candidate.empty? }
|
|
62
75
|
markdown = markdown_from_root(root, title)
|
|
63
76
|
return nil if clean_text(markdown).length < 40
|
|
64
77
|
|
|
@@ -149,23 +162,49 @@ module FetchUtil
|
|
|
149
162
|
end
|
|
150
163
|
|
|
151
164
|
def docs_root(document)
|
|
165
|
+
candidates = []
|
|
152
166
|
DOCS_ROOT_SELECTORS.each do |selector|
|
|
153
|
-
|
|
154
|
-
|
|
167
|
+
document.css(selector).each do |node|
|
|
168
|
+
text = clean_text(node.text)
|
|
169
|
+
next if text.length < 120
|
|
170
|
+
|
|
171
|
+
candidates << [node, text.length + (node.css("p").length * 200)]
|
|
172
|
+
end
|
|
155
173
|
end
|
|
156
174
|
|
|
175
|
+
best = candidates.max_by { |_node, score| score }
|
|
176
|
+
return best.first.dup if best
|
|
177
|
+
|
|
157
178
|
body = document.at_css("body")
|
|
158
179
|
body&.dup
|
|
159
180
|
end
|
|
160
181
|
|
|
161
182
|
def prune!(root)
|
|
162
183
|
DROP_SELECTORS.each { |selector| root.css(selector).remove }
|
|
184
|
+
prune_leading_promo_cards!(root)
|
|
163
185
|
root.css("*").each do |node|
|
|
164
186
|
text = clean_text(node.text)
|
|
165
187
|
node.remove if text.match?(PRUNED_TEXT_PATTERN)
|
|
166
188
|
end
|
|
167
189
|
end
|
|
168
190
|
|
|
191
|
+
def prune_leading_promo_cards!(root)
|
|
192
|
+
return unless root.at_css("h1, h2")
|
|
193
|
+
|
|
194
|
+
root.css("article.card, .card").each do |node|
|
|
195
|
+
text = clean_text(node.text)
|
|
196
|
+
next if node.ancestors.any? { |ancestor| class_list(ancestor).any? { |klass| %w[landing-card card--fullwidth].include?(klass) } }
|
|
197
|
+
next if node.at_css(".title, .body")
|
|
198
|
+
next if text.empty? || text.length > 600 || node.at_css("h1, h2, h3, h4, h5, h6")
|
|
199
|
+
|
|
200
|
+
node.remove
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def class_list(node)
|
|
205
|
+
node["class"].to_s.split
|
|
206
|
+
end
|
|
207
|
+
|
|
169
208
|
def meta_title(document)
|
|
170
209
|
meta_value(document, "og:title", attr: "property") || document.title
|
|
171
210
|
end
|
|
@@ -235,26 +274,11 @@ module FetchUtil
|
|
|
235
274
|
%(concat(#{parts.join(%q(, "'", ))}))
|
|
236
275
|
end
|
|
237
276
|
|
|
238
|
-
def fetch_html(url
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
uri = URI.parse(url)
|
|
242
|
-
request = Net::HTTP::Get.new(uri)
|
|
243
|
-
DEFAULT_HEADERS.each { |key, value| request[key] = value }
|
|
244
|
-
|
|
245
|
-
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: @timeout, read_timeout: @timeout) do |http|
|
|
246
|
-
http.request(request)
|
|
247
|
-
end
|
|
277
|
+
def fetch_html(url)
|
|
278
|
+
response = @http_client.get(url)
|
|
279
|
+
raise Error, "HTTP #{response.status}" unless response.status.between?(200, 299)
|
|
248
280
|
|
|
249
|
-
|
|
250
|
-
when Net::HTTPSuccess
|
|
251
|
-
[uri.to_s, response.body]
|
|
252
|
-
when Net::HTTPRedirection
|
|
253
|
-
location = URI.join(uri, response["location"]).to_s
|
|
254
|
-
fetch_html(location, limit: limit - 1)
|
|
255
|
-
else
|
|
256
|
-
raise Error, "HTTP #{response.code}"
|
|
257
|
-
end
|
|
281
|
+
[response.url, response.body]
|
|
258
282
|
end
|
|
259
283
|
end
|
|
260
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/result.rb
CHANGED
|
@@ -5,11 +5,106 @@ module FetchUtil
|
|
|
5
5
|
attr_reader :url, :final_url, :title, :byline, :excerpt, :site_name,
|
|
6
6
|
:published_time, :canonical_url, :language, :html, :markdown,
|
|
7
7
|
:metadata, :reader_mode, :content_type, :suspect, :warnings,
|
|
8
|
-
:content_completeness_ratio, :content_format, :paywall_state
|
|
8
|
+
:content_completeness_ratio, :content_format, :paywall_state,
|
|
9
|
+
:error_message
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def from_payload(url:, final_url:, payload:, canonical_url:, content_type:, warnings:, suspect:)
|
|
13
|
+
content_completeness_ratio = payload["contentCompletenessRatio"]&.to_f || 1.0
|
|
14
|
+
content_format = payload["contentFormat"]
|
|
15
|
+
paywall_state = payload["paywallState"]
|
|
16
|
+
metadata = payload_metadata(
|
|
17
|
+
payload,
|
|
18
|
+
canonical_url: canonical_url,
|
|
19
|
+
final_url: final_url,
|
|
20
|
+
content_type: content_type,
|
|
21
|
+
suspect: suspect,
|
|
22
|
+
warnings: warnings,
|
|
23
|
+
content_completeness_ratio: content_completeness_ratio,
|
|
24
|
+
content_format: content_format,
|
|
25
|
+
paywall_state: paywall_state
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
new(
|
|
29
|
+
url: url,
|
|
30
|
+
final_url: final_url,
|
|
31
|
+
title: payload["title"],
|
|
32
|
+
byline: payload["byline"],
|
|
33
|
+
excerpt: payload["excerpt"],
|
|
34
|
+
site_name: payload["siteName"],
|
|
35
|
+
published_time: payload["publishedTime"],
|
|
36
|
+
canonical_url: canonical_url,
|
|
37
|
+
language: payload["language"],
|
|
38
|
+
html: payload["html"],
|
|
39
|
+
markdown: payload["markdown"],
|
|
40
|
+
metadata: metadata,
|
|
41
|
+
reader_mode: payload["readerMode"],
|
|
42
|
+
content_type: content_type,
|
|
43
|
+
suspect: suspect,
|
|
44
|
+
warnings: warnings,
|
|
45
|
+
content_completeness_ratio: content_completeness_ratio,
|
|
46
|
+
content_format: content_format,
|
|
47
|
+
paywall_state: paywall_state
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def error(url:, warning:, message:)
|
|
52
|
+
metadata = {
|
|
53
|
+
content_url: url,
|
|
54
|
+
content_type: "error",
|
|
55
|
+
suspect: true,
|
|
56
|
+
warnings: [warning],
|
|
57
|
+
error_message: message
|
|
58
|
+
}.freeze
|
|
59
|
+
|
|
60
|
+
new(
|
|
61
|
+
url: url,
|
|
62
|
+
final_url: url,
|
|
63
|
+
title: nil,
|
|
64
|
+
byline: nil,
|
|
65
|
+
excerpt: nil,
|
|
66
|
+
site_name: nil,
|
|
67
|
+
published_time: nil,
|
|
68
|
+
canonical_url: nil,
|
|
69
|
+
language: nil,
|
|
70
|
+
html: nil,
|
|
71
|
+
markdown: "",
|
|
72
|
+
metadata: metadata,
|
|
73
|
+
reader_mode: nil,
|
|
74
|
+
content_type: "error",
|
|
75
|
+
suspect: true,
|
|
76
|
+
warnings: [warning],
|
|
77
|
+
error_message: message
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def payload_metadata(payload, canonical_url:, final_url:, content_type:, suspect:, warnings:,
|
|
84
|
+
content_completeness_ratio:, content_format:, paywall_state:)
|
|
85
|
+
{
|
|
86
|
+
title: payload["title"],
|
|
87
|
+
byline: payload["byline"],
|
|
88
|
+
excerpt: payload["excerpt"],
|
|
89
|
+
site_name: payload["siteName"],
|
|
90
|
+
published_time: payload["publishedTime"],
|
|
91
|
+
canonical_url: canonical_url,
|
|
92
|
+
language: payload["language"],
|
|
93
|
+
content_url: final_url,
|
|
94
|
+
reader_mode: payload["readerMode"],
|
|
95
|
+
content_type: content_type,
|
|
96
|
+
suspect: suspect,
|
|
97
|
+
warnings: warnings,
|
|
98
|
+
content_completeness_ratio: content_completeness_ratio,
|
|
99
|
+
content_format: content_format,
|
|
100
|
+
paywall_state: paywall_state
|
|
101
|
+
}.freeze
|
|
102
|
+
end
|
|
103
|
+
end
|
|
9
104
|
|
|
10
105
|
def initialize(url:, final_url:, title:, byline:, excerpt:, site_name:, published_time:,
|
|
11
106
|
canonical_url:, language:, html:, markdown:, metadata:, reader_mode:, content_type:, suspect:, warnings:,
|
|
12
|
-
content_completeness_ratio: 1.0, content_format: nil, paywall_state: nil)
|
|
107
|
+
content_completeness_ratio: 1.0, content_format: nil, paywall_state: nil, error_message: nil)
|
|
13
108
|
@url = url
|
|
14
109
|
@final_url = final_url
|
|
15
110
|
@title = title
|
|
@@ -29,6 +124,7 @@ module FetchUtil
|
|
|
29
124
|
@content_completeness_ratio = content_completeness_ratio
|
|
30
125
|
@content_format = content_format&.freeze
|
|
31
126
|
@paywall_state = paywall_state&.freeze
|
|
127
|
+
@error_message = error_message
|
|
32
128
|
end
|
|
33
129
|
|
|
34
130
|
def to_h
|
|
@@ -51,7 +147,8 @@ module FetchUtil
|
|
|
51
147
|
warnings: warnings,
|
|
52
148
|
content_completeness_ratio: content_completeness_ratio,
|
|
53
149
|
content_format: content_format,
|
|
54
|
-
paywall_state: paywall_state
|
|
150
|
+
paywall_state: paywall_state,
|
|
151
|
+
error_message: error_message
|
|
55
152
|
}
|
|
56
153
|
end
|
|
57
154
|
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
|
|
@@ -82,6 +82,7 @@ files:
|
|
|
82
82
|
- lib/fetch_util/browser/navigation/navigator_patch.rb
|
|
83
83
|
- lib/fetch_util/browser/site_stabilization.rb
|
|
84
84
|
- lib/fetch_util/browser/site_stabilization/community_and_marketplace.rb
|
|
85
|
+
- lib/fetch_util/browser/site_stabilization/gitlab_repo.rb
|
|
85
86
|
- lib/fetch_util/browser/site_stabilization/social_platforms.rb
|
|
86
87
|
- lib/fetch_util/browser/stabilization.rb
|
|
87
88
|
- lib/fetch_util/browser/stabilization/page_flow.rb
|
|
@@ -139,7 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
139
140
|
- !ruby/object:Gem::Version
|
|
140
141
|
version: '0'
|
|
141
142
|
requirements: []
|
|
142
|
-
rubygems_version: 4.0.
|
|
143
|
+
rubygems_version: 4.0.15
|
|
143
144
|
specification_version: 4
|
|
144
145
|
summary: AI for fetching in Ruby
|
|
145
146
|
test_files: []
|