fetch_util 0.3.2 → 0.5.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 +49 -0
- data/README.md +24 -10
- data/SKILL.md +10 -4
- data/lib/fetch_util/assets/extract.js +1 -1
- data/lib/fetch_util/browser/interaction_helpers/consent_helpers.rb +5 -0
- data/lib/fetch_util/browser/site_stabilization/travel_and_lodging.rb +39 -0
- data/lib/fetch_util/browser/site_stabilization.rb +2 -0
- data/lib/fetch_util/browser/stabilization/page_flow.rb +39 -6
- data/lib/fetch_util/browser/stabilization/spa_hydration.rb +17 -0
- data/lib/fetch_util/cli.rb +37 -12
- data/lib/fetch_util/fetcher.rb +149 -2
- data/lib/fetch_util/result.rb +124 -9
- data/lib/fetch_util/search_transport.rb +496 -0
- data/lib/fetch_util/searcher.rb +51 -100
- data/lib/fetch_util/version.rb +1 -1
- data/lib/fetch_util.rb +1 -0
- metadata +3 -1
data/lib/fetch_util/searcher.rb
CHANGED
|
@@ -5,77 +5,66 @@ require "uri"
|
|
|
5
5
|
|
|
6
6
|
module FetchUtil
|
|
7
7
|
class Searcher
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
SOURCES = {
|
|
11
|
-
"duckduckgo" => "https://duckduckgo.com/?q=%<query>s&ia=web&kl=us-en",
|
|
12
|
-
"google" => "https://www.google.com/search?hl=en&q=%<query>s",
|
|
13
|
-
"bing" => "https://www.bing.com/search?setlang=en-US&q=%<query>s",
|
|
14
|
-
"ecosia" => "https://www.ecosia.org/search?q=%<query>s",
|
|
15
|
-
"brave" => "https://search.brave.com/search?q=%<query>s"
|
|
16
|
-
}.freeze
|
|
17
|
-
|
|
18
|
-
DEFAULT_SOURCES = %w[duckduckgo google].freeze
|
|
8
|
+
DEFAULT_SOURCES = %w[brave bing].freeze
|
|
19
9
|
|
|
20
10
|
autoload :ResultFiltering, "fetch_util/searcher/result_filtering"
|
|
21
11
|
include ResultFiltering
|
|
22
12
|
private_constant :ResultFiltering
|
|
23
13
|
|
|
24
|
-
def initialize(
|
|
14
|
+
def initialize(transport: nil, request_log: RequestLog.new, sources: nil, limit: nil, verbose: false,
|
|
15
|
+
timeout: SearchTransport::DEFAULT_TIMEOUT)
|
|
25
16
|
@request_log = request_log
|
|
26
|
-
@sources = Array(sources || DEFAULT_SOURCES).map(&:to_s)
|
|
27
|
-
|
|
17
|
+
@sources = Array(sources || DEFAULT_SOURCES).map(&:to_s).uniq
|
|
18
|
+
unknown = @sources - SearchTransport::SOURCES.keys
|
|
19
|
+
raise ArgumentError, "unsupported search source: #{unknown.first}" if unknown.any?
|
|
20
|
+
|
|
21
|
+
validate_limit!(limit)
|
|
22
|
+
@limit = limit
|
|
28
23
|
@verbose = verbose
|
|
29
|
-
@
|
|
24
|
+
@transport = transport || SearchTransport.new(sources: @sources, timeout: timeout)
|
|
30
25
|
end
|
|
31
26
|
|
|
32
27
|
def search(query)
|
|
33
28
|
encoded_query = query.to_s.strip
|
|
34
29
|
raise ArgumentError, "query must not be empty" if encoded_query.empty?
|
|
35
30
|
|
|
36
|
-
urls = search_urls(encoded_query)
|
|
37
31
|
@request_log.append(search_request_uri(encoded_query))
|
|
38
|
-
|
|
39
|
-
@fetcher.fetch(urls.values)
|
|
40
|
-
rescue ParallelFetcher::ParallelFetchError => e
|
|
41
|
-
raise unless e.results&.compact&.any?
|
|
42
|
-
|
|
43
|
-
e.results
|
|
44
|
-
end
|
|
32
|
+
responses = @transport.search(encoded_query)
|
|
45
33
|
|
|
46
|
-
{
|
|
34
|
+
payload = {
|
|
47
35
|
query: encoded_query,
|
|
48
|
-
results: formatted_results(aggregate(
|
|
36
|
+
results: formatted_results(apply_limit(aggregate(responses)))
|
|
49
37
|
}
|
|
38
|
+
payload[:diagnostics] = diagnostics(responses) if @verbose
|
|
39
|
+
payload
|
|
50
40
|
end
|
|
51
41
|
|
|
52
42
|
private
|
|
53
43
|
|
|
54
44
|
attr_reader :limit
|
|
55
45
|
|
|
56
|
-
def
|
|
57
|
-
|
|
46
|
+
def apply_limit(results)
|
|
47
|
+
limit.nil? ? results : results.first(limit)
|
|
48
|
+
end
|
|
58
49
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
end
|
|
63
|
-
urls[source] = format(template, query: CGI.escape(query))
|
|
64
|
-
end
|
|
50
|
+
def validate_limit!(value)
|
|
51
|
+
return if value.nil?
|
|
52
|
+
return if value.is_a?(Integer) && value >= 0
|
|
65
53
|
|
|
66
|
-
|
|
54
|
+
raise ArgumentError, "limit must be a nonnegative integer"
|
|
67
55
|
end
|
|
68
56
|
|
|
69
57
|
def search_request_uri(query)
|
|
70
58
|
"search://#{@sources.join(",")}?q=#{CGI.escape(query)}"
|
|
71
59
|
end
|
|
72
60
|
|
|
73
|
-
def aggregate(
|
|
61
|
+
def aggregate(responses)
|
|
74
62
|
parsed = {}
|
|
75
63
|
max_size = 0
|
|
76
64
|
|
|
77
|
-
sources.
|
|
78
|
-
|
|
65
|
+
@sources.each do |source|
|
|
66
|
+
response = responses.find { |item| item.source == source }
|
|
67
|
+
items = response ? response.candidates.filter_map { |candidate| normalized_candidate(candidate) } : []
|
|
79
68
|
parsed[source] = items
|
|
80
69
|
max_size = [max_size, items.length].max
|
|
81
70
|
end
|
|
@@ -84,12 +73,10 @@ module FetchUtil
|
|
|
84
73
|
seen = {}
|
|
85
74
|
|
|
86
75
|
max_size.times do |index|
|
|
87
|
-
sources.each do |source|
|
|
76
|
+
@sources.each do |source|
|
|
88
77
|
item = parsed.fetch(source)[index]
|
|
89
78
|
next unless item
|
|
90
79
|
|
|
91
|
-
item = item.merge(rank: index + 1)
|
|
92
|
-
|
|
93
80
|
existing = seen[item[:url]]
|
|
94
81
|
if existing
|
|
95
82
|
merge_result!(existing, source, item)
|
|
@@ -119,7 +106,7 @@ module FetchUtil
|
|
|
119
106
|
def merge_result!(result, source, item)
|
|
120
107
|
result[:sources] << source unless result[:sources].include?(source)
|
|
121
108
|
result[:ranks][source] ||= item[:rank]
|
|
122
|
-
return if
|
|
109
|
+
return if result[:snippet] || !item[:snippet]
|
|
123
110
|
|
|
124
111
|
result[:snippet] = item[:snippet]
|
|
125
112
|
end
|
|
@@ -131,7 +118,7 @@ module FetchUtil
|
|
|
131
118
|
url: result[:url]
|
|
132
119
|
}
|
|
133
120
|
item[:snippet] = result[:snippet] if result[:snippet]
|
|
134
|
-
if verbose
|
|
121
|
+
if @verbose
|
|
135
122
|
item[:sources] = result[:sources]
|
|
136
123
|
item[:ranks] = result[:ranks]
|
|
137
124
|
end
|
|
@@ -139,75 +126,45 @@ module FetchUtil
|
|
|
139
126
|
end
|
|
140
127
|
end
|
|
141
128
|
|
|
142
|
-
def
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
next unless
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
url_start = title_end + 2
|
|
159
|
-
url_end = markdown_url_end_index(stripped, url_start)
|
|
160
|
-
return nil unless url_end
|
|
161
|
-
|
|
162
|
-
title = stripped[3...title_end]
|
|
163
|
-
url = stripped[url_start...url_end]
|
|
164
|
-
remainder = stripped[(url_end + 1)..].to_s
|
|
165
|
-
snippet = remainder.start_with?(" - ") ? remainder[3..] : nil
|
|
166
|
-
|
|
167
|
-
{ title: title, url: url, snippet: snippet }
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def markdown_url_end_index(line, url_start)
|
|
171
|
-
depth = 0
|
|
172
|
-
|
|
173
|
-
url_start.upto(line.length - 1) do |index|
|
|
174
|
-
char = line[index]
|
|
175
|
-
if char == "("
|
|
176
|
-
depth += 1
|
|
177
|
-
elsif char == ")"
|
|
178
|
-
return index if depth.zero?
|
|
179
|
-
|
|
180
|
-
depth -= 1
|
|
181
|
-
end
|
|
129
|
+
def diagnostics(responses)
|
|
130
|
+
@sources.filter_map do |source|
|
|
131
|
+
response = responses.find { |item| item.source == source }
|
|
132
|
+
next unless response
|
|
133
|
+
|
|
134
|
+
diagnostic = {
|
|
135
|
+
source: response.source,
|
|
136
|
+
transport: response.transport,
|
|
137
|
+
status: response.status,
|
|
138
|
+
result_count: response.candidates.length,
|
|
139
|
+
elapsed_ms: response.elapsed_ms
|
|
140
|
+
}
|
|
141
|
+
diagnostic[:final_url] = response.final_url if response.final_url
|
|
142
|
+
diagnostic[:reason] = response.reason if response.reason
|
|
143
|
+
diagnostic
|
|
182
144
|
end
|
|
183
|
-
|
|
184
|
-
nil
|
|
185
145
|
end
|
|
186
146
|
|
|
187
|
-
def
|
|
188
|
-
normalized_url = normalize_url(url)
|
|
147
|
+
def normalized_candidate(candidate)
|
|
148
|
+
normalized_url = normalize_url(candidate.url)
|
|
189
149
|
return nil unless normalized_url
|
|
190
150
|
|
|
191
|
-
normalized_title = normalize_title(title, normalized_url)
|
|
151
|
+
normalized_title = normalize_title(candidate.title, normalized_url)
|
|
192
152
|
return nil if normalized_title.empty? || generic_title?(normalized_title, normalized_url)
|
|
193
153
|
|
|
194
|
-
normalized_snippet = normalize_snippet(snippet, normalized_title, normalized_url)
|
|
154
|
+
normalized_snippet = normalize_snippet(candidate.snippet, normalized_title, normalized_url)
|
|
195
155
|
return nil if search_engine_self_link?(normalized_title, normalized_url, normalized_snippet)
|
|
196
156
|
return nil if low_value_result?(normalized_title, normalized_url, normalized_snippet)
|
|
197
157
|
|
|
198
158
|
item = {
|
|
199
159
|
title: normalized_title,
|
|
200
|
-
url: normalized_url
|
|
160
|
+
url: normalized_url,
|
|
161
|
+
rank: candidate.source_rank
|
|
201
162
|
}
|
|
202
163
|
|
|
203
164
|
item[:snippet] = normalized_snippet if normalized_snippet
|
|
204
165
|
item
|
|
205
166
|
end
|
|
206
167
|
|
|
207
|
-
def verbose?
|
|
208
|
-
@verbose
|
|
209
|
-
end
|
|
210
|
-
|
|
211
168
|
def compact_text(value)
|
|
212
169
|
FetchUtil.normalize_whitespace(value)
|
|
213
170
|
end
|
|
@@ -296,13 +253,7 @@ module FetchUtil
|
|
|
296
253
|
return nil if metadata_only_snippet?(text)
|
|
297
254
|
return nil if jammed_navigation_text?(text)
|
|
298
255
|
|
|
299
|
-
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
def truncate(text, max_length)
|
|
303
|
-
return text if text.length <= max_length
|
|
304
|
-
|
|
305
|
-
"#{text[0, max_length - 3].rstrip}..."
|
|
256
|
+
text
|
|
306
257
|
end
|
|
307
258
|
|
|
308
259
|
def domain_only?(text, host)
|
data/lib/fetch_util/version.rb
CHANGED
data/lib/fetch_util.rb
CHANGED
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.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- hmdne
|
|
@@ -84,6 +84,7 @@ files:
|
|
|
84
84
|
- lib/fetch_util/browser/site_stabilization/community_and_marketplace.rb
|
|
85
85
|
- lib/fetch_util/browser/site_stabilization/gitlab_repo.rb
|
|
86
86
|
- lib/fetch_util/browser/site_stabilization/social_platforms.rb
|
|
87
|
+
- lib/fetch_util/browser/site_stabilization/travel_and_lodging.rb
|
|
87
88
|
- lib/fetch_util/browser/stabilization.rb
|
|
88
89
|
- lib/fetch_util/browser/stabilization/page_flow.rb
|
|
89
90
|
- lib/fetch_util/browser/stabilization/spa_hydration.rb
|
|
@@ -113,6 +114,7 @@ files:
|
|
|
113
114
|
- lib/fetch_util/regulatory/usage_preferences.rb
|
|
114
115
|
- lib/fetch_util/request_log.rb
|
|
115
116
|
- lib/fetch_util/result.rb
|
|
117
|
+
- lib/fetch_util/search_transport.rb
|
|
116
118
|
- lib/fetch_util/searcher.rb
|
|
117
119
|
- lib/fetch_util/searcher/result_filtering.rb
|
|
118
120
|
- lib/fetch_util/version.rb
|