detergent 2.3.0 → 2.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/README.md +5 -0
- data/lib/detergent/cleaner.rb +13 -2
- data/lib/detergent/inspector.rb +10 -3
- data/lib/detergent/link_list_extractor.rb +61 -0
- data/lib/detergent/markdown_renderer.rb +5 -1
- data/lib/detergent/version.rb +1 -1
- data/lib/detergent.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fe5c0dc45668a4c14b991433606aec83c6fd2672e93533632d608e66b858f03a
|
|
4
|
+
data.tar.gz: eada52c178f26669036609d5431f85245cd06d5de6b3920b010ba91a587ba18c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9ed056835cc1475822f443caa51610abc1dab4000dccf97f1058971f15cf8d9ceb6dbf44c7a91b079b4d6b886a8f239f1ec1408a53311e8d335d17cb34174886
|
|
7
|
+
data.tar.gz: e0ff4553f215510c0aebfcbbe35b2a5cca86ebfc8c755f597157a4c565b9950ad6b188b32610a92c7661635307ac388968a5e6a2fcd157e87463e2582c51810e
|
data/README.md
CHANGED
|
@@ -75,6 +75,11 @@ report object also exposes `located?`, `best_score`, `top_nodes`, and
|
|
|
75
75
|
3. The highest-scoring node is located and walked up to an appropriate semantic container.
|
|
76
76
|
4. A second pass cleans that content of empty and suspect elements.
|
|
77
77
|
|
|
78
|
+
Pages with no article content but a substantial set of title-like links —
|
|
79
|
+
index pages and link aggregators like the Hacker News front page — fall
|
|
80
|
+
back to link-list extraction: the result is a clean list of the page's
|
|
81
|
+
links instead of an article.
|
|
82
|
+
|
|
78
83
|
## License
|
|
79
84
|
|
|
80
85
|
[WTFPL](LICENSE)
|
data/lib/detergent/cleaner.rb
CHANGED
|
@@ -5,8 +5,9 @@ module Detergent
|
|
|
5
5
|
# scrubs it, and renders the result back out as a standalone document.
|
|
6
6
|
class Cleaner
|
|
7
7
|
# observer (optional) receives instrumentation callbacks during
|
|
8
|
-
# extraction: node_removed(node, pass:)
|
|
9
|
-
# Used by Inspector to build debug
|
|
8
|
+
# extraction: node_removed(node, pass:), content_pruned(body, scorer),
|
|
9
|
+
# and extraction_strategy(strategy). Used by Inspector to build debug
|
|
10
|
+
# reports.
|
|
10
11
|
def initialize(observer: nil)
|
|
11
12
|
@observer = observer
|
|
12
13
|
@obvious_junk_matcher = Matchers::ObviousJunkMatcher.new
|
|
@@ -57,6 +58,16 @@ module Detergent
|
|
|
57
58
|
scorer = NodeScorer.new
|
|
58
59
|
@observer&.content_pruned(body, scorer)
|
|
59
60
|
content = ContentLocator.new(scorer).locate(body)
|
|
61
|
+
strategy = content ? :article : nil
|
|
62
|
+
|
|
63
|
+
# No article-shaped content: the page may be a link index (front
|
|
64
|
+
# page, aggregator), whose content is its list of links.
|
|
65
|
+
if content.nil?
|
|
66
|
+
content = LinkListExtractor.new.extract(body)
|
|
67
|
+
strategy = :link_list if content
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
@observer&.extraction_strategy(strategy)
|
|
60
71
|
|
|
61
72
|
# Apply second-pass cleaning to the content
|
|
62
73
|
if content
|
data/lib/detergent/inspector.rb
CHANGED
|
@@ -19,7 +19,7 @@ module Detergent
|
|
|
19
19
|
ScoredNode = Struct.new(:score, :descriptor, :preview, :breakdown)
|
|
20
20
|
|
|
21
21
|
class Report
|
|
22
|
-
attr_accessor :title, :best_score, :content_root, :content_root_size, :top_nodes
|
|
22
|
+
attr_accessor :title, :best_score, :content_root, :content_root_size, :top_nodes, :strategy
|
|
23
23
|
attr_reader :removals
|
|
24
24
|
|
|
25
25
|
def initialize
|
|
@@ -63,8 +63,11 @@ module Detergent
|
|
|
63
63
|
def verdict
|
|
64
64
|
threshold = ContentLocator::MINIMUM_CONTENT_SCORE
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
case strategy
|
|
67
|
+
when :article
|
|
68
|
+
"Verdict: article content located (best score #{best_score}, threshold #{threshold})"
|
|
69
|
+
when :link_list
|
|
70
|
+
"Verdict: no article content (best score #{best_score} < threshold #{threshold}); extracted link list instead"
|
|
68
71
|
else
|
|
69
72
|
"Verdict: NO MAIN CONTENT (best score #{best_score} < threshold #{threshold})"
|
|
70
73
|
end
|
|
@@ -117,6 +120,10 @@ module Detergent
|
|
|
117
120
|
@report.best_score = scored.first&.first || 0
|
|
118
121
|
end
|
|
119
122
|
|
|
123
|
+
def extraction_strategy(strategy)
|
|
124
|
+
@report.strategy = strategy
|
|
125
|
+
end
|
|
126
|
+
|
|
120
127
|
private
|
|
121
128
|
|
|
122
129
|
def descriptor(node)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Detergent
|
|
4
|
+
# Fallback extractor for index pages: link aggregators, front pages, and
|
|
5
|
+
# directories (e.g. the Hacker News front page) where the "content" is a
|
|
6
|
+
# list of links to other pages. The article locator correctly rejects
|
|
7
|
+
# these — link density is its strongest navigation signal — so when it
|
|
8
|
+
# finds nothing, this extractor looks for a substantial collection of
|
|
9
|
+
# title-like links and returns them as a synthetic list node that flows
|
|
10
|
+
# through the normal cleaning and rendering pipeline.
|
|
11
|
+
class LinkListExtractor
|
|
12
|
+
# Links with less text than this are treated as chrome ("login",
|
|
13
|
+
# "hide", "42 comments"), not titles.
|
|
14
|
+
MIN_LINK_TEXT_LENGTH = 15
|
|
15
|
+
|
|
16
|
+
# Fewer title-like links than this isn't an index page.
|
|
17
|
+
MIN_LINKS = 10
|
|
18
|
+
|
|
19
|
+
# Returns a synthetic <ul> of the page's title-like links, or nil if
|
|
20
|
+
# the page doesn't look like a link index.
|
|
21
|
+
def extract(body)
|
|
22
|
+
links = title_like_links(body)
|
|
23
|
+
return nil if links.length < MIN_LINKS
|
|
24
|
+
|
|
25
|
+
build_list(body.document, links)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def title_like_links(body)
|
|
31
|
+
seen_hrefs = {}
|
|
32
|
+
|
|
33
|
+
body.css("a").filter_map do |link|
|
|
34
|
+
href = link["href"].to_s
|
|
35
|
+
next if href.empty? || href.start_with?("#", "javascript:")
|
|
36
|
+
|
|
37
|
+
text = link.text.gsub(/\s+/, " ").strip
|
|
38
|
+
next if text.length < MIN_LINK_TEXT_LENGTH
|
|
39
|
+
next if seen_hrefs[href]
|
|
40
|
+
|
|
41
|
+
seen_hrefs[href] = true
|
|
42
|
+
[text, href]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def build_list(doc, links)
|
|
47
|
+
list = Nokogiri::XML::Node.new("ul", doc)
|
|
48
|
+
|
|
49
|
+
links.each do |text, href|
|
|
50
|
+
item = Nokogiri::XML::Node.new("li", doc)
|
|
51
|
+
anchor = Nokogiri::XML::Node.new("a", doc)
|
|
52
|
+
anchor["href"] = href
|
|
53
|
+
anchor.content = text
|
|
54
|
+
item.add_child(anchor)
|
|
55
|
+
list.add_child(item)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
list
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -9,7 +9,11 @@ module Detergent
|
|
|
9
9
|
# image, ...) so TextRenderer can subclass this and strip the syntax.
|
|
10
10
|
class MarkdownRenderer
|
|
11
11
|
def render(node)
|
|
12
|
-
|
|
12
|
+
# Route through render_block so a root that is itself a block
|
|
13
|
+
# element (a ul, a blockquote) gets its block treatment; generic
|
|
14
|
+
# containers and fragments fall through to render_blocks.
|
|
15
|
+
output = node.element? ? render_block(node) : render_blocks(node)
|
|
16
|
+
output.gsub(/\n{3,}/, "\n\n").strip
|
|
13
17
|
end
|
|
14
18
|
|
|
15
19
|
private
|
data/lib/detergent/version.rb
CHANGED
data/lib/detergent.rb
CHANGED
|
@@ -46,6 +46,7 @@ end
|
|
|
46
46
|
require_relative "detergent/version"
|
|
47
47
|
require_relative "detergent/node_scorer"
|
|
48
48
|
require_relative "detergent/content_locator"
|
|
49
|
+
require_relative "detergent/link_list_extractor"
|
|
49
50
|
require_relative "detergent/markdown_renderer"
|
|
50
51
|
require_relative "detergent/text_renderer"
|
|
51
52
|
require_relative "detergent/matchers/obvious_junk_matcher"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: detergent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jeff McFadden
|
|
@@ -67,6 +67,7 @@ files:
|
|
|
67
67
|
- lib/detergent/cleaner.rb
|
|
68
68
|
- lib/detergent/content_locator.rb
|
|
69
69
|
- lib/detergent/inspector.rb
|
|
70
|
+
- lib/detergent/link_list_extractor.rb
|
|
70
71
|
- lib/detergent/markdown_renderer.rb
|
|
71
72
|
- lib/detergent/matchers/obvious_junk_matcher.rb
|
|
72
73
|
- lib/detergent/matchers/removable_node_matcher.rb
|