Crawlr-Browser / src /scraper.js
senku21230's picture
Upload 11 files
ce9e053 verified
Raw
History Blame Contribute Delete
5.29 kB
// src/scraper.js
const puppeteer = require("puppeteer-extra");
const StealthPlugin = require("puppeteer-extra-plugin-stealth");
const { Readability } = require("@mozilla/readability");
const { JSDOM } = require("jsdom");
const TurndownService = require("turndown");
const { gfm } = require("turndown-plugin-gfm");
puppeteer.use(StealthPlugin());
// ── Turndown ──────────────────────────────────────────────────────────────
const td = new TurndownService({ headingStyle: "atx", codeBlockStyle: "fenced", bulletListMarker: "-" });
td.use(gfm);
td.addRule("removeNoise", {
filter: ["script", "style", "noscript", "svg", "iframe"],
replacement: () => "",
});
// ── HTML β†’ Clean Markdown (Readability দিয়ে) ─────────────────────────────
async function pageToMarkdown(page) {
const html = await page.content();
const url = page.url();
try {
const dom = new JSDOM(html, { url });
const reader = new Readability(dom.window.document);
const article = reader.parse();
if (article?.content) return td.turndown(article.content).replace(/\n{3,}/g, "\n\n").trim();
} catch (_) {}
// Readability fail করলে fallback
const bodyHtml = await page.evaluate(() => document.body?.innerHTML || "");
return td.turndown(bodyHtml).replace(/\n{3,}/g, "\n\n").trim();
}
// ── Page metadata ─────────────────────────────────────────────────────────
async function getPageMeta(page) {
return page.evaluate(() => {
const getMeta = (n) =>
document.querySelector(`meta[name="${n}"]`)?.content ||
document.querySelector(`meta[property="og:${n}"]`)?.content || null;
const links = Array.from(document.querySelectorAll("a[href]"))
.map((a) => ({ text: a.innerText.trim().slice(0, 100), href: a.href }))
.filter((l) => l.text && l.href.startsWith("http"))
.slice(0, 50);
const inputs = Array.from(document.querySelectorAll("input,textarea,select")).map((el) => ({
tag: el.tagName.toLowerCase(),
type: el.type || null,
name: el.name || el.id || null,
placeholder: el.placeholder || null,
selector: el.id ? `#${el.id}` : el.name ? `[name="${el.name}"]` : el.tagName.toLowerCase(),
}));
const buttons = Array.from(document.querySelectorAll("button,[role=button],input[type=submit]"))
.map((b) => ({
text: (b.innerText?.trim() || b.value || "").slice(0, 60),
selector: b.id ? `#${b.id}` : b.className ? `.${b.className.split(" ")[0]}` : "button",
}))
.filter((b) => b.text).slice(0, 20);
const headings = Array.from(document.querySelectorAll("h1,h2,h3"))
.map((h) => ({ level: h.tagName, text: h.innerText.trim() }))
.filter((h) => h.text).slice(0, 20);
return {
title: document.title,
url: window.location.href,
description: getMeta("description"),
keywords: getMeta("keywords"),
links, inputs, buttons, headings,
};
});
}
// ── Browser launch (stealth) ──────────────────────────────────────────────
async function launchBrowser(proxy = null) {
const args = [
"--no-sandbox", "--disable-setuid-sandbox",
"--disable-dev-shm-usage", "--disable-gpu",
"--window-size=1280,800",
"--disable-blink-features=AutomationControlled",
];
if (proxy) args.push(`--proxy-server=${proxy}`);
return puppeteer.launch({
executablePath: process.env.CHROMIUM_PATH || "/usr/bin/chromium",
headless: "new",
args,
});
}
// ── Stealth page setup ────────────────────────────────────────────────────
async function newStealthPage(browser) {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
await page.setUserAgent(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
);
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, "webdriver", { get: () => undefined });
Object.defineProperty(navigator, "plugins", { get: () => [1, 2, 3] });
window.chrome = { runtime: {} };
});
return page;
}
// ── Smart wait β€” SPA support ──────────────────────────────────────────────
async function smartWait(page, options = {}) {
const { waitFor, waitForSelector, networkIdle = false } = options;
if (networkIdle) {
await page.waitForLoadState?.("networkidle").catch(() =>
new Promise((r) => setTimeout(r, 2000))
);
}
if (waitForSelector) await page.waitForSelector(waitForSelector, { timeout: 10000 }).catch(() => {});
if (waitFor > 0) await new Promise((r) => setTimeout(r, Math.min(waitFor, 15000)));
}
module.exports = { launchBrowser, newStealthPage, pageToMarkdown, getPageMeta, smartWait, td };