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