rubycrawl 0.2.0 → 0.3.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 +25 -15
- data/lib/rubycrawl/browser/extraction.rb +34 -12
- data/lib/rubycrawl/browser/readability.js +2786 -0
- data/lib/rubycrawl/browser.rb +1 -1
- data/lib/rubycrawl/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 56d56f2c264e3febc0f1b22badabb739393332e0948a0f4e4ceb534a68127604
|
|
4
|
+
data.tar.gz: ebcadd14ba65b12870f6069f898658240073ba7233eab63468e0502871a7a408
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 182e8c771358324d256b38a42a236f634a113b18e16c716da891543ddb43a90ea68242bbc1655639781485e05802a203b64d7ea874eb4cb98900c2e771b85ec0
|
|
7
|
+
data.tar.gz: f150a6394fb2279b1f872c4074ef9b9df489f19266a7a35886b1e9fbd57e3d4d3761e0519a015270a5259c698d163e2ca223cf0106b6db6471c7911d65c12a29
|
data/README.md
CHANGED
|
@@ -16,6 +16,7 @@ RubyCrawl provides **accurate, JavaScript-enabled web scraping** using a pure Ru
|
|
|
16
16
|
- ✅ **Production-ready** — Auto-retry, error handling, resource optimization
|
|
17
17
|
- ✅ **Multi-page crawling** — BFS algorithm with smart URL deduplication
|
|
18
18
|
- ✅ **Rails-friendly** — Generators, initializers, and ActiveJob integration
|
|
19
|
+
- ✅ **Readability-powered** — Mozilla Readability.js for article-quality extraction, heuristic fallback for all other pages
|
|
19
20
|
|
|
20
21
|
```ruby
|
|
21
22
|
# One line to crawl any JavaScript-heavy site
|
|
@@ -35,7 +36,7 @@ result.metadata # Title, description, OG tags, etc.
|
|
|
35
36
|
- **Simple API**: Clean Ruby interface — zero Ferrum or CDP knowledge required
|
|
36
37
|
- **Resource optimization**: Built-in resource blocking for 2-3x faster crawls
|
|
37
38
|
- **Auto-managed browsers**: Lazy Chrome singleton, isolated page per crawl
|
|
38
|
-
- **Content extraction**:
|
|
39
|
+
- **Content extraction**: Mozilla Readability.js (primary) + link-density heuristic (fallback) — article-quality `clean_html`, `clean_text`, `clean_markdown`, links, metadata
|
|
39
40
|
- **Multi-page crawling**: BFS crawler with configurable depth limits and URL deduplication
|
|
40
41
|
- **Smart URL handling**: Automatic normalization, tracking parameter removal, same-host filtering
|
|
41
42
|
- **Rails integration**: First-class Rails support with generators and initializers
|
|
@@ -102,14 +103,15 @@ require "rubycrawl"
|
|
|
102
103
|
result = RubyCrawl.crawl("https://example.com")
|
|
103
104
|
|
|
104
105
|
# Access extracted content
|
|
105
|
-
result.final_url
|
|
106
|
-
result.clean_text
|
|
107
|
-
result.clean_html
|
|
108
|
-
result.raw_text
|
|
109
|
-
result.html
|
|
110
|
-
result.links
|
|
111
|
-
result.metadata
|
|
112
|
-
result.
|
|
106
|
+
result.final_url # Final URL after redirects
|
|
107
|
+
result.clean_text # Noise-stripped plain text (no nav/footer/ads)
|
|
108
|
+
result.clean_html # Noise-stripped HTML (same noise removed as clean_text)
|
|
109
|
+
result.raw_text # Full body.innerText (unfiltered)
|
|
110
|
+
result.html # Full raw HTML content
|
|
111
|
+
result.links # Extracted links with url, text, title, rel
|
|
112
|
+
result.metadata # Title, description, OG tags, etc.
|
|
113
|
+
result.metadata['extractor'] # "readability" or "heuristic" — which extractor ran
|
|
114
|
+
result.clean_markdown # Markdown converted from clean_html (lazy — first access only)
|
|
113
115
|
```
|
|
114
116
|
|
|
115
117
|
## Use Cases
|
|
@@ -318,7 +320,8 @@ result.metadata
|
|
|
318
320
|
# "twitter_image" => "https://...",
|
|
319
321
|
# "canonical" => "https://...",
|
|
320
322
|
# "lang" => "en",
|
|
321
|
-
# "charset" => "UTF-8"
|
|
323
|
+
# "charset" => "UTF-8",
|
|
324
|
+
# "extractor" => "readability" # or "heuristic"
|
|
322
325
|
# }
|
|
323
326
|
```
|
|
324
327
|
|
|
@@ -473,16 +476,21 @@ RubyCrawl uses a single-process architecture:
|
|
|
473
476
|
```
|
|
474
477
|
RubyCrawl (public API)
|
|
475
478
|
↓
|
|
476
|
-
Browser (lib/rubycrawl/browser.rb)
|
|
479
|
+
Browser (lib/rubycrawl/browser.rb) ← Ferrum wrapper
|
|
477
480
|
↓
|
|
478
|
-
Ferrum::Browser
|
|
481
|
+
Ferrum::Browser ← Chrome DevTools Protocol (pure Ruby)
|
|
479
482
|
↓
|
|
480
|
-
Chromium
|
|
483
|
+
Chromium ← headless browser
|
|
484
|
+
↓
|
|
485
|
+
Readability.js → heuristic fallback ← content extraction (inside browser)
|
|
481
486
|
```
|
|
482
487
|
|
|
483
488
|
- Chrome launches once lazily and is reused across all crawls
|
|
484
489
|
- Each crawl gets an isolated page context (own cookies/storage)
|
|
485
|
-
-
|
|
490
|
+
- Content extraction runs inside the browser via `page.evaluate()`:
|
|
491
|
+
- **Primary**: Mozilla Readability.js — article-quality extraction for blogs, docs, news
|
|
492
|
+
- **Fallback**: link-density heuristic — covers marketing pages, homepages, SPAs
|
|
493
|
+
- `result.metadata['extractor']` tells you which path was used (`"readability"` or `"heuristic"`)
|
|
486
494
|
- No separate processes, no HTTP boundary, no Node.js
|
|
487
495
|
|
|
488
496
|
## Performance
|
|
@@ -528,7 +536,9 @@ The gem is available as open source under the terms of the [MIT License](LICENSE
|
|
|
528
536
|
|
|
529
537
|
Built with [Ferrum](https://github.com/rubycdp/ferrum) — pure Ruby Chrome DevTools Protocol client.
|
|
530
538
|
|
|
531
|
-
|
|
539
|
+
Content extraction powered by [Mozilla Readability.js](https://github.com/mozilla/readability) — the algorithm behind Firefox Reader View.
|
|
540
|
+
|
|
541
|
+
Markdown conversion powered by [reverse_markdown](https://github.com/xijo/reverse_markdown) for GitHub-flavored output.
|
|
532
542
|
|
|
533
543
|
## Support
|
|
534
544
|
|
|
@@ -3,13 +3,10 @@
|
|
|
3
3
|
class RubyCrawl
|
|
4
4
|
class Browser
|
|
5
5
|
# JavaScript extraction constants, evaluated inside Chromium via page.evaluate().
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# All constants are IIFEs — Ferrum's page.evaluate() evaluates an expression,
|
|
7
|
+
# it does NOT call function definitions. Wrapping as (() => { ... })() ensures
|
|
8
|
+
# the function is immediately invoked and its return value is captured.
|
|
9
9
|
module Extraction
|
|
10
|
-
# All constants are IIFEs — Ferrum's page.evaluate() evaluates an expression,
|
|
11
|
-
# it does NOT call function definitions. Wrapping as (() => { ... })() ensures
|
|
12
|
-
# the function is immediately invoked and its return value is captured.
|
|
13
10
|
EXTRACT_METADATA_JS = <<~JS
|
|
14
11
|
(() => {
|
|
15
12
|
const getMeta = (name) => {
|
|
@@ -54,8 +51,7 @@ class RubyCrawl
|
|
|
54
51
|
(() => (document.body?.innerText || "").trim())()
|
|
55
52
|
JS
|
|
56
53
|
|
|
57
|
-
# Semantic noise selectors —
|
|
58
|
-
# Interpolated directly into EXTRACT_CONTENT_JS as a string literal.
|
|
54
|
+
# Semantic noise selectors — used by the heuristic fallback.
|
|
59
55
|
NOISE_SELECTORS = [
|
|
60
56
|
'nav', 'header', 'footer', 'aside',
|
|
61
57
|
'[role="navigation"]', '[role="banner"]', '[role="contentinfo"]',
|
|
@@ -64,11 +60,37 @@ class RubyCrawl
|
|
|
64
60
|
'script', 'style', 'noscript', 'iframe'
|
|
65
61
|
].join(', ').freeze
|
|
66
62
|
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
63
|
+
# Mozilla Readability.js v0.6.0 — vendored source, read once at load time.
|
|
64
|
+
# Embedded inside EXTRACT_CONTENT_JS's outer IIFE so Readability is defined
|
|
65
|
+
# and used within the same Runtime.evaluate expression (Ferrum evaluates a
|
|
66
|
+
# single expression — separate evaluate calls have separate scopes).
|
|
67
|
+
READABILITY_JS = File.read(File.join(__dir__, 'readability.js')).freeze
|
|
68
|
+
|
|
69
|
+
# Extracts clean article HTML using Mozilla Readability (primary) with a
|
|
70
|
+
# link-density heuristic as fallback when Readability returns no content.
|
|
71
|
+
# Everything is wrapped in one outer IIFE so page.evaluate gets a single
|
|
72
|
+
# expression and Readability is in scope for the extraction logic.
|
|
73
|
+
# DOM mutations from the fallback path are reversed after extraction.
|
|
70
74
|
EXTRACT_CONTENT_JS = <<~JS.freeze
|
|
71
75
|
(() => {
|
|
76
|
+
// Mozilla Readability.js v0.6.0 — defined in this IIFE's scope.
|
|
77
|
+
#{READABILITY_JS}
|
|
78
|
+
|
|
79
|
+
// Primary: Mozilla Readability — article-quality extraction.
|
|
80
|
+
let readabilityDebug = null;
|
|
81
|
+
try {
|
|
82
|
+
const docClone = document.cloneNode(true);
|
|
83
|
+
const reader = new Readability(docClone, { charThreshold: 100 });
|
|
84
|
+
const article = reader.parse();
|
|
85
|
+
if (article && article.textContent && article.textContent.trim().length > 200) {
|
|
86
|
+
return { cleanHtml: article.content, extractor: "readability" };
|
|
87
|
+
}
|
|
88
|
+
readabilityDebug = article ? `returned ${article.textContent?.trim().length ?? 0} text chars (below threshold)` : "returned null (no article detected)";
|
|
89
|
+
} catch (e) {
|
|
90
|
+
readabilityDebug = `error: ${e.message}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Fallback: link-density heuristic (works on nav-heavy / non-article pages).
|
|
72
94
|
const noiseSelectors = #{NOISE_SELECTORS.to_json};
|
|
73
95
|
function linkDensity(el) {
|
|
74
96
|
const total = (el.innerText || "").trim().length;
|
|
@@ -98,7 +120,7 @@ class RubyCrawl
|
|
|
98
120
|
}
|
|
99
121
|
const cleanHtml = document.body.innerHTML;
|
|
100
122
|
removed.reverse().forEach(({ el, parent, next }) => parent.insertBefore(el, next));
|
|
101
|
-
return { cleanHtml };
|
|
123
|
+
return { cleanHtml, extractor: "heuristic", debug: readabilityDebug };
|
|
102
124
|
})()
|
|
103
125
|
JS
|
|
104
126
|
end
|