← back to Dw Vendor Scrapers Local
recrawl-osborne.js
78 lines
#!/usr/bin/env node
// Re-crawl driver: pulls every distinct osborne product_url from the DB and
// re-runs the FIXED extraction (images + specs) via the deployed scraper's
// exported functions, then upserts into osborne_catalog. Avoids the flaky
// browser collection-listing crawl by reusing known product URLs.
require('dotenv').config({ path: '/root/DW-Agents/dw-price-stock/.env' });
const fs = require('fs');
const https = require('https');
const { upsertProduct, pool } = require('/root/DW-Agents/vendor-scrapers/scraper-utils');
// Load extractFromHtml from the deployed scraper without running its main().
// Write the temp module INSIDE vendor-scrapers so node_modules (dotenv/pg) resolve.
const EXTRACT_TMP = '/root/DW-Agents/vendor-scrapers/_osb_extract.js';
let src = fs.readFileSync('/root/DW-Agents/vendor-scrapers/scrape-osborne.js', 'utf8')
.replace(/main\(\)\.catch\([\s\S]*$/, 'module.exports={extractFromHtml};\n');
fs.writeFileSync(EXTRACT_TMP, src);
const { extractFromHtml } = require(EXTRACT_TMP);
const SITE_BASE = 'https://www.osborneandlittle.com';
const TABLE = 'osborne_catalog';
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
function httpGet(url, maxRedirects = 4) {
return new Promise((resolve, reject) => {
const req = https.get(url, { timeout: 25000, headers: { 'User-Agent': UA, 'Accept': 'text/html,application/xhtml+xml', 'Accept-Language': 'en-US,en;q=0.9' } }, (res) => {
if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location && maxRedirects > 0) {
const loc = res.headers.location.startsWith('http') ? res.headers.location : SITE_BASE + res.headers.location;
res.resume();
return httpGet(loc, maxRedirects - 1).then(resolve).catch(reject);
}
if (res.statusCode !== 200) { res.resume(); return reject(new Error('HTTP ' + res.statusCode)); }
let d = ''; res.on('data', c => d += c); res.on('end', () => resolve(d)); res.on('error', reject);
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
});
}
const sleep = (ms) => new Promise(r => setTimeout(r, ms + Math.floor(Math.random() * 250)));
(async () => {
const { rows } = await pool.query(
`SELECT DISTINCT split_part(product_url,'?',1) AS url FROM ${TABLE} WHERE product_url IS NOT NULL AND product_url <> '' ORDER BY 1`
);
console.log(`Re-crawling ${rows.length} distinct osborne product pages...`);
let pages = 0, skus = 0, withImg = 0, withSpecs = 0, errs = 0;
const concurrency = 3;
let idx = 0;
async function worker() {
while (idx < rows.length) {
const i = idx++;
const url = rows[i].url;
try {
const html = await httpGet(url);
const products = extractFromHtml(html, url);
for (const p of products) {
if (!p.mfr_sku) continue;
const id = await upsertProduct(TABLE, p);
if (id) {
skus++;
if (p.image_url) withImg++;
if (p.specs) withSpecs++;
}
}
pages++;
if (pages % 25 === 0) console.log(` ${pages}/${rows.length} pages | ${skus} skus | ${withImg} w/img | ${withSpecs} w/specs | ${errs} errs`);
await sleep(350);
} catch (e) {
errs++;
if (errs % 10 === 0) console.error(` err at ${url}: ${e.message}`);
await sleep(800);
}
}
}
await Promise.all(Array.from({ length: concurrency }, worker));
console.log(`\nDONE. pages=${pages} skus_upserted=${skus} with_img=${withImg} with_specs=${withSpecs} errors=${errs}`);
await pool.end();
})().catch(e => { console.error('FATAL', e); pool.end().catch(() => {}); process.exit(1); });