[object Object]

← back to Rebel Walls Scraper

Add resumable checkpointed crawl mode for full 4309-product run

93cb660f0c2a0564c272015b8333a4c8248bcfdb · 2026-06-03 20:56:15 -0700 · SteveStudio2

Files touched

Diff

commit 93cb660f0c2a0564c272015b8333a4c8248bcfdb
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed Jun 3 20:56:15 2026 -0700

    Add resumable checkpointed crawl mode for full 4309-product run
---
 scripts/scrape.js | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 63 insertions(+), 1 deletion(-)

diff --git a/scripts/scrape.js b/scripts/scrape.js
index 2c80e59..80cdaeb 100644
--- a/scripts/scrape.js
+++ b/scripts/scrape.js
@@ -4,6 +4,7 @@
  * Modes:
  *   node scripts/scrape.js enumerate            -> data/product-urls.json
  *   node scripts/scrape.js pilot <N> [offset]   -> output/pilot-<date>.json
+ *   node scripts/scrape.js crawl                -> output/crawl-<date>.json (full set, resumable, checkpointed)
  *   node scripts/scrape.js fullproduct <json>   -> data-quality scorecard
  *
  * Zero-dependency: uses global fetch (Node 18+). Polite: 1.5s delay, single-threaded.
@@ -213,6 +214,66 @@ async function pilot(n, offset) {
   return outFile;
 }
 
+/* ---- crawl: full set, single-threaded, checkpointed + resumable ---- */
+async function crawl() {
+  const urlsPath = path.join(DATA, 'product-urls.json');
+  let urls;
+  if (fs.existsSync(urlsPath)) urls = JSON.parse(fs.readFileSync(urlsPath, 'utf8'));
+  else urls = await enumerate();
+
+  fs.mkdirSync(OUT, { recursive: true });
+  const outFile = path.join(OUT, `crawl-${today()}.json`);
+
+  // Resume support: if outFile exists, skip URLs already captured OK.
+  let results = [];
+  const done = new Set();
+  if (fs.existsSync(outFile)) {
+    try {
+      results = JSON.parse(fs.readFileSync(outFile, 'utf8'));
+      for (const r of results) if (r && r.product_url && !r.error) done.add(r.product_url);
+      console.error(`Resume: ${done.size} already captured in ${path.basename(outFile)}; ${results.length} total rows.`);
+    } catch (_) { results = []; }
+  }
+  // Drop prior error rows for URLs we'll retry, then re-add fresh.
+  results = results.filter((r) => r && r.product_url && !r.error);
+
+  const todo = urls.filter((u) => !done.has(u));
+  console.error(`Crawling ${todo.length} of ${urls.length} URLs (${done.size} already done). ~${Math.round(todo.length * DELAY_MS / 1000 / 60)} min @ ${DELAY_MS}ms.`);
+
+  let ok = 0, err = 0;
+  for (let i = 0; i < todo.length; i++) {
+    const u = todo[i];
+    process.stderr.write(`[${i + 1}/${todo.length}] ${u} … `);
+    let attempt = 0, captured = null, lastErr = null;
+    while (attempt < 3 && !captured) {
+      attempt++;
+      try {
+        captured = await parseProduct(u);
+      } catch (e) {
+        lastErr = e;
+        if (attempt < 3) { process.stderr.write(`retry${attempt} `); await sleep(DELAY_MS * 2); }
+      }
+    }
+    if (captured) {
+      results.push(captured);
+      ok++;
+      process.stderr.write(`OK ${captured.mfr_sku} $${captured.price_retail}/m²\n`);
+    } else {
+      results.push({ product_url: u, error: String((lastErr && lastErr.message) || lastErr) });
+      err++;
+      process.stderr.write(`ERR ${(lastErr && lastErr.message) || lastErr}\n`);
+    }
+    // Checkpoint every 25 products (and on the last one) so a crash loses ≤25.
+    if ((i + 1) % 25 === 0 || i === todo.length - 1) {
+      fs.writeFileSync(outFile, JSON.stringify(results, null, 2));
+    }
+    if (i < todo.length - 1) await sleep(DELAY_MS);
+  }
+  fs.writeFileSync(outFile, JSON.stringify(results, null, 2));
+  console.error(`\nCrawl complete: ${ok} OK, ${err} ERR this run; ${results.length} total rows -> ${outFile}`);
+  return outFile;
+}
+
 /* ---- fullproduct gate ---- */
 function fullproduct(jsonPath) {
   const rows = JSON.parse(fs.readFileSync(jsonPath, 'utf8')).filter((r) => !r.error);
@@ -260,9 +321,10 @@ function fullproduct(jsonPath) {
   try {
     if (mode === 'enumerate') await enumerate();
     else if (mode === 'pilot') await pilot(parseInt(a || '25', 10), parseInt(b || '0', 10));
+    else if (mode === 'crawl') await crawl();
     else if (mode === 'fullproduct') fullproduct(a);
     else {
-      console.error('Usage: scrape.js enumerate | pilot <N> [offset] | fullproduct <json>');
+      console.error('Usage: scrape.js enumerate | pilot <N> [offset] | crawl | fullproduct <json>');
       process.exit(1);
     }
   } catch (e) {

← f8fa280 Rebel Walls scraper: enumerate 4309 + 25-SKU pilot (fullprod  ·  back to Rebel Walls Scraper  ·  Add dw_unified import script: gate + 2.5x markup + DWRW SKU 87c6420 →