html2rss 0.20.1 → 0.22.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/html2rss.gemspec +1 -2
- data/lib/html2rss/auto_source/scraper/html/class_clustering.rb +185 -0
- data/lib/html2rss/auto_source/scraper/html.rb +73 -16
- data/lib/html2rss/auto_source/scraper/json_state.rb +41 -28
- data/lib/html2rss/auto_source/scraper/link_heuristics.rb +85 -131
- data/lib/html2rss/auto_source/scraper/microdata.rb +2 -2
- data/lib/html2rss/auto_source/scraper/schema/category_extractor.rb +74 -28
- data/lib/html2rss/auto_source/scraper/schema/list_item.rb +3 -2
- data/lib/html2rss/auto_source/scraper/schema/thing.rb +31 -60
- data/lib/html2rss/auto_source/scraper/schema.rb +8 -2
- data/lib/html2rss/auto_source/scraper/semantic_html/deduplicator.rb +4 -18
- data/lib/html2rss/auto_source/scraper/semantic_html.rb +55 -11
- data/lib/html2rss/auto_source/scraper.rb +0 -3
- data/lib/html2rss/auto_source.rb +5 -12
- data/lib/html2rss/category_extractor.rb +54 -20
- data/lib/html2rss/cli.rb +61 -10
- data/lib/html2rss/config/class_methods.rb +3 -3
- data/lib/html2rss/config/validator.rb +1 -0
- data/lib/html2rss/config.rb +2 -2
- data/lib/html2rss/html_extractor/enclosure_extractor.rb +60 -89
- data/lib/html2rss/html_extractor/heading_extractor.rb +50 -0
- data/lib/html2rss/html_extractor/id_generator.rb +67 -0
- data/lib/html2rss/html_extractor/list_candidates.rb +2 -8
- data/lib/html2rss/html_extractor/semantic_anchor_candidates.rb +29 -12
- data/lib/html2rss/html_extractor/semantic_containers.rb +9 -35
- data/lib/html2rss/html_extractor/text_extractor.rb +77 -0
- data/lib/html2rss/html_extractor.rb +80 -61
- data/lib/html2rss/rendering/description_builder.rb +3 -3
- data/lib/html2rss/rendering.rb +2 -2
- data/lib/html2rss/request_service/local_file_strategy.rb +29 -0
- data/lib/html2rss/request_service/puppet_commander.rb +4 -0
- data/lib/html2rss/request_service.rb +2 -1
- data/lib/html2rss/rss_builder/article.rb +44 -23
- data/lib/html2rss/rss_builder/enclosure.rb +4 -2
- data/lib/html2rss/selectors/extractors/attribute.rb +1 -1
- data/lib/html2rss/selectors/extractors/href.rb +1 -1
- data/lib/html2rss/selectors/extractors/html.rb +1 -1
- data/lib/html2rss/selectors/extractors/static.rb +1 -1
- data/lib/html2rss/selectors/extractors/text.rb +1 -1
- data/lib/html2rss/selectors/post_processors/sanitize_html.rb +32 -36
- data/lib/html2rss/selectors/post_processors/substring.rb +11 -18
- data/lib/html2rss/selectors/post_processors/template.rb +3 -2
- data/lib/html2rss/selectors.rb +19 -5
- data/lib/html2rss/url.rb +29 -15
- data/lib/html2rss/version.rb +1 -1
- data/lib/html2rss.rb +28 -6
- data/schema/html2rss-config.schema.json +12 -1
- metadata +8 -17
|
@@ -29,7 +29,7 @@ module Html2rss
|
|
|
29
29
|
# selector: h1
|
|
30
30
|
# post_process:
|
|
31
31
|
# name: template
|
|
32
|
-
# string: '
|
|
32
|
+
# string: '`%{self}` (`%{price}`)'
|
|
33
33
|
#
|
|
34
34
|
# Would return:
|
|
35
35
|
# 'Product (23,42€)'
|
|
@@ -54,12 +54,13 @@ module Html2rss
|
|
|
54
54
|
@scraper = context[:scraper]
|
|
55
55
|
@item = context[:item]
|
|
56
56
|
@string = @options[:string].to_s
|
|
57
|
+
@getter = ->(key) { item_value(key) }
|
|
57
58
|
end
|
|
58
59
|
|
|
59
60
|
##
|
|
60
61
|
# @return [String]
|
|
61
62
|
def get
|
|
62
|
-
Html2rss::Config::DynamicParams.call(@string, {}, getter:
|
|
63
|
+
Html2rss::Config::DynamicParams.call(@string, {}, getter: @getter, replace_missing_with: '')
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
private
|
data/lib/html2rss/selectors.rb
CHANGED
|
@@ -18,7 +18,7 @@ module Html2rss
|
|
|
18
18
|
include Enumerable
|
|
19
19
|
|
|
20
20
|
# A context instance passed to item extractors and post-processors.
|
|
21
|
-
Context = Struct.new('Context', :options, :item, :config, :scraper, keyword_init: true)
|
|
21
|
+
Context = Struct.new('Context', :options, :item, :config, :scraper, keyword_init: true)
|
|
22
22
|
|
|
23
23
|
# Default selectors options merged into user configuration.
|
|
24
24
|
DEFAULT_CONFIG = { items: { enhance: true } }.freeze
|
|
@@ -199,7 +199,10 @@ module Html2rss
|
|
|
199
199
|
end
|
|
200
200
|
|
|
201
201
|
def select_regular(_name, item:, config:, base_url:)
|
|
202
|
-
|
|
202
|
+
@merged_configs ||= {}
|
|
203
|
+
merged_config = @merged_configs[[config.object_id, base_url]] ||=
|
|
204
|
+
config.merge(channel: channel_context(base_url)).freeze
|
|
205
|
+
value = Extractors.get(merged_config, item)
|
|
203
206
|
|
|
204
207
|
if value && (post_process_steps = config[:post_process])
|
|
205
208
|
steps = post_process_steps.is_a?(Array) ? post_process_steps : [post_process_steps]
|
|
@@ -210,8 +213,9 @@ module Html2rss
|
|
|
210
213
|
end
|
|
211
214
|
|
|
212
215
|
def post_process(item, value, post_process_steps, base_url:)
|
|
216
|
+
pp_context = channel_post_process_context(base_url)
|
|
213
217
|
post_process_steps.each do |options|
|
|
214
|
-
context = Context.new(config:
|
|
218
|
+
context = Context.new(config: pp_context,
|
|
215
219
|
item:, scraper: self, options:)
|
|
216
220
|
|
|
217
221
|
value = PostProcessors.get(options[:name], value, context)
|
|
@@ -262,7 +266,11 @@ module Html2rss
|
|
|
262
266
|
end
|
|
263
267
|
|
|
264
268
|
def category_node_options(selector_config, base_url:)
|
|
265
|
-
|
|
269
|
+
@category_node_configs ||= {}
|
|
270
|
+
@category_node_configs[[selector_config.object_id, base_url]] ||= selector_config.merge(
|
|
271
|
+
channel: channel_context(base_url),
|
|
272
|
+
selector: nil
|
|
273
|
+
).freeze
|
|
266
274
|
end
|
|
267
275
|
|
|
268
276
|
def apply_post_process_steps(item:, value:, post_process_steps:, base_url:)
|
|
@@ -288,7 +296,13 @@ module Html2rss
|
|
|
288
296
|
end
|
|
289
297
|
|
|
290
298
|
def channel_context(base_url)
|
|
291
|
-
|
|
299
|
+
@channel_contexts ||= {}
|
|
300
|
+
@channel_contexts[base_url] ||= { url: base_url, time_zone: @time_zone }.freeze
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def channel_post_process_context(base_url)
|
|
304
|
+
@channel_pp_contexts ||= {}
|
|
305
|
+
@channel_pp_contexts[base_url] ||= { channel: channel_context(base_url) }.freeze
|
|
292
306
|
end
|
|
293
307
|
|
|
294
308
|
# @return [Hash] enclosure details.
|
data/lib/html2rss/url.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'addressable/uri'
|
|
4
4
|
require 'cgi'
|
|
5
|
+
require 'nokogiri'
|
|
5
6
|
|
|
6
7
|
module Html2rss
|
|
7
8
|
##
|
|
@@ -54,11 +55,10 @@ module Html2rss
|
|
|
54
55
|
# @param raw_url [String] the raw URL string to sanitize
|
|
55
56
|
# @return [Url, nil] the sanitized URL, or nil if no valid URL found
|
|
56
57
|
def self.sanitize(raw_url)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return nil if url.empty?
|
|
58
|
+
match = raw_url.to_s.match(%r{(?:(?:https?|ftp|mailto)://|mailto:)[^\s<>"]+})
|
|
59
|
+
return unless match
|
|
60
60
|
|
|
61
|
-
new(Addressable::URI.parse(
|
|
61
|
+
new(Addressable::URI.parse(match[0].strip).normalize)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
##
|
|
@@ -70,10 +70,9 @@ module Html2rss
|
|
|
70
70
|
def self.from_absolute(url_string)
|
|
71
71
|
return url_string if url_string.is_a?(self)
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
url
|
|
73
|
+
new(Addressable::URI.parse(url_string.to_s.strip).normalize).tap do |url|
|
|
74
|
+
raise ArgumentError, 'URL must be absolute' unless url.absolute?
|
|
75
|
+
end
|
|
77
76
|
rescue Addressable::URI::InvalidURIError
|
|
78
77
|
raise ArgumentError, 'URL must be absolute'
|
|
79
78
|
end
|
|
@@ -92,14 +91,28 @@ module Html2rss
|
|
|
92
91
|
# Url.for_channel('/relative/path')
|
|
93
92
|
# # => raises ArgumentError: "URL must be absolute"
|
|
94
93
|
def self.for_channel(url_string)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
stripped = url_string.strip
|
|
94
|
+
stripped = url_string.to_s.strip
|
|
98
95
|
return nil if stripped.empty?
|
|
99
96
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
from_absolute(stripped).tap { validate_channel_url(_1) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
##
|
|
101
|
+
# Extracts a base URL from HTML metadata tags.
|
|
102
|
+
#
|
|
103
|
+
# @param html [String] raw HTML content
|
|
104
|
+
# @return [Url, nil] the extracted absolute URL, or nil if none is found
|
|
105
|
+
def self.extract_from_html(html)
|
|
106
|
+
doc = Nokogiri::HTML(html)
|
|
107
|
+
tags = { 'link[rel="canonical"]' => 'href', 'meta[property="og:url"]' => 'content',
|
|
108
|
+
'meta[name="twitter:url"]' => 'content', 'base[href]' => 'href' }
|
|
109
|
+
tags.each do |sel, attr|
|
|
110
|
+
val = doc.at_css(sel)&.[](attr).to_s.strip
|
|
111
|
+
return from_absolute(val) unless val.empty?
|
|
112
|
+
rescue ArgumentError
|
|
113
|
+
next
|
|
114
|
+
end
|
|
115
|
+
nil
|
|
103
116
|
end
|
|
104
117
|
|
|
105
118
|
##
|
|
@@ -125,6 +138,7 @@ module Html2rss
|
|
|
125
138
|
# @param uri [Addressable::URI] the underlying Addressable::URI object (internal use only)
|
|
126
139
|
def initialize(uri)
|
|
127
140
|
@uri = uri.freeze
|
|
141
|
+
@path_segments = @uri.path.to_s.split('/').reject(&:empty?).freeze
|
|
128
142
|
freeze
|
|
129
143
|
end
|
|
130
144
|
|
|
@@ -162,7 +176,7 @@ module Html2rss
|
|
|
162
176
|
# Returns the URL path split into non-empty segments.
|
|
163
177
|
#
|
|
164
178
|
# @return [Array<String>] normalized path segments
|
|
165
|
-
|
|
179
|
+
attr_reader :path_segments
|
|
166
180
|
|
|
167
181
|
##
|
|
168
182
|
# Returns a copy of the URL with the provided path.
|
data/lib/html2rss/version.rb
CHANGED
data/lib/html2rss.rb
CHANGED
|
@@ -52,6 +52,8 @@ module Html2rss
|
|
|
52
52
|
FeedPipeline.new(raw_config).to_json_feed
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# rubocop:disable Metrics/ParameterLists
|
|
56
|
+
|
|
55
57
|
##
|
|
56
58
|
# Scrapes the provided URL and returns an RSS object.
|
|
57
59
|
#
|
|
@@ -60,9 +62,15 @@ module Html2rss
|
|
|
60
62
|
# @param items_selector [String, nil] optional selector hint for item extraction
|
|
61
63
|
# @param max_redirects [Integer, nil] optional redirect limit override
|
|
62
64
|
# @param max_requests [Integer, nil] optional request budget override
|
|
65
|
+
# @param local_file_path [String, nil] optional local HTML file path
|
|
63
66
|
# @return [RSS::Rss] generated RSS feed
|
|
64
|
-
def self.auto_source(url,
|
|
65
|
-
|
|
67
|
+
def self.auto_source(url,
|
|
68
|
+
strategy: :auto,
|
|
69
|
+
items_selector: nil,
|
|
70
|
+
max_redirects: nil,
|
|
71
|
+
max_requests: nil,
|
|
72
|
+
local_file_path: nil)
|
|
73
|
+
feed(build_auto_source_config(url:, strategy:, items_selector:, max_redirects:, max_requests:, local_file_path:))
|
|
66
74
|
end
|
|
67
75
|
|
|
68
76
|
##
|
|
@@ -73,11 +81,20 @@ module Html2rss
|
|
|
73
81
|
# @param items_selector [String, nil] optional selector hint for item extraction
|
|
74
82
|
# @param max_redirects [Integer, nil] optional redirect limit override
|
|
75
83
|
# @param max_requests [Integer, nil] optional request budget override
|
|
84
|
+
# @param local_file_path [String, nil] optional local HTML file path
|
|
76
85
|
# @return [Hash] JSONFeed-compliant hash
|
|
77
|
-
def self.auto_json_feed(url,
|
|
78
|
-
|
|
86
|
+
def self.auto_json_feed(url,
|
|
87
|
+
strategy: :auto,
|
|
88
|
+
items_selector: nil,
|
|
89
|
+
max_redirects: nil,
|
|
90
|
+
max_requests: nil,
|
|
91
|
+
local_file_path: nil)
|
|
92
|
+
json_feed(build_auto_source_config(url:, strategy:, items_selector:, max_redirects:, max_requests:,
|
|
93
|
+
local_file_path:))
|
|
79
94
|
end
|
|
80
95
|
|
|
96
|
+
# rubocop:enable Metrics/ParameterLists
|
|
97
|
+
|
|
81
98
|
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
82
99
|
class << self
|
|
83
100
|
##
|
|
@@ -125,12 +142,17 @@ module Html2rss
|
|
|
125
142
|
class << self
|
|
126
143
|
private
|
|
127
144
|
|
|
128
|
-
def build_auto_source_config(url:, strategy:, items_selector:, max_redirects:, max_requests:)
|
|
129
|
-
Config.auto_source_config(
|
|
145
|
+
def build_auto_source_config(url:, strategy:, items_selector:, max_redirects:, max_requests:, local_file_path: nil) # rubocop:disable Metrics/ParameterLists
|
|
146
|
+
config = Config.auto_source_config(
|
|
130
147
|
url:,
|
|
131
148
|
items_selector:,
|
|
132
149
|
request_controls: shortcut_request_controls(strategy:, max_redirects:, max_requests:)
|
|
133
150
|
)
|
|
151
|
+
if local_file_path
|
|
152
|
+
config[:request] ||= {}
|
|
153
|
+
config[:request][:local_file_path] = local_file_path
|
|
154
|
+
end
|
|
155
|
+
config
|
|
134
156
|
end
|
|
135
157
|
|
|
136
158
|
def shortcut_request_controls(strategy:, max_redirects:, max_requests:)
|
|
@@ -179,6 +179,12 @@
|
|
|
179
179
|
"type": "null"
|
|
180
180
|
},
|
|
181
181
|
"exclusiveMinimum": 0
|
|
182
|
+
},
|
|
183
|
+
"fallback_anchorless": {
|
|
184
|
+
"type": "boolean",
|
|
185
|
+
"not": {
|
|
186
|
+
"type": "null"
|
|
187
|
+
}
|
|
182
188
|
}
|
|
183
189
|
},
|
|
184
190
|
"required": []
|
|
@@ -227,7 +233,8 @@
|
|
|
227
233
|
"html": {
|
|
228
234
|
"enabled": true,
|
|
229
235
|
"minimum_selector_frequency": 2,
|
|
230
|
-
"use_top_selectors": 5
|
|
236
|
+
"use_top_selectors": 5,
|
|
237
|
+
"fallback_anchorless": true
|
|
231
238
|
}
|
|
232
239
|
},
|
|
233
240
|
"cleanup": {
|
|
@@ -535,6 +542,10 @@
|
|
|
535
542
|
}
|
|
536
543
|
},
|
|
537
544
|
"required": []
|
|
545
|
+
},
|
|
546
|
+
"local_file_path": {
|
|
547
|
+
"type": "string",
|
|
548
|
+
"minLength": 1
|
|
538
549
|
}
|
|
539
550
|
},
|
|
540
551
|
"required": []
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: html2rss
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.22.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gil Desmarais
|
|
@@ -147,20 +147,6 @@ dependencies:
|
|
|
147
147
|
- - "<"
|
|
148
148
|
- !ruby/object:Gem::Version
|
|
149
149
|
version: '2.0'
|
|
150
|
-
- !ruby/object:Gem::Dependency
|
|
151
|
-
name: parallel
|
|
152
|
-
requirement: !ruby/object:Gem::Requirement
|
|
153
|
-
requirements:
|
|
154
|
-
- - ">="
|
|
155
|
-
- !ruby/object:Gem::Version
|
|
156
|
-
version: '0'
|
|
157
|
-
type: :runtime
|
|
158
|
-
prerelease: false
|
|
159
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
160
|
-
requirements:
|
|
161
|
-
- - ">="
|
|
162
|
-
- !ruby/object:Gem::Version
|
|
163
|
-
version: '0'
|
|
164
150
|
- !ruby/object:Gem::Dependency
|
|
165
151
|
name: puppeteer-ruby
|
|
166
152
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -292,6 +278,7 @@ files:
|
|
|
292
278
|
- lib/html2rss/auto_source/cleanup.rb
|
|
293
279
|
- lib/html2rss/auto_source/scraper.rb
|
|
294
280
|
- lib/html2rss/auto_source/scraper/html.rb
|
|
281
|
+
- lib/html2rss/auto_source/scraper/html/class_clustering.rb
|
|
295
282
|
- lib/html2rss/auto_source/scraper/json_state.rb
|
|
296
283
|
- lib/html2rss/auto_source/scraper/link_heuristics.rb
|
|
297
284
|
- lib/html2rss/auto_source/scraper/microdata.rb
|
|
@@ -324,10 +311,13 @@ files:
|
|
|
324
311
|
- lib/html2rss/html_extractor.rb
|
|
325
312
|
- lib/html2rss/html_extractor/date_extractor.rb
|
|
326
313
|
- lib/html2rss/html_extractor/enclosure_extractor.rb
|
|
314
|
+
- lib/html2rss/html_extractor/heading_extractor.rb
|
|
315
|
+
- lib/html2rss/html_extractor/id_generator.rb
|
|
327
316
|
- lib/html2rss/html_extractor/image_extractor.rb
|
|
328
317
|
- lib/html2rss/html_extractor/list_candidates.rb
|
|
329
318
|
- lib/html2rss/html_extractor/semantic_anchor_candidates.rb
|
|
330
319
|
- lib/html2rss/html_extractor/semantic_containers.rb
|
|
320
|
+
- lib/html2rss/html_extractor/text_extractor.rb
|
|
331
321
|
- lib/html2rss/html_navigator.rb
|
|
332
322
|
- lib/html2rss/json_feed_builder.rb
|
|
333
323
|
- lib/html2rss/json_feed_builder/item.rb
|
|
@@ -346,6 +336,7 @@ files:
|
|
|
346
336
|
- lib/html2rss/request_service/budget.rb
|
|
347
337
|
- lib/html2rss/request_service/context.rb
|
|
348
338
|
- lib/html2rss/request_service/faraday_strategy.rb
|
|
339
|
+
- lib/html2rss/request_service/local_file_strategy.rb
|
|
349
340
|
- lib/html2rss/request_service/policy.rb
|
|
350
341
|
- lib/html2rss/request_service/puppet_commander.rb
|
|
351
342
|
- lib/html2rss/request_service/response.rb
|
|
@@ -390,7 +381,7 @@ licenses:
|
|
|
390
381
|
- MIT
|
|
391
382
|
metadata:
|
|
392
383
|
allowed_push_host: https://rubygems.org
|
|
393
|
-
changelog_uri: https://github.com/html2rss/html2rss/releases/tag/v0.
|
|
384
|
+
changelog_uri: https://github.com/html2rss/html2rss/releases/tag/v0.22.0
|
|
394
385
|
rubygems_mfa_required: 'true'
|
|
395
386
|
rdoc_options: []
|
|
396
387
|
require_paths:
|
|
@@ -399,7 +390,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
399
390
|
requirements:
|
|
400
391
|
- - ">="
|
|
401
392
|
- !ruby/object:Gem::Version
|
|
402
|
-
version: '3.
|
|
393
|
+
version: '3.3'
|
|
403
394
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
404
395
|
requirements:
|
|
405
396
|
- - ">="
|