ask-web-fetch 0.5.5 → 0.5.7
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/lib/ask/web_fetch/backend.rb +16 -0
- data/lib/ask/web_fetch/backends/browser.rb +4 -0
- data/lib/ask/web_fetch/backends/local.rb +59 -0
- data/lib/ask/web_fetch/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '08275e5b901ca9aaba313cac4cd3f776efb2c16b93aa709645c0ad367a1ff720'
|
|
4
|
+
data.tar.gz: 0eaba632b36081a0bc3fbf7dd1ff2b463cadb613e44d8a280d0546aeeda40676
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3c82f4b9abfb154fb06b9a5feaaf2254ddb6caa1cac99892a034311b4113ab1775114ad7c5aa126db88682c3ecf4cdfbee13786b61a06a00f2f30d6b838a2cdf
|
|
7
|
+
data.tar.gz: f3d14810e581699b5d2f8717fa329052ad8b81f03611d637d99fde979f1b324d2939ee79217ee812018327596f07ae0659c987b2e42f197df60af4bff30c8a7b
|
|
@@ -59,6 +59,18 @@ module Ask
|
|
|
59
59
|
# Wikipedia embeds an hcaptcha edit-config flag on every page).
|
|
60
60
|
CHALLENGE_RE = /just a moment|checking your browser|cf-chl/i
|
|
61
61
|
|
|
62
|
+
# Registrar parking-page markers: the page is an ad for a parked
|
|
63
|
+
# (for-sale) domain, not the site's content. A content company must
|
|
64
|
+
# never store these as if they were the site. Observed live on the
|
|
65
|
+
# CC list, three shapes: (a) GoDaddy's parking-lander JS app
|
|
66
|
+
# (ap:"parking" flag, parking-lander asset, LANDER_SYSTEM="PW")
|
|
67
|
+
# served at /lander, (b) Namecheap's parking app (utm_campaign=
|
|
68
|
+
# nc_market + parkingpage links), and (c) static registrar pages
|
|
69
|
+
# ("is parked free, courtesy of GoDaddy.com", "is registered at
|
|
70
|
+
# Namecheap"). Deliberately specific — generic terms like "domain"
|
|
71
|
+
# or "for sale" appear on real pages.
|
|
72
|
+
PARKED_DOMAIN_MARKERS = /ap:"parking"|parking-lander|LANDER_SYSTEM="PW"|utm_campaign=nc_market|utm_source=parkingpage|is parked free, courtesy of GoDaddy|is available on GoDaddy Auctions|is registered at Namecheap/i
|
|
73
|
+
|
|
62
74
|
def self.backend_name
|
|
63
75
|
name.split('::').last
|
|
64
76
|
end
|
|
@@ -118,6 +130,10 @@ module Ask
|
|
|
118
130
|
body.to_s.match?(CHALLENGE_RE)
|
|
119
131
|
end
|
|
120
132
|
|
|
133
|
+
def parked_domain?(body)
|
|
134
|
+
body.to_s.match?(PARKED_DOMAIN_MARKERS)
|
|
135
|
+
end
|
|
136
|
+
|
|
121
137
|
def usable_content?(content)
|
|
122
138
|
content.to_s.strip.length >= MIN_CONTENT_LENGTH
|
|
123
139
|
end
|
|
@@ -199,6 +199,10 @@ module Ask
|
|
|
199
199
|
|
|
200
200
|
return fetch_attempt(page, url, warmed: true)
|
|
201
201
|
end
|
|
202
|
+
# Browser renders the parked page a JS redirect lands on (the
|
|
203
|
+
# server shell hands /lander to JS) — Local never sees it. The
|
|
204
|
+
# shared parked-domain detector catches it here.
|
|
205
|
+
raise EmptyContentError, "parked domain at #{url} — registrar parking page, not site content" if parked_domain?(body)
|
|
202
206
|
|
|
203
207
|
result = Markdown.generate(body, base_url: url, filter: self.class.content_filter)
|
|
204
208
|
result[:outlinks] = outlink_urls(body, url)
|
|
@@ -45,7 +45,32 @@ module Ask
|
|
|
45
45
|
|
|
46
46
|
page = to_markdown(body, url)
|
|
47
47
|
page[:redirected] = redirect
|
|
48
|
+
# Parked-domain pages are not content: the domain owner parked it
|
|
49
|
+
# with a registrar and the page is an ad for buying the domain
|
|
50
|
+
# (GoDaddy/Namecheap/Sedo parking). A content company must never
|
|
51
|
+
# store these as if they were the site. Checked BEFORE the
|
|
52
|
+
# content-minimum — a parking page can render as "content" above
|
|
53
|
+
# the minimum (puncta.ai: 395c of Namecheap auction ads), and
|
|
54
|
+
# must still be rejected. Detectable from the server HTML —
|
|
55
|
+
# parked pages are fully server-rendered, so both Local and
|
|
56
|
+
# Browser see the same ad.
|
|
57
|
+
if parked_domain?(body)
|
|
58
|
+
raise EmptyContentError, "parked domain at #{url} — registrar parking page, not site content"
|
|
59
|
+
end
|
|
48
60
|
raise EmptyContentError, "no readable content at #{url}" unless usable_content?(page[:content])
|
|
61
|
+
# The completeness signal: a JS-app shell whose server HTML
|
|
62
|
+
# renders little is a TRUNCATED page, not a complete one — the
|
|
63
|
+
# real content awaits client-side JS that Local cannot run.
|
|
64
|
+
# Storing the shell as success would silently under-deliver
|
|
65
|
+
# (airbnb: 613KB server HTML -> 143 chars of markdown); failing
|
|
66
|
+
# through lets the chain prefer a rendering backend (Browser),
|
|
67
|
+
# and keeps a partial page from ever being stored as the real
|
|
68
|
+
# thing. Two detectors: known framework markers, or a large
|
|
69
|
+
# HTML page with almost no server-rendered text.
|
|
70
|
+
if js_app_shell?(body) && page[:content].length < SHELL_CONTENT_THRESHOLD
|
|
71
|
+
raise EmptyContentError,
|
|
72
|
+
"JS-app shell at #{url} — server HTML renders only #{page[:content].length} chars; a rendering backend is required"
|
|
73
|
+
end
|
|
49
74
|
|
|
50
75
|
page
|
|
51
76
|
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, SocketError, URI::InvalidURIError => e
|
|
@@ -55,6 +80,40 @@ module Ask
|
|
|
55
80
|
raise TimeoutError, "#{e.class}: #{e.message}"
|
|
56
81
|
end
|
|
57
82
|
|
|
83
|
+
# Client-rendered framework markers: the server HTML is (at least
|
|
84
|
+
# partly) a shell awaiting JS. Deliberately narrow — generic terms
|
|
85
|
+
# like "script" or "root" appear on every page; these are the
|
|
86
|
+
# specific footprints of React/Vue/Next/Nuxt app shells.
|
|
87
|
+
JS_APP_SHELL_MARKERS = /id=["'](?:root|app|__next|site-content)["']|__NEXT_DATA__|window\.__NUXT__|ng-app|data-reactroot/
|
|
88
|
+
|
|
89
|
+
# Markdown below this from a JS-app shell is "server sent a shell",
|
|
90
|
+
# not "page is genuinely short" — a real page (even a short one)
|
|
91
|
+
# is usually server-rendered above this. Tunable; the chain turns
|
|
92
|
+
# the signal into "prefer Browser for this URL".
|
|
93
|
+
SHELL_CONTENT_THRESHOLD = 4_000
|
|
94
|
+
|
|
95
|
+
# A page whose server HTML is large but yields almost no text is a
|
|
96
|
+
# shell whatever framework it uses (airbnb: 613KB HTML -> 143 chars
|
|
97
|
+
# of markdown). Framework markers miss these; the size ratio
|
|
98
|
+
# catches them. nytimes (1.4MB -> 7.5k text) stays above the text
|
|
99
|
+
# floor and is correctly left to Local.
|
|
100
|
+
SHELL_HTML_BYTES = 20_000
|
|
101
|
+
|
|
102
|
+
def js_app_shell?(body)
|
|
103
|
+
body.to_s.match?(JS_APP_SHELL_MARKERS) ||
|
|
104
|
+
(body.to_s.bytesize > SHELL_HTML_BYTES && markdown_visible_chars(body) < SHELL_CONTENT_THRESHOLD)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# A cheap text estimate from the raw HTML (tags stripped) — the
|
|
108
|
+
# "how much did the server actually render" number. Deliberately
|
|
109
|
+
# rough: it only feeds a shell-vs-page heuristic.
|
|
110
|
+
def markdown_visible_chars(body)
|
|
111
|
+
body.to_s.gsub(/<script[\s\S]*?<\/script>/i, "")
|
|
112
|
+
.gsub(/<style[\s\S]*?<\/style>/i, "")
|
|
113
|
+
.gsub(/<[^>]+>/, " ")
|
|
114
|
+
.gsub(/\s+/, " ").strip.length
|
|
115
|
+
end
|
|
116
|
+
|
|
58
117
|
# Parses +html+ and returns { title:, description:, content:,
|
|
59
118
|
# licenses:, outlinks: } where content is clean markdown (pruned by
|
|
60
119
|
# the ContentFilter), licenses are the page's declared license
|