html2rss 0.24.0 → 0.26.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/README.md +76 -2
- data/html2rss.gemspec +4 -1
- data/lib/html2rss/auto_source/cleanup.rb +62 -36
- data/lib/html2rss/auto_source/scraper/html.rb +68 -145
- data/lib/html2rss/auto_source/scraper/json_state/article_normalizer.rb +116 -0
- data/lib/html2rss/auto_source/scraper/json_state/candidate_detector.rb +63 -0
- data/lib/html2rss/auto_source/scraper/json_state/document_scanner.rb +179 -0
- data/lib/html2rss/auto_source/scraper/json_state/value_finder.rb +55 -0
- data/lib/html2rss/auto_source/scraper/json_state.rb +31 -401
- data/lib/html2rss/auto_source/scraper/schema/item_list.rb +2 -14
- data/lib/html2rss/auto_source/scraper/semantic_html/entry_deduplicator.rb +35 -30
- data/lib/html2rss/auto_source/scraper/semantic_html.rb +40 -120
- data/lib/html2rss/auto_source/scraper/sitemap/parser.rb +161 -0
- data/lib/html2rss/auto_source/scraper/sitemap.rb +10 -7
- data/lib/html2rss/auto_source/scraper/wordpress_api.rb +6 -2
- data/lib/html2rss/auto_source/scraper/xhr_articles.rb +69 -0
- data/lib/html2rss/auto_source/scraper.rb +109 -53
- data/lib/html2rss/auto_source/segment.rb +29 -0
- data/lib/html2rss/auto_source/segmenter/cluster.rb +163 -0
- data/lib/html2rss/auto_source/segmenter/list.rb +103 -0
- data/lib/html2rss/auto_source/segmenter/primary_link.rb +80 -0
- data/lib/html2rss/auto_source/segmenter/semantic.rb +104 -0
- data/lib/html2rss/auto_source/segmenter.rb +95 -0
- data/lib/html2rss/auto_source.rb +79 -21
- data/lib/html2rss/capture.rb +315 -0
- data/lib/html2rss/cli.rb +72 -18
- data/lib/html2rss/config/auto_source_contract.rb +5 -1
- data/lib/html2rss/config/request_controls.rb +33 -0
- data/lib/html2rss/config/schema.rb +67 -26
- data/lib/html2rss/config/selectors_validator.rb +60 -21
- data/lib/html2rss/config/validator.rb +33 -35
- data/lib/html2rss/config.rb +6 -2
- data/lib/html2rss/error.rb +15 -3
- data/lib/html2rss/feed_builder/rss.rb +17 -5
- data/lib/html2rss/feed_pipeline/auto_fallback.rb +16 -6
- data/lib/html2rss/feed_pipeline/runtime_policy.rb +3 -43
- data/lib/html2rss/feed_pipeline.rb +11 -0
- data/lib/html2rss/feed_result.rb +1 -1
- data/lib/html2rss/hash_util.rb +17 -0
- data/lib/html2rss/html/article_extractor/category_extractor.rb +10 -36
- data/lib/html2rss/html/article_extractor/date_extractor.rb +2 -7
- data/lib/html2rss/html/article_extractor/enclosure_extractor.rb +5 -50
- data/lib/html2rss/html/article_extractor/image_extractor.rb +5 -16
- data/lib/html2rss/html/article_rules/category.rb +55 -0
- data/lib/html2rss/html/article_rules/date.rb +25 -0
- data/lib/html2rss/html/article_rules/enclosure.rb +72 -0
- data/lib/html2rss/html/article_rules/image.rb +109 -0
- data/lib/html2rss/html/article_rules.rb +10 -0
- data/lib/html2rss/html/rendering/audio_renderer.rb +2 -12
- data/lib/html2rss/html/rendering/escaped_attributes.rb +27 -0
- data/lib/html2rss/html/rendering/image_renderer.rb +2 -12
- data/lib/html2rss/html/rendering/pdf_renderer.rb +2 -8
- data/lib/html2rss/html/rendering/video_renderer.rb +2 -12
- data/lib/html2rss/html/sst_article_extractor.rb +308 -0
- data/lib/html2rss/link_destination/destination_facts.rb +40 -0
- data/lib/html2rss/link_destination/noise_policy.rb +68 -0
- data/lib/html2rss/link_destination/path_classifier.rb +208 -0
- data/lib/html2rss/link_destination/text_classifier.rb +64 -0
- data/lib/html2rss/link_destination.rb +8 -0
- data/lib/html2rss/mcp/server.rb +577 -0
- data/lib/html2rss/mcp.rb +21 -0
- data/lib/html2rss/request_service/blocked_surface.rb +24 -1
- data/lib/html2rss/request_service/botasaurus_contract.rb +89 -9
- data/lib/html2rss/request_service/botasaurus_strategy.rb +4 -2
- data/lib/html2rss/request_service/budget.rb +7 -35
- data/lib/html2rss/request_service/context.rb +0 -6
- data/lib/html2rss/request_service/faraday_strategy.rb +87 -3
- data/lib/html2rss/request_service/network_guard.rb +5 -3
- data/lib/html2rss/request_service/policy.rb +1 -1
- data/lib/html2rss/request_service/response.rb +15 -1
- data/lib/html2rss/request_service/strategy.rb +1 -2
- data/lib/html2rss/request_service.rb +4 -9
- data/lib/html2rss/scoring/cluster_scorer.rb +87 -0
- data/lib/html2rss/scoring/container_assessor.rb +77 -0
- data/lib/html2rss/scoring/engine.rb +145 -0
- data/lib/html2rss/scoring/link_resolver.rb +84 -0
- data/lib/html2rss/scoring/observation.rb +53 -0
- data/lib/html2rss/scoring/ranked_segment.rb +33 -0
- data/lib/html2rss/scoring/score.rb +11 -0
- data/lib/html2rss/scoring.rb +8 -0
- data/lib/html2rss/selectors/extractors/attribute.rb +15 -0
- data/lib/html2rss/selectors/extractors/href.rb +12 -0
- data/lib/html2rss/selectors/extractors/html.rb +12 -0
- data/lib/html2rss/selectors/extractors/static.rb +14 -0
- data/lib/html2rss/selectors/extractors/text.rb +11 -0
- data/lib/html2rss/selectors/post_processors/gsub.rb +18 -0
- data/lib/html2rss/selectors/post_processors/html_to_markdown.rb +11 -0
- data/lib/html2rss/selectors/post_processors/markdown_to_html.rb +12 -0
- data/lib/html2rss/selectors/post_processors/parse_time.rb +11 -0
- data/lib/html2rss/selectors/post_processors/parse_uri.rb +11 -0
- data/lib/html2rss/selectors/post_processors/sanitize_html.rb +23 -1
- data/lib/html2rss/selectors/post_processors/substring.rb +21 -0
- data/lib/html2rss/selectors/post_processors/template.rb +20 -0
- data/lib/html2rss/selectors/schema_doc.rb +99 -0
- data/lib/html2rss/selectors.rb +0 -20
- data/lib/html2rss/sst/attrs.rb +91 -0
- data/lib/html2rss/sst/document.rb +23 -0
- data/lib/html2rss/sst/index.rb +112 -0
- data/lib/html2rss/sst/node.rb +147 -0
- data/lib/html2rss/sst/normalizer.rb +171 -0
- data/lib/html2rss/sst/tags.rb +26 -0
- data/lib/html2rss/sst/text.rb +81 -0
- data/lib/html2rss/sst.rb +8 -0
- data/lib/html2rss/url.rb +11 -7
- data/lib/html2rss/version.rb +1 -1
- data/lib/html2rss.rb +57 -26
- data/schema/html2rss-config.schema.json +428 -85
- metadata +93 -27
- data/lib/html2rss/auto_source/discovery/dom_clustering/group_scorer.rb +0 -80
- data/lib/html2rss/auto_source/discovery/dom_clustering/overlap_resolver.rb +0 -86
- data/lib/html2rss/auto_source/discovery/dom_clustering.rb +0 -119
- data/lib/html2rss/auto_source/discovery/list_candidates.rb +0 -94
- data/lib/html2rss/auto_source/discovery/semantic_anchor_candidates.rb +0 -219
- data/lib/html2rss/auto_source/discovery/semantic_containers.rb +0 -71
- data/lib/html2rss/auto_source/discovery/sitemap.rb +0 -159
- data/lib/html2rss/auto_source/discovery.rb +0 -14
- data/lib/html2rss/auto_source/link_heuristics/anchor_signals.rb +0 -28
- data/lib/html2rss/auto_source/link_heuristics/container_assessor.rb +0 -106
- data/lib/html2rss/auto_source/link_heuristics/container_signals.rb +0 -80
- data/lib/html2rss/auto_source/link_heuristics/destination_facts.rb +0 -42
- data/lib/html2rss/auto_source/link_heuristics/href_extractor.rb +0 -38
- data/lib/html2rss/auto_source/link_heuristics/path_classifier.rb +0 -221
- data/lib/html2rss/auto_source/link_heuristics/text_classifier.rb +0 -66
- data/lib/html2rss/auto_source/link_heuristics.rb +0 -139
- data/lib/html2rss/request_service/browserless_strategy.rb +0 -132
- data/lib/html2rss/request_service/puppet_commander/navigation_guards.rb +0 -148
- data/lib/html2rss/request_service/puppet_commander/preload_runner.rb +0 -86
- data/lib/html2rss/request_service/puppet_commander.rb +0 -95
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.26.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gil Desmarais
|
|
@@ -113,6 +113,20 @@ dependencies:
|
|
|
113
113
|
- - ">="
|
|
114
114
|
- !ruby/object:Gem::Version
|
|
115
115
|
version: '0'
|
|
116
|
+
- !ruby/object:Gem::Dependency
|
|
117
|
+
name: mcp
|
|
118
|
+
requirement: !ruby/object:Gem::Requirement
|
|
119
|
+
requirements:
|
|
120
|
+
- - "~>"
|
|
121
|
+
- !ruby/object:Gem::Version
|
|
122
|
+
version: '1.0'
|
|
123
|
+
type: :runtime
|
|
124
|
+
prerelease: false
|
|
125
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
126
|
+
requirements:
|
|
127
|
+
- - "~>"
|
|
128
|
+
- !ruby/object:Gem::Version
|
|
129
|
+
version: '1.0'
|
|
116
130
|
- !ruby/object:Gem::Dependency
|
|
117
131
|
name: mime-types
|
|
118
132
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -148,19 +162,33 @@ dependencies:
|
|
|
148
162
|
- !ruby/object:Gem::Version
|
|
149
163
|
version: '2.0'
|
|
150
164
|
- !ruby/object:Gem::Dependency
|
|
151
|
-
name:
|
|
165
|
+
name: rack
|
|
152
166
|
requirement: !ruby/object:Gem::Requirement
|
|
153
167
|
requirements:
|
|
154
|
-
- - "
|
|
168
|
+
- - "~>"
|
|
155
169
|
- !ruby/object:Gem::Version
|
|
156
|
-
version: '0'
|
|
170
|
+
version: '3.0'
|
|
157
171
|
type: :runtime
|
|
158
172
|
prerelease: false
|
|
159
173
|
version_requirements: !ruby/object:Gem::Requirement
|
|
160
174
|
requirements:
|
|
161
|
-
- - "
|
|
175
|
+
- - "~>"
|
|
162
176
|
- !ruby/object:Gem::Version
|
|
163
|
-
version: '0'
|
|
177
|
+
version: '3.0'
|
|
178
|
+
- !ruby/object:Gem::Dependency
|
|
179
|
+
name: rackup
|
|
180
|
+
requirement: !ruby/object:Gem::Requirement
|
|
181
|
+
requirements:
|
|
182
|
+
- - "~>"
|
|
183
|
+
- !ruby/object:Gem::Version
|
|
184
|
+
version: '2.0'
|
|
185
|
+
type: :runtime
|
|
186
|
+
prerelease: false
|
|
187
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
188
|
+
requirements:
|
|
189
|
+
- - "~>"
|
|
190
|
+
- !ruby/object:Gem::Version
|
|
191
|
+
version: '2.0'
|
|
164
192
|
- !ruby/object:Gem::Dependency
|
|
165
193
|
name: regexp_parser
|
|
166
194
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -245,6 +273,20 @@ dependencies:
|
|
|
245
273
|
- - ">="
|
|
246
274
|
- !ruby/object:Gem::Version
|
|
247
275
|
version: '0'
|
|
276
|
+
- !ruby/object:Gem::Dependency
|
|
277
|
+
name: webrick
|
|
278
|
+
requirement: !ruby/object:Gem::Requirement
|
|
279
|
+
requirements:
|
|
280
|
+
- - "~>"
|
|
281
|
+
- !ruby/object:Gem::Version
|
|
282
|
+
version: '1.9'
|
|
283
|
+
type: :runtime
|
|
284
|
+
prerelease: false
|
|
285
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
286
|
+
requirements:
|
|
287
|
+
- - "~>"
|
|
288
|
+
- !ruby/object:Gem::Version
|
|
289
|
+
version: '1.9'
|
|
248
290
|
- !ruby/object:Gem::Dependency
|
|
249
291
|
name: zeitwerk
|
|
250
292
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -278,25 +320,13 @@ files:
|
|
|
278
320
|
- lib/html2rss/article/enclosure.rb
|
|
279
321
|
- lib/html2rss/auto_source.rb
|
|
280
322
|
- lib/html2rss/auto_source/cleanup.rb
|
|
281
|
-
- lib/html2rss/auto_source/discovery.rb
|
|
282
|
-
- lib/html2rss/auto_source/discovery/dom_clustering.rb
|
|
283
|
-
- lib/html2rss/auto_source/discovery/dom_clustering/group_scorer.rb
|
|
284
|
-
- lib/html2rss/auto_source/discovery/dom_clustering/overlap_resolver.rb
|
|
285
|
-
- lib/html2rss/auto_source/discovery/list_candidates.rb
|
|
286
|
-
- lib/html2rss/auto_source/discovery/semantic_anchor_candidates.rb
|
|
287
|
-
- lib/html2rss/auto_source/discovery/semantic_containers.rb
|
|
288
|
-
- lib/html2rss/auto_source/discovery/sitemap.rb
|
|
289
|
-
- lib/html2rss/auto_source/link_heuristics.rb
|
|
290
|
-
- lib/html2rss/auto_source/link_heuristics/anchor_signals.rb
|
|
291
|
-
- lib/html2rss/auto_source/link_heuristics/container_assessor.rb
|
|
292
|
-
- lib/html2rss/auto_source/link_heuristics/container_signals.rb
|
|
293
|
-
- lib/html2rss/auto_source/link_heuristics/destination_facts.rb
|
|
294
|
-
- lib/html2rss/auto_source/link_heuristics/href_extractor.rb
|
|
295
|
-
- lib/html2rss/auto_source/link_heuristics/path_classifier.rb
|
|
296
|
-
- lib/html2rss/auto_source/link_heuristics/text_classifier.rb
|
|
297
323
|
- lib/html2rss/auto_source/scraper.rb
|
|
298
324
|
- lib/html2rss/auto_source/scraper/html.rb
|
|
299
325
|
- lib/html2rss/auto_source/scraper/json_state.rb
|
|
326
|
+
- lib/html2rss/auto_source/scraper/json_state/article_normalizer.rb
|
|
327
|
+
- lib/html2rss/auto_source/scraper/json_state/candidate_detector.rb
|
|
328
|
+
- lib/html2rss/auto_source/scraper/json_state/document_scanner.rb
|
|
329
|
+
- lib/html2rss/auto_source/scraper/json_state/value_finder.rb
|
|
300
330
|
- lib/html2rss/auto_source/scraper/meta_oembed.rb
|
|
301
331
|
- lib/html2rss/auto_source/scraper/microdata.rb
|
|
302
332
|
- lib/html2rss/auto_source/scraper/microformats2.rb
|
|
@@ -307,10 +337,19 @@ files:
|
|
|
307
337
|
- lib/html2rss/auto_source/scraper/semantic_html.rb
|
|
308
338
|
- lib/html2rss/auto_source/scraper/semantic_html/entry_deduplicator.rb
|
|
309
339
|
- lib/html2rss/auto_source/scraper/sitemap.rb
|
|
340
|
+
- lib/html2rss/auto_source/scraper/sitemap/parser.rb
|
|
310
341
|
- lib/html2rss/auto_source/scraper/wordpress_api.rb
|
|
311
342
|
- lib/html2rss/auto_source/scraper/wordpress_api/page_scope.rb
|
|
312
343
|
- lib/html2rss/auto_source/scraper/wordpress_api/page_scope/date_archive_range.rb
|
|
313
344
|
- lib/html2rss/auto_source/scraper/wordpress_api/posts_endpoint.rb
|
|
345
|
+
- lib/html2rss/auto_source/scraper/xhr_articles.rb
|
|
346
|
+
- lib/html2rss/auto_source/segment.rb
|
|
347
|
+
- lib/html2rss/auto_source/segmenter.rb
|
|
348
|
+
- lib/html2rss/auto_source/segmenter/cluster.rb
|
|
349
|
+
- lib/html2rss/auto_source/segmenter/list.rb
|
|
350
|
+
- lib/html2rss/auto_source/segmenter/primary_link.rb
|
|
351
|
+
- lib/html2rss/auto_source/segmenter/semantic.rb
|
|
352
|
+
- lib/html2rss/capture.rb
|
|
314
353
|
- lib/html2rss/channel.rb
|
|
315
354
|
- lib/html2rss/cli.rb
|
|
316
355
|
- lib/html2rss/config.rb
|
|
@@ -344,29 +383,39 @@ files:
|
|
|
344
383
|
- lib/html2rss/html/article_extractor/heading_extractor.rb
|
|
345
384
|
- lib/html2rss/html/article_extractor/id_generator.rb
|
|
346
385
|
- lib/html2rss/html/article_extractor/image_extractor.rb
|
|
386
|
+
- lib/html2rss/html/article_rules.rb
|
|
387
|
+
- lib/html2rss/html/article_rules/category.rb
|
|
388
|
+
- lib/html2rss/html/article_rules/date.rb
|
|
389
|
+
- lib/html2rss/html/article_rules/enclosure.rb
|
|
390
|
+
- lib/html2rss/html/article_rules/image.rb
|
|
347
391
|
- lib/html2rss/html/navigator.rb
|
|
348
392
|
- lib/html2rss/html/navigator/text_extractor.rb
|
|
349
393
|
- lib/html2rss/html/rendering.rb
|
|
350
394
|
- lib/html2rss/html/rendering/audio_renderer.rb
|
|
351
395
|
- lib/html2rss/html/rendering/description_builder.rb
|
|
396
|
+
- lib/html2rss/html/rendering/escaped_attributes.rb
|
|
352
397
|
- lib/html2rss/html/rendering/image_renderer.rb
|
|
353
398
|
- lib/html2rss/html/rendering/media_renderer.rb
|
|
354
399
|
- lib/html2rss/html/rendering/pdf_renderer.rb
|
|
355
400
|
- lib/html2rss/html/rendering/video_renderer.rb
|
|
401
|
+
- lib/html2rss/html/sst_article_extractor.rb
|
|
402
|
+
- lib/html2rss/link_destination.rb
|
|
403
|
+
- lib/html2rss/link_destination/destination_facts.rb
|
|
404
|
+
- lib/html2rss/link_destination/noise_policy.rb
|
|
405
|
+
- lib/html2rss/link_destination/path_classifier.rb
|
|
406
|
+
- lib/html2rss/link_destination/text_classifier.rb
|
|
407
|
+
- lib/html2rss/mcp.rb
|
|
408
|
+
- lib/html2rss/mcp/server.rb
|
|
356
409
|
- lib/html2rss/request_service.rb
|
|
357
410
|
- lib/html2rss/request_service/blocked_surface.rb
|
|
358
411
|
- lib/html2rss/request_service/botasaurus_contract.rb
|
|
359
412
|
- lib/html2rss/request_service/botasaurus_strategy.rb
|
|
360
|
-
- lib/html2rss/request_service/browserless_strategy.rb
|
|
361
413
|
- lib/html2rss/request_service/budget.rb
|
|
362
414
|
- lib/html2rss/request_service/context.rb
|
|
363
415
|
- lib/html2rss/request_service/faraday_strategy.rb
|
|
364
416
|
- lib/html2rss/request_service/local_file_strategy.rb
|
|
365
417
|
- lib/html2rss/request_service/network_guard.rb
|
|
366
418
|
- lib/html2rss/request_service/policy.rb
|
|
367
|
-
- lib/html2rss/request_service/puppet_commander.rb
|
|
368
|
-
- lib/html2rss/request_service/puppet_commander/navigation_guards.rb
|
|
369
|
-
- lib/html2rss/request_service/puppet_commander/preload_runner.rb
|
|
370
419
|
- lib/html2rss/request_service/response.rb
|
|
371
420
|
- lib/html2rss/request_service/response_guard.rb
|
|
372
421
|
- lib/html2rss/request_service/strategy.rb
|
|
@@ -378,6 +427,14 @@ files:
|
|
|
378
427
|
- lib/html2rss/request_session/pager/offset.rb
|
|
379
428
|
- lib/html2rss/request_session/pager/rel_next.rb
|
|
380
429
|
- lib/html2rss/request_session/pager/url_template.rb
|
|
430
|
+
- lib/html2rss/scoring.rb
|
|
431
|
+
- lib/html2rss/scoring/cluster_scorer.rb
|
|
432
|
+
- lib/html2rss/scoring/container_assessor.rb
|
|
433
|
+
- lib/html2rss/scoring/engine.rb
|
|
434
|
+
- lib/html2rss/scoring/link_resolver.rb
|
|
435
|
+
- lib/html2rss/scoring/observation.rb
|
|
436
|
+
- lib/html2rss/scoring/ranked_segment.rb
|
|
437
|
+
- lib/html2rss/scoring/score.rb
|
|
381
438
|
- lib/html2rss/selectors.rb
|
|
382
439
|
- lib/html2rss/selectors/extractors.rb
|
|
383
440
|
- lib/html2rss/selectors/extractors/attribute.rb
|
|
@@ -399,6 +456,15 @@ files:
|
|
|
399
456
|
- lib/html2rss/selectors/post_processors/sanitize_html.rb
|
|
400
457
|
- lib/html2rss/selectors/post_processors/substring.rb
|
|
401
458
|
- lib/html2rss/selectors/post_processors/template.rb
|
|
459
|
+
- lib/html2rss/selectors/schema_doc.rb
|
|
460
|
+
- lib/html2rss/sst.rb
|
|
461
|
+
- lib/html2rss/sst/attrs.rb
|
|
462
|
+
- lib/html2rss/sst/document.rb
|
|
463
|
+
- lib/html2rss/sst/index.rb
|
|
464
|
+
- lib/html2rss/sst/node.rb
|
|
465
|
+
- lib/html2rss/sst/normalizer.rb
|
|
466
|
+
- lib/html2rss/sst/tags.rb
|
|
467
|
+
- lib/html2rss/sst/text.rb
|
|
402
468
|
- lib/html2rss/status.rb
|
|
403
469
|
- lib/html2rss/url.rb
|
|
404
470
|
- lib/html2rss/version.rb
|
|
@@ -409,7 +475,7 @@ licenses:
|
|
|
409
475
|
- MIT
|
|
410
476
|
metadata:
|
|
411
477
|
allowed_push_host: https://rubygems.org
|
|
412
|
-
changelog_uri: https://github.com/html2rss/html2rss/releases/tag/v0.
|
|
478
|
+
changelog_uri: https://github.com/html2rss/html2rss/releases/tag/v0.26.0
|
|
413
479
|
rubygems_mfa_required: 'true'
|
|
414
480
|
rdoc_options: []
|
|
415
481
|
require_paths:
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Html2rss
|
|
4
|
-
class AutoSource
|
|
5
|
-
module Discovery
|
|
6
|
-
class DomClustering
|
|
7
|
-
##
|
|
8
|
-
# Scores candidate DOM groups using heading, time, date, and word-count signals.
|
|
9
|
-
class GroupScorer
|
|
10
|
-
def initialize
|
|
11
|
-
@text_words = {}.compare_by_identity
|
|
12
|
-
@has_date = {}.compare_by_identity
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# @param groups [Hash{String => Array<Nokogiri::XML::Node>}] candidate DOM groups
|
|
16
|
-
# @return [Array<Nokogiri::XML::Node>] nodes from the highest-scoring group
|
|
17
|
-
def select_best_group(groups)
|
|
18
|
-
best_nodes = []
|
|
19
|
-
best_score = -1
|
|
20
|
-
|
|
21
|
-
groups.each_value do |nodes|
|
|
22
|
-
score = score_group(nodes)
|
|
23
|
-
next if score.negative?
|
|
24
|
-
|
|
25
|
-
(best_nodes = nodes) && (best_score = score) if score > best_score
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
best_nodes
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# @param nodes [Array<Nokogiri::XML::Node>]
|
|
32
|
-
# @return [Float] average visible word count across nodes
|
|
33
|
-
def avg_words(nodes)
|
|
34
|
-
nodes.sum { |n| text_words(n) } / nodes.size.to_f
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
private
|
|
38
|
-
|
|
39
|
-
def score_group(nodes)
|
|
40
|
-
avg_w = avg_words(nodes)
|
|
41
|
-
return -1 if avg_w < 5
|
|
42
|
-
|
|
43
|
-
score = nodes.size + (avg_w / 5.0)
|
|
44
|
-
score += 20 if nodes_heading?(nodes)
|
|
45
|
-
score += 20 if nodes_time?(nodes)
|
|
46
|
-
score += 40 if nodes_date?(nodes)
|
|
47
|
-
score
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def nodes_heading?(nodes)
|
|
51
|
-
nodes.any? do |n|
|
|
52
|
-
n.at_css(Html2rss::Html::Navigator::HEADING_TAGS.join(',')) ||
|
|
53
|
-
n.at_css('.font-bold, .font-semibold')
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def nodes_time?(nodes)
|
|
58
|
-
nodes.any? { |n| n.at_css('time, [datetime]') }
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def nodes_date?(nodes)
|
|
62
|
-
nodes.any? { |n| date?(n) }
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def text_words(node)
|
|
66
|
-
@text_words[node] ||= Html2rss::Html::Navigator.extract_visible_text(node).to_s.scan(/\p{Alnum}+/).size
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def date?(node)
|
|
70
|
-
@has_date[node] ||= begin
|
|
71
|
-
text = Html2rss::Html::Navigator.extract_visible_text(node).to_s
|
|
72
|
-
text.match?(%r{\b\d{4}[-/]\d{2}[-/]\d{2}\b}) ||
|
|
73
|
-
text.match?(/\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\b/i)
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Html2rss
|
|
4
|
-
class AutoSource
|
|
5
|
-
module Discovery
|
|
6
|
-
class DomClustering
|
|
7
|
-
##
|
|
8
|
-
# Filters layout wrapper groups and resolves 1-to-1 nested card wrappers.
|
|
9
|
-
class OverlapResolver
|
|
10
|
-
# @param layout_tags [Set<String>] tags treated as layout containers
|
|
11
|
-
# @param word_counter [#avg_words] averages visible word counts for nodes
|
|
12
|
-
def initialize(layout_tags:, word_counter:)
|
|
13
|
-
@layout_tags = layout_tags
|
|
14
|
-
@word_counter = word_counter
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# Discard group A if any node of A contains > 1 node of another group B
|
|
18
|
-
#
|
|
19
|
-
# @param groups [Hash{String => Array<Nokogiri::XML::Node>}] candidate groups
|
|
20
|
-
# @return [Hash{String => Array<Nokogiri::XML::Node>}] groups that are not layout wrappers
|
|
21
|
-
def filter_containers(groups)
|
|
22
|
-
groups.reject do |cls_a, nodes_a|
|
|
23
|
-
groups.any? { |cls_b, nodes_b| cls_a != cls_b && container_of?(nodes_a, nodes_b) }
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# If group A contains group B with the same size, keep the real content card.
|
|
28
|
-
#
|
|
29
|
-
# @param groups [Hash{String => Array<Nokogiri::XML::Node>}] candidate groups
|
|
30
|
-
# @return [Hash{String => Array<Nokogiri::XML::Node>}] groups after 1-to-1 resolution
|
|
31
|
-
def filter_1_to_1_overlap(groups)
|
|
32
|
-
discarded = Set.new
|
|
33
|
-
groups.each_key do |cls_a|
|
|
34
|
-
groups.each_key do |cls_b|
|
|
35
|
-
next if cls_a == cls_b || discarded.include?(cls_a) || discarded.include?(cls_b)
|
|
36
|
-
|
|
37
|
-
resolve_1_to_1_overlap(cls_a, cls_b, groups, discarded)
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
groups.except(*discarded)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
private
|
|
45
|
-
|
|
46
|
-
attr_reader :layout_tags, :word_counter
|
|
47
|
-
|
|
48
|
-
# rubocop:disable Metrics/MethodLength
|
|
49
|
-
def container_of?(nodes_a, nodes_b)
|
|
50
|
-
return false unless layout_tags.include?(nodes_b.first.name)
|
|
51
|
-
|
|
52
|
-
nodes_a.any? do |node_a|
|
|
53
|
-
count = 0
|
|
54
|
-
nodes_b.each do |node_b|
|
|
55
|
-
next if node_a == node_b
|
|
56
|
-
|
|
57
|
-
if Html2rss::Html::Navigator.descendant_of?(node_b, node_a)
|
|
58
|
-
count += 1
|
|
59
|
-
break if count > 1
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
count > 1
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
# rubocop:enable Metrics/MethodLength
|
|
66
|
-
|
|
67
|
-
def resolve_1_to_1_overlap(cls_a, cls_b, groups, discarded)
|
|
68
|
-
nodes_a = groups[cls_a]
|
|
69
|
-
nodes_b = groups[cls_b]
|
|
70
|
-
return if nodes_a.size != nodes_b.size
|
|
71
|
-
|
|
72
|
-
nested = nodes_a.zip(nodes_b).all? { |a, b| a != b && Html2rss::Html::Navigator.descendant_of?(b, a) }
|
|
73
|
-
return unless nested
|
|
74
|
-
|
|
75
|
-
discarded << (keep_descendant?(nodes_a, nodes_b) ? cls_a : cls_b)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def keep_descendant?(nodes_a, nodes_b)
|
|
79
|
-
word_counter.avg_words(nodes_b) >= 0.8 * word_counter.avg_words(nodes_a) &&
|
|
80
|
-
layout_tags.include?(nodes_b.first.name)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Html2rss
|
|
4
|
-
class AutoSource
|
|
5
|
-
module Discovery
|
|
6
|
-
##
|
|
7
|
-
# Discovers repeated content card nodes in anchorless or classless DOM trees
|
|
8
|
-
# by evaluating class groups first and falling back to 1-level tag structure signatures.
|
|
9
|
-
class DomClustering
|
|
10
|
-
# Node tags considered layout containers
|
|
11
|
-
LAYOUT_TAG_NAMES = Set['div', 'section', 'article', 'li', 'ul', 'ol'].freeze
|
|
12
|
-
# HTML/layout tags excluded from candidate nodes (owned by Html::Navigator).
|
|
13
|
-
EXCLUDED_TAGS = Html2rss::Html::Navigator::CLUSTER_EXCLUDED_TAGS
|
|
14
|
-
|
|
15
|
-
class << self
|
|
16
|
-
##
|
|
17
|
-
# Clusters elements in parsed_body and returns the best set of content card nodes.
|
|
18
|
-
#
|
|
19
|
-
# @param parsed_body [Nokogiri::HTML::Document] parsed HTML document
|
|
20
|
-
# @param minimum_selector_frequency [Integer] minimum frequency for candidate groups
|
|
21
|
-
# @return [Array<Nokogiri::XML::Node>] candidate nodes of the top-scoring cluster
|
|
22
|
-
def call(parsed_body, minimum_selector_frequency:)
|
|
23
|
-
new(parsed_body, minimum_frequency: minimum_selector_frequency).call
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# @param parsed_body [Nokogiri::HTML::Document]
|
|
28
|
-
# @param minimum_frequency [Integer]
|
|
29
|
-
def initialize(parsed_body, minimum_frequency:)
|
|
30
|
-
@parsed_body = parsed_body
|
|
31
|
-
@minimum_frequency = minimum_frequency
|
|
32
|
-
@cache = {}.compare_by_identity
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# @return [Array<Nokogiri::XML::Node>]
|
|
36
|
-
def call
|
|
37
|
-
class_result = cluster_by_class
|
|
38
|
-
return class_result unless class_result.empty?
|
|
39
|
-
|
|
40
|
-
cluster_by_structure
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
attr_reader :parsed_body, :minimum_frequency, :cache
|
|
46
|
-
|
|
47
|
-
def cluster_by_class
|
|
48
|
-
class_groups = collect_class_groups
|
|
49
|
-
return [] if class_groups.empty?
|
|
50
|
-
|
|
51
|
-
score_groups(class_groups)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def cluster_by_structure
|
|
55
|
-
structure_groups = collect_structure_groups
|
|
56
|
-
return [] if structure_groups.empty?
|
|
57
|
-
|
|
58
|
-
score_groups(structure_groups)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def score_groups(candidate_groups)
|
|
62
|
-
scorer = GroupScorer.new
|
|
63
|
-
resolver = OverlapResolver.new(layout_tags: LAYOUT_TAG_NAMES, word_counter: scorer)
|
|
64
|
-
|
|
65
|
-
non_containers = resolver.filter_containers(candidate_groups)
|
|
66
|
-
final_groups = resolver.filter_1_to_1_overlap(non_containers)
|
|
67
|
-
|
|
68
|
-
scorer.select_best_group(final_groups)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def collect_class_groups
|
|
72
|
-
groups = Hash.new { |h, k| h[k] = [] }
|
|
73
|
-
parsed_body.css('[class]').each { |node| add_class_node(node, groups) }
|
|
74
|
-
groups.select { |_, nodes| nodes.size >= minimum_frequency }
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def add_class_node(node, groups)
|
|
78
|
-
return if EXCLUDED_TAGS.include?(node.name)
|
|
79
|
-
return if Html2rss::Html::Navigator.ignored_container_path?(node, cache)
|
|
80
|
-
|
|
81
|
-
cls = normalize_class(node['class'])
|
|
82
|
-
groups[cls] << node unless cls.empty?
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def collect_structure_groups
|
|
86
|
-
groups = Hash.new { |h, k| h[k] = [] }
|
|
87
|
-
parsed_body.xpath('//*').each { |node| add_structure_node(node, groups) }
|
|
88
|
-
groups.select { |_, nodes| nodes.size >= minimum_frequency }
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def add_structure_node(node, groups)
|
|
92
|
-
return if EXCLUDED_TAGS.include?(node.name)
|
|
93
|
-
return if Html2rss::Html::Navigator.ignored_container_path?(node, cache)
|
|
94
|
-
|
|
95
|
-
sig = structure_signature(node)
|
|
96
|
-
groups[sig] << node unless sig.empty?
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def normalize_class(class_attr)
|
|
100
|
-
class_str = class_attr.to_s.strip
|
|
101
|
-
return '' if class_str.empty?
|
|
102
|
-
|
|
103
|
-
if class_str.include?(' ')
|
|
104
|
-
class_str.split(/\s+/).sort.join(' ')
|
|
105
|
-
else
|
|
106
|
-
class_str
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def structure_signature(node)
|
|
111
|
-
child_elements = node.element_children
|
|
112
|
-
return '' if child_elements.empty?
|
|
113
|
-
|
|
114
|
-
child_elements.map(&:name).join('>')
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
end
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Html2rss
|
|
4
|
-
class AutoSource
|
|
5
|
-
module Discovery
|
|
6
|
-
##
|
|
7
|
-
# Builds repeated-list article container candidates from generic HTML.
|
|
8
|
-
class ListCandidates
|
|
9
|
-
##
|
|
10
|
-
# Simplify an XPath selector by removing index notation.
|
|
11
|
-
#
|
|
12
|
-
# @param xpath [String] original XPath
|
|
13
|
-
# @return [String] XPath without positional indexes
|
|
14
|
-
def self.simplify_xpath(xpath)
|
|
15
|
-
xpath.gsub(/\[\d+\]/, '')
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# @param parsed_body [Nokogiri::HTML::Document] parsed document
|
|
19
|
-
# @param minimum_selector_frequency [Integer] minimum repeated anchor path count
|
|
20
|
-
# @param use_top_selectors [Integer] number of frequent anchor paths to inspect
|
|
21
|
-
def initialize(parsed_body, minimum_selector_frequency:, use_top_selectors:)
|
|
22
|
-
@parsed_body = parsed_body
|
|
23
|
-
@minimum_selector_frequency = minimum_selector_frequency
|
|
24
|
-
@use_top_selectors = use_top_selectors
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
##
|
|
28
|
-
# @param anchor_filter [#call] predicate for scraper-specific anchor eligibility
|
|
29
|
-
# @param boundary_condition [#call] predicate for article container boundary
|
|
30
|
-
# @yieldparam article_tag [Nokogiri::XML::Node] candidate article container
|
|
31
|
-
# @yieldparam selected_anchor [Nokogiri::XML::Node] anchor that made the container eligible
|
|
32
|
-
# @return [Enumerator]
|
|
33
|
-
def each_article_tag(anchor_filter:, boundary_condition:)
|
|
34
|
-
return enum_for(:each_article_tag, anchor_filter:, boundary_condition:) unless block_given?
|
|
35
|
-
|
|
36
|
-
article_tags(anchor_filter:, boundary_condition:).each { yield _1[:article_tag], _1[:selected_anchor] }
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
private
|
|
40
|
-
|
|
41
|
-
attr_reader :parsed_body, :minimum_selector_frequency, :use_top_selectors
|
|
42
|
-
|
|
43
|
-
def article_tags(anchor_filter:, boundary_condition:)
|
|
44
|
-
cache = {}.compare_by_identity
|
|
45
|
-
selectors(anchor_filter:).flat_map do |selector|
|
|
46
|
-
article_tags_for_selector(selector, boundary_condition, cache)
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def article_tags_for_selector(selector, boundary_condition, cache = {}.compare_by_identity)
|
|
51
|
-
parsed_body.xpath(selector).filter_map do |selected_tag|
|
|
52
|
-
next if Html2rss::Html::Navigator.ignored_container_path?(selected_tag, cache)
|
|
53
|
-
|
|
54
|
-
article_tag = Html2rss::Html::Navigator.parent_until_condition(selected_tag, boundary_condition)
|
|
55
|
-
next unless article_tag
|
|
56
|
-
|
|
57
|
-
{ article_tag:, selected_anchor: selected_tag }
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def selectors(anchor_filter:)
|
|
62
|
-
anchor_counts(anchor_filter:)
|
|
63
|
-
.select { |_selector, count| count >= minimum_selector_frequency }
|
|
64
|
-
.max_by(use_top_selectors, &:last)
|
|
65
|
-
.map(&:first)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def anchor_counts(anchor_filter:)
|
|
69
|
-
cache = {}.compare_by_identity
|
|
70
|
-
Hash.new(0).tap do |counts|
|
|
71
|
-
each_anchor(anchor_filter:) do |node|
|
|
72
|
-
next if Html2rss::Html::Navigator.ignored_container_path?(node, cache)
|
|
73
|
-
|
|
74
|
-
path = self.class.simplify_xpath(node.path)
|
|
75
|
-
counts[path] += 1
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def each_anchor(anchor_filter:)
|
|
81
|
-
return enum_for(:each_anchor, anchor_filter:) unless block_given?
|
|
82
|
-
|
|
83
|
-
traversal_root&.css(Html2rss::Html::Navigator::MAIN_ANCHOR_SELECTOR)&.each do |node|
|
|
84
|
-
yield node if anchor_filter.call(node)
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def traversal_root
|
|
89
|
-
parsed_body.at_css('body, html') || parsed_body.root
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|