fetch_util 0.3.1 → 0.4.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 +54 -0
- data/README.md +6 -8
- data/lib/fetch_util/assets/extract.js +1 -1
- data/lib/fetch_util/browser/interaction_helpers/consent_helpers.rb +192 -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/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 +128 -13
- data/lib/fetch_util/browser/stabilization/spa_hydration.rb +17 -0
- data/lib/fetch_util/browser.rb +55 -16
- data/lib/fetch_util/cli.rb +36 -10
- data/lib/fetch_util/extractor.rb +27 -3
- data/lib/fetch_util/fetcher.rb +417 -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/result.rb +124 -9
- data/lib/fetch_util/searcher.rb +9 -17
- 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 = {
|
|
@@ -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/result.rb
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
module FetchUtil
|
|
4
4
|
class Result
|
|
5
5
|
attr_reader :url, :final_url, :title, :byline, :excerpt, :site_name,
|
|
6
|
-
:published_time, :canonical_url, :language, :
|
|
6
|
+
:published_time, :canonical_url, :language, :name, :company, :location,
|
|
7
|
+
:description, :ingredients, :instructions, :bedrooms, :bathrooms,
|
|
8
|
+
:area_sqft, :html, :markdown,
|
|
7
9
|
:metadata, :reader_mode, :content_type, :suspect, :warnings,
|
|
8
10
|
:content_completeness_ratio, :content_format, :paywall_state,
|
|
9
|
-
:
|
|
11
|
+
:price, :rating, :address, :social_kind, :platform, :handle,
|
|
12
|
+
:reply_count, :community, :score, :error_message
|
|
10
13
|
|
|
11
14
|
class << self
|
|
12
15
|
def from_payload(url:, final_url:, payload:, canonical_url:, content_type:, warnings:, suspect:)
|
|
@@ -22,7 +25,25 @@ module FetchUtil
|
|
|
22
25
|
warnings: warnings,
|
|
23
26
|
content_completeness_ratio: content_completeness_ratio,
|
|
24
27
|
content_format: content_format,
|
|
25
|
-
paywall_state: paywall_state
|
|
28
|
+
paywall_state: paywall_state,
|
|
29
|
+
name: payload["name"],
|
|
30
|
+
company: payload["company"],
|
|
31
|
+
location: payload["location"],
|
|
32
|
+
description: payload["description"],
|
|
33
|
+
ingredients: payload["ingredients"],
|
|
34
|
+
instructions: payload["instructions"],
|
|
35
|
+
bedrooms: payload["bedrooms"],
|
|
36
|
+
bathrooms: payload["bathrooms"],
|
|
37
|
+
area_sqft: payload["areaSqft"],
|
|
38
|
+
price: payload["price"],
|
|
39
|
+
rating: payload["rating"],
|
|
40
|
+
address: payload["address"],
|
|
41
|
+
social_kind: payload["socialKind"],
|
|
42
|
+
platform: payload["platform"],
|
|
43
|
+
handle: payload["handle"],
|
|
44
|
+
reply_count: payload["replyCount"],
|
|
45
|
+
community: payload["community"],
|
|
46
|
+
score: payload["score"]
|
|
26
47
|
)
|
|
27
48
|
|
|
28
49
|
new(
|
|
@@ -35,6 +56,15 @@ module FetchUtil
|
|
|
35
56
|
published_time: payload["publishedTime"],
|
|
36
57
|
canonical_url: canonical_url,
|
|
37
58
|
language: payload["language"],
|
|
59
|
+
name: payload["name"],
|
|
60
|
+
company: payload["company"],
|
|
61
|
+
location: payload["location"],
|
|
62
|
+
description: payload["description"],
|
|
63
|
+
ingredients: payload["ingredients"],
|
|
64
|
+
instructions: payload["instructions"],
|
|
65
|
+
bedrooms: payload["bedrooms"],
|
|
66
|
+
bathrooms: payload["bathrooms"],
|
|
67
|
+
area_sqft: payload["areaSqft"],
|
|
38
68
|
html: payload["html"],
|
|
39
69
|
markdown: payload["markdown"],
|
|
40
70
|
metadata: metadata,
|
|
@@ -44,7 +74,16 @@ module FetchUtil
|
|
|
44
74
|
warnings: warnings,
|
|
45
75
|
content_completeness_ratio: content_completeness_ratio,
|
|
46
76
|
content_format: content_format,
|
|
47
|
-
paywall_state: paywall_state
|
|
77
|
+
paywall_state: paywall_state,
|
|
78
|
+
price: payload["price"],
|
|
79
|
+
rating: payload["rating"],
|
|
80
|
+
address: payload["address"],
|
|
81
|
+
social_kind: payload["socialKind"],
|
|
82
|
+
platform: payload["platform"],
|
|
83
|
+
handle: payload["handle"],
|
|
84
|
+
reply_count: payload["replyCount"],
|
|
85
|
+
community: payload["community"],
|
|
86
|
+
score: payload["score"]
|
|
48
87
|
)
|
|
49
88
|
end
|
|
50
89
|
|
|
@@ -54,7 +93,13 @@ module FetchUtil
|
|
|
54
93
|
content_type: "error",
|
|
55
94
|
suspect: true,
|
|
56
95
|
warnings: [warning],
|
|
57
|
-
error_message: message
|
|
96
|
+
error_message: message,
|
|
97
|
+
social_kind: nil,
|
|
98
|
+
platform: nil,
|
|
99
|
+
handle: nil,
|
|
100
|
+
reply_count: nil,
|
|
101
|
+
community: nil,
|
|
102
|
+
score: nil
|
|
58
103
|
}.freeze
|
|
59
104
|
|
|
60
105
|
new(
|
|
@@ -67,6 +112,15 @@ module FetchUtil
|
|
|
67
112
|
published_time: nil,
|
|
68
113
|
canonical_url: nil,
|
|
69
114
|
language: nil,
|
|
115
|
+
name: nil,
|
|
116
|
+
company: nil,
|
|
117
|
+
location: nil,
|
|
118
|
+
description: nil,
|
|
119
|
+
ingredients: nil,
|
|
120
|
+
instructions: nil,
|
|
121
|
+
bedrooms: nil,
|
|
122
|
+
bathrooms: nil,
|
|
123
|
+
area_sqft: nil,
|
|
70
124
|
html: nil,
|
|
71
125
|
markdown: "",
|
|
72
126
|
metadata: metadata,
|
|
@@ -81,7 +135,10 @@ module FetchUtil
|
|
|
81
135
|
private
|
|
82
136
|
|
|
83
137
|
def payload_metadata(payload, canonical_url:, final_url:, content_type:, suspect:, warnings:,
|
|
84
|
-
content_completeness_ratio:, content_format:, paywall_state
|
|
138
|
+
content_completeness_ratio:, content_format:, paywall_state:,
|
|
139
|
+
name:, company:, location:, description:, ingredients:, instructions:,
|
|
140
|
+
bedrooms:, bathrooms:, area_sqft:, price:, rating:, address:,
|
|
141
|
+
social_kind:, platform:, handle:, reply_count:, community:, score:)
|
|
85
142
|
{
|
|
86
143
|
title: payload["title"],
|
|
87
144
|
byline: payload["byline"],
|
|
@@ -90,6 +147,15 @@ module FetchUtil
|
|
|
90
147
|
published_time: payload["publishedTime"],
|
|
91
148
|
canonical_url: canonical_url,
|
|
92
149
|
language: payload["language"],
|
|
150
|
+
name: name,
|
|
151
|
+
company: company,
|
|
152
|
+
location: location,
|
|
153
|
+
description: description,
|
|
154
|
+
ingredients: ingredients,
|
|
155
|
+
instructions: instructions,
|
|
156
|
+
bedrooms: bedrooms,
|
|
157
|
+
bathrooms: bathrooms,
|
|
158
|
+
area_sqft: area_sqft,
|
|
93
159
|
content_url: final_url,
|
|
94
160
|
reader_mode: payload["readerMode"],
|
|
95
161
|
content_type: content_type,
|
|
@@ -97,14 +163,27 @@ module FetchUtil
|
|
|
97
163
|
warnings: warnings,
|
|
98
164
|
content_completeness_ratio: content_completeness_ratio,
|
|
99
165
|
content_format: content_format,
|
|
100
|
-
paywall_state: paywall_state
|
|
166
|
+
paywall_state: paywall_state,
|
|
167
|
+
price: price,
|
|
168
|
+
rating: rating,
|
|
169
|
+
address: address,
|
|
170
|
+
social_kind: social_kind,
|
|
171
|
+
platform: platform,
|
|
172
|
+
handle: handle,
|
|
173
|
+
reply_count: reply_count,
|
|
174
|
+
community: community,
|
|
175
|
+
score: score
|
|
101
176
|
}.freeze
|
|
102
177
|
end
|
|
103
178
|
end
|
|
104
179
|
|
|
105
180
|
def initialize(url:, final_url:, title:, byline:, excerpt:, site_name:, published_time:,
|
|
106
|
-
canonical_url:, language:,
|
|
107
|
-
|
|
181
|
+
canonical_url:, language:, name:, company:, location:, description:, ingredients:,
|
|
182
|
+
instructions:, bedrooms:, bathrooms:, area_sqft:, html:, markdown:, metadata:,
|
|
183
|
+
reader_mode:, content_type:, suspect:, warnings:,
|
|
184
|
+
content_completeness_ratio: 1.0, content_format: nil, paywall_state: nil,
|
|
185
|
+
price: nil, rating: nil, address: nil, social_kind: nil, platform: nil,
|
|
186
|
+
handle: nil, reply_count: nil, community: nil, score: nil, error_message: nil)
|
|
108
187
|
@url = url
|
|
109
188
|
@final_url = final_url
|
|
110
189
|
@title = title
|
|
@@ -114,6 +193,15 @@ module FetchUtil
|
|
|
114
193
|
@published_time = published_time
|
|
115
194
|
@canonical_url = canonical_url
|
|
116
195
|
@language = language
|
|
196
|
+
@name = name
|
|
197
|
+
@company = company
|
|
198
|
+
@location = location
|
|
199
|
+
@description = description
|
|
200
|
+
@ingredients = ingredients
|
|
201
|
+
@instructions = instructions
|
|
202
|
+
@bedrooms = bedrooms
|
|
203
|
+
@bathrooms = bathrooms
|
|
204
|
+
@area_sqft = area_sqft
|
|
117
205
|
@html = html
|
|
118
206
|
@markdown = markdown
|
|
119
207
|
@metadata = metadata.freeze
|
|
@@ -124,6 +212,15 @@ module FetchUtil
|
|
|
124
212
|
@content_completeness_ratio = content_completeness_ratio
|
|
125
213
|
@content_format = content_format&.freeze
|
|
126
214
|
@paywall_state = paywall_state&.freeze
|
|
215
|
+
@price = price
|
|
216
|
+
@rating = rating
|
|
217
|
+
@address = address
|
|
218
|
+
@social_kind = social_kind
|
|
219
|
+
@platform = platform
|
|
220
|
+
@handle = handle
|
|
221
|
+
@reply_count = reply_count
|
|
222
|
+
@community = community
|
|
223
|
+
@score = score
|
|
127
224
|
@error_message = error_message
|
|
128
225
|
end
|
|
129
226
|
|
|
@@ -138,6 +235,15 @@ module FetchUtil
|
|
|
138
235
|
published_time: published_time,
|
|
139
236
|
canonical_url: canonical_url,
|
|
140
237
|
language: language,
|
|
238
|
+
name: name,
|
|
239
|
+
company: company,
|
|
240
|
+
location: location,
|
|
241
|
+
description: description,
|
|
242
|
+
ingredients: ingredients,
|
|
243
|
+
instructions: instructions,
|
|
244
|
+
bedrooms: bedrooms,
|
|
245
|
+
bathrooms: bathrooms,
|
|
246
|
+
area_sqft: area_sqft,
|
|
141
247
|
html: html,
|
|
142
248
|
markdown: markdown,
|
|
143
249
|
metadata: metadata,
|
|
@@ -148,6 +254,15 @@ module FetchUtil
|
|
|
148
254
|
content_completeness_ratio: content_completeness_ratio,
|
|
149
255
|
content_format: content_format,
|
|
150
256
|
paywall_state: paywall_state,
|
|
257
|
+
price: price,
|
|
258
|
+
rating: rating,
|
|
259
|
+
address: address,
|
|
260
|
+
social_kind: social_kind,
|
|
261
|
+
platform: platform,
|
|
262
|
+
handle: handle,
|
|
263
|
+
reply_count: reply_count,
|
|
264
|
+
community: community,
|
|
265
|
+
score: score,
|
|
151
266
|
error_message: error_message
|
|
152
267
|
}
|
|
153
268
|
end
|
data/lib/fetch_util/searcher.rb
CHANGED
|
@@ -5,8 +5,6 @@ require "uri"
|
|
|
5
5
|
|
|
6
6
|
module FetchUtil
|
|
7
7
|
class Searcher
|
|
8
|
-
MAX_SNIPPET_LENGTH = 180
|
|
9
|
-
|
|
10
8
|
SOURCES = {
|
|
11
9
|
"duckduckgo" => "https://duckduckgo.com/?q=%<query>s&ia=web&kl=us-en",
|
|
12
10
|
"google" => "https://www.google.com/search?hl=en&q=%<query>s",
|
|
@@ -21,10 +19,10 @@ module FetchUtil
|
|
|
21
19
|
include ResultFiltering
|
|
22
20
|
private_constant :ResultFiltering
|
|
23
21
|
|
|
24
|
-
def initialize(fetcher: nil, request_log: RequestLog.new, sources: nil, limit:
|
|
22
|
+
def initialize(fetcher: nil, request_log: RequestLog.new, sources: nil, limit: nil, concurrency: 2, verbose: false, **fetch_options)
|
|
25
23
|
@request_log = request_log
|
|
26
24
|
@sources = Array(sources || DEFAULT_SOURCES).map(&:to_s)
|
|
27
|
-
@limit = limit
|
|
25
|
+
@limit = limit
|
|
28
26
|
@verbose = verbose
|
|
29
27
|
@fetcher = fetcher || ParallelFetcher.new(concurrency: concurrency, request_log: request_log, **fetch_options)
|
|
30
28
|
end
|
|
@@ -45,7 +43,7 @@ module FetchUtil
|
|
|
45
43
|
|
|
46
44
|
{
|
|
47
45
|
query: encoded_query,
|
|
48
|
-
results: formatted_results(aggregate(urls.keys, fetched)
|
|
46
|
+
results: formatted_results(apply_limit(aggregate(urls.keys, fetched)))
|
|
49
47
|
}
|
|
50
48
|
end
|
|
51
49
|
|
|
@@ -53,6 +51,10 @@ module FetchUtil
|
|
|
53
51
|
|
|
54
52
|
attr_reader :limit
|
|
55
53
|
|
|
54
|
+
def apply_limit(results)
|
|
55
|
+
limit.nil? ? results : results.first(limit)
|
|
56
|
+
end
|
|
57
|
+
|
|
56
58
|
def search_urls(query)
|
|
57
59
|
urls = {}
|
|
58
60
|
|
|
@@ -131,7 +133,7 @@ module FetchUtil
|
|
|
131
133
|
url: result[:url]
|
|
132
134
|
}
|
|
133
135
|
item[:snippet] = result[:snippet] if result[:snippet]
|
|
134
|
-
if verbose
|
|
136
|
+
if @verbose
|
|
135
137
|
item[:sources] = result[:sources]
|
|
136
138
|
item[:ranks] = result[:ranks]
|
|
137
139
|
end
|
|
@@ -204,10 +206,6 @@ module FetchUtil
|
|
|
204
206
|
item
|
|
205
207
|
end
|
|
206
208
|
|
|
207
|
-
def verbose?
|
|
208
|
-
@verbose
|
|
209
|
-
end
|
|
210
|
-
|
|
211
209
|
def compact_text(value)
|
|
212
210
|
FetchUtil.normalize_whitespace(value)
|
|
213
211
|
end
|
|
@@ -296,13 +294,7 @@ module FetchUtil
|
|
|
296
294
|
return nil if metadata_only_snippet?(text)
|
|
297
295
|
return nil if jammed_navigation_text?(text)
|
|
298
296
|
|
|
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}..."
|
|
297
|
+
text
|
|
306
298
|
end
|
|
307
299
|
|
|
308
300
|
def domain_only?(text, host)
|
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.
|
|
4
|
+
version: 0.4.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
|
|
@@ -140,7 +141,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
140
141
|
- !ruby/object:Gem::Version
|
|
141
142
|
version: '0'
|
|
142
143
|
requirements: []
|
|
143
|
-
rubygems_version: 4.0.
|
|
144
|
+
rubygems_version: 4.0.15
|
|
144
145
|
specification_version: 4
|
|
145
146
|
summary: AI for fetching in Ruby
|
|
146
147
|
test_files: []
|