Skip to content

Scrapers ​

Scrapers are how Ripperoni fetches raw source material — HTML pages or MediaWiki API responses — and hands the result to your plugin node. You do not call them directly; the dagonizer dispatcher (engine: @studnicky/dagonizer, wired up via RipperServices) fetches each page and places the result in state.input before your node runs.

Two scraper classes, one parser: HtmlScraper works the HTML side, handing you a Cheerio document to carve with; MediaWikiScraper works the API side, speaking structured wikitext directly. WikitextParser turns that wikitext into structured infobox, section, and category data.

Choosing which to use ​

Use HtmlScraper (set baseUrl in state.json) when:

  • The site serves HTML pages you want to scrape with CSS selectors.
  • You need redirect handling, custom headers, or cookie-based auth.
  • The content is in the HTML body, not behind a structured API.

Use MediaWikiScraper (set apiUrl in state.json) when:

  • The target is a MediaWiki site (Wikipedia, Fandom wikis, internal wikis).
  • You want structured wikitext parsing with infobox extraction.
  • You need to enumerate a full wiki or specific categories.

HtmlScraper ​

import { HtmlScraper } from 'ripperoni/HtmlScraper'

HtmlScraper works the HTML counter: it fetches static pages via native fetch, parses them with cheerio, and hands back a live handle to carve with. Rate limiting, jitter, caching, and retry classification are handled automatically — the gristle is trimmed before your plugin ever sees the cut.

Creating an instance ​

ts
const scraper = HtmlScraper.create({
  baseUrl:       'https://example.com',
  rateLimitMs:   250,
  jitterMs:      50,
  maxRetries:    3,
  retryBaseDelayMs: 500,
  retryMaxDelayMs:  30_000,
  headers:       { 'User-Agent': 'my-bot/1.0' },
  cache,         // optional ScraperCache
});

HtmlScraper.create(config: HtmlScraperConfigType): HtmlScraper

fetchPage ​

ts
async fetchPage(path: string): Promise<ScrapedPageType>

path is a URL path or full URL. When relative, it is joined to baseUrl with exactly one / separator. Returns:

ts
type ScrapedPageType = {
  readonly url:  string;      // resolved URL that was fetched
  readonly $:    CheerioAPI;  // Cheerio document loaded from html
  readonly html: string;      // raw HTML body
};

Fetch state machine: (1) rate limiter delays if needed; (2) cache is checked — on a hit, return immediately (skip 3–5); (3) HTTP GET is issued with configured headers; (4) on error, HttpRetryPolicy classifies and retries if applicable; (5) on success, body is stored in cache and returned.

The dispatcher places a ScrapedPageType value into state.input before your plugin node runs. Cast it and use $ directly:

ts
const { $, url } = state.input as ScrapedPageType;
$('h1.title').first().text().trim();

Or load the raw HTML yourself:

ts
import * as cheerio from 'cheerio';
const $ = cheerio.load(state.input['html'] as string);

JSDOM mode ​

For pages that require script execution (single-page apps, lazy-loaded content, synchronous DOM manipulation that happens before content is accessible), enable JSDOM mode:

json
"useJsdom": true

When useJsdom is enabled, each fetched HTML body is passed through JSDOM with runScripts: 'dangerously' and resources: 'usable' before Cheerio loads it. JSDOM fires the window.load event once external resources settle, then fetchPage serializes the final DOM and returns it.

The wait is a Promise.race between the load event and a ceiling timeout — so a page that loads in 3 s resolves at 3 s regardless of the ceiling. The ceiling only fires for scripts that hang and never dispatch load.

Ceiling timeout defaults to max(10 000, retryMaxDelayMs ?? 30 000) — it scales with the site's configured retry tolerance. Override explicitly when needed:

json
"useJsdom":            true,
"jsdomLoadTimeoutMs":  15000
Config fieldTypeDefaultDescription
useJsdombooleanfalsePass fetched HTML through JSDOM before Cheerio.
jsdomLoadTimeoutMsinteger ≥ 1000max(10000, retryMaxDelayMs ?? 30000)Ceiling for the JSDOM load event wait.

JSDOM executes scripts in-process. Do not enable useJsdom against untrusted or adversarial pages.

fetchText ​

ts
async fetchText(path: string): Promise<string>

Convenience wrapper around fetchPage that returns only the raw HTML string.

Retry behavior ​

Errors are classified into seven categories. Only four are retryable:

CategoryTriggerRetryableRationale
NETWORKECONNREFUSED, ECONNRESET, ENOTFOUNDyesNetwork layer is transient; the server might be back up
TIMEOUTETIMEDOUT, ESOCKETTIMEDOUTyesTimeout means the request got no response; server might recover
THROTTLEDHTTP 429 (reads Retry-After)yesServer is asking you to wait and retry; honoring this prevents IP bans
TRANSIENTHTTP 5xxyesServer errors are temporary; the instance might recover or failover
PERMANENTHTTP 4xx (except 429)no400, 403, 404, 410 mean the request is malformed or the resource doesn't exist; retrying won't help
VALIDATIONTypeError, SyntaxErrornoCode or response parsing is broken; retrying won't fix it
RESOURCEENOMEM, ENOSPCnoMachine is out of memory or disk; retrying will fail again

On THROTTLED: if the server sends a Retry-After header, that value overrides the configured backoff delay. If the header is malformed, the exponential backoff curve is used as a fallback.

Retry config in state.json:

json
"maxRetries":       3,
"retryBaseDelayMs": 500,
"retryMaxDelayMs":  30000

Worst-case latency: With maxRetries: 3, baseDelayMs: 500, multiplier: 2, maxDelayMs: 30000, a single URL can take: initial attempt + 500ms + attempt + 1000ms + attempt + 2000ms + attempt = ~3.5 seconds in the best case (all retries fail). If the server throttles with a high Retry-After, the wait is longer. Parallel scatter runs multiple URLs concurrently, so total time for N URLs is roughly (N / concurrency) * maxLatency.


MediaWikiScraper ​

import { MediaWikiScraper } from 'ripperoni/MediaWikiScraper'

MediaWikiScraper works the API counter: it calls the MediaWiki JSON API directly via fetch() and returns meaty, structured wikitext. Batching, auto-pagination, rate limiting, caching, and retry are handled in one place.

Creating an instance ​

MediaWikiScraper.create is async:

ts
const scraper = await MediaWikiScraper.create({
  apiUrl:          'https://wiki.example.com/api.php',
  rateLimitMs:     1_000,
  jitterMs:        100,
  maxRetries:      3,
  retryBaseDelayMs: 500,
  retryMaxDelayMs:  30_000,
  cache,           // optional ScraperCache
});

static async create(config: MediaWikiConfigType): Promise<MediaWikiScraper>

Methods ​

MethodAPI callReturns
fetchPage(title)action=query&prop=revisions (via batch)Promise<WikiPageType>
fetchPagesBatch(titles)action=query&prop=revisions&titles=<pipe-delimited>Promise<WikiPageType[]>
fetchCategory(name)action=query&list=categorymembers — auto-paginatedPromise<CategoryMemberType[]>
fetchAllPages(batchSize?)action=query&list=allpages — auto-paginatedPromise<CategoryMemberType[]>

fetchPage delegates to fetchPagesBatch internally; both are cache-aware.

fetchAllPages takes an optional batchSize argument (default 500) controlling how many pages are requested per API call.

Return shapes:

ts
type WikiPageType = {
  readonly title:    string;  // article title
  readonly wikitext: string;  // raw wikitext source
};

type CategoryMemberType = {
  readonly title:  string;  // article title
  readonly pageid: number;  // numeric MediaWiki page ID
};

What your plugin gets in state.input for a wiki scrape is a WikiPageType. Parse it with WikitextParser:

ts
import { WikitextParser } from 'ripperoni/WikitextParser';
const { title, wikitext } = state.input as WikiPageType;
const parsed = WikitextParser.parse(title, wikitext);

Rate limiting ​

Rate limit and jitter apply per API request, same as HtmlScraper. Batch requests count as one request toward the rate limit.


WikitextParser ​

import { WikitextParser } from 'ripperoni/WikitextParser'

WikitextParser converts raw wikitext into structured infobox, section, and category data using wtf_wikipedia. All methods are static.

parse ​

ts
static parse(title: string, wikitext: string): ParsedPageType
ts
type ParsedPageType = {
  readonly title:      string;
  readonly infobox:    Record<string, string | string[] | number | boolean | null>;
  readonly sections:   ReadonlyArray<{ readonly title: string; readonly text: string }>;
  readonly categories: readonly string[];
};

infoboxField ​

ts
static infoboxField(parsed: ParsedPageType, field: string): string | null

Returns the infobox field value as a string, or null if the field is absent.

infoboxNumber ​

ts
static infoboxNumber(parsed: ParsedPageType, field: string): number | null

Returns the infobox field value parsed as a finite number via parseFloat, or null if absent or non-numeric.

Usage:

ts
const parsed = WikitextParser.parse(title, wikitext);
const cr   = WikitextParser.infoboxNumber(parsed, 'cr');    // number | null
const name = WikitextParser.infoboxField(parsed, 'name');   // string | null

Released under the MIT License.