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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9fb671708a756ff233448c5d18ac0bd815eb44ecf8d0a8c893fcf8107f95c182
4
- data.tar.gz: 66898cb3f978494123441044319105e027f670557cd61430a184e0e1d105a3ed
3
+ metadata.gz: 56d56f2c264e3febc0f1b22badabb739393332e0948a0f4e4ceb534a68127604
4
+ data.tar.gz: ebcadd14ba65b12870f6069f898658240073ba7233eab63468e0502871a7a408
5
5
  SHA512:
6
- metadata.gz: d715984a47719f7c022c512bf136c7bd01050b67ea1dba44f9ea2e24b93196525f46c31e95402c18ba2afbceba02a734f8f214ee686522260557589fca7fea01
7
- data.tar.gz: 621e0c5ee326f2757c5459ca763bffd24bfa70baa9c33d820003d7bae0da0ce20529e1bff95136893b846fd80efc70129873ee87ccdb7c2bef5926879d01e6ba
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**: HTML, plain text, clean HTML, Markdown (lazy), links, metadata
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 # Final URL after redirects
106
- result.clean_text # Noise-stripped plain text (no nav/footer/ads)
107
- result.clean_html # Noise-stripped HTML (same noise removed as clean_text)
108
- result.raw_text # Full body.innerText (unfiltered)
109
- result.html # Full raw HTML content
110
- result.links # Extracted links with url, text, title, rel
111
- result.metadata # Title, description, OG tags, etc.
112
- result.clean_markdown # Markdown converted from clean_html (lazy first access only)
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) ← Ferrum wrapper
479
+ Browser (lib/rubycrawl/browser.rb) ← Ferrum wrapper
477
480
 
478
- Ferrum::Browser ← Chrome DevTools Protocol (pure Ruby)
481
+ Ferrum::Browser ← Chrome DevTools Protocol (pure Ruby)
479
482
 
480
- Chromium ← headless browser
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
- - JS extraction runs inside the browser via `page.evaluate()`
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
- Powered by [reverse_markdown](https://github.com/xijo/reverse_markdown) for GitHub-flavored Markdown conversion.
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
- # Ported verbatim from node/src/index.jslogic is unchanged.
7
- # NOISE_SELECTORS is interpolated directly into EXTRACT_CONTENT_JS (no need to
8
- # pass as a JS argument as the Node version did).
6
+ # All constants are IIFEsFerrum'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 — covers standard HTML5 elements and ARIA roles.
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
- # Removes semantic noise (nav/header/footer/aside + ARIA roles) and high
68
- # link-density containers, then returns both clean plain text and clean HTML.
69
- # DOM mutations are reversed after extraction so the page is unchanged.
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