UNPKG

snazzy-crawler

Version:

A snazzy light Node.js image crawler laced with TypeScript goodness! 🕵️🦾

149 lines (148 loc) 5.81 kB
import puppeteer from 'puppeteer'; import inquirer from 'inquirer'; import { DEPTH_LIMIT, exportToJson, getOnlyHostURLs, getRandomEmoji, getUnique, normalizeURL } from '../lib/utils.js'; import chalk from 'chalk'; class Crawler { constructor(options) { this.baseURL = options.baseURL; this.depth = options.depth; this.browser = options.browser; this.page = options.page; this.crawledUrls = new Set(); this.stack = []; this.result = []; } static async init(url, depth = 0) { const browser = await puppeteer.launch({ headless: 'new' }); const page = await browser.newPage(); return new Crawler({ baseURL: url, depth, browser, page }); } async run() { const startTime = process.hrtime.bigint(); /* eslint-disable-next-line @typescript-eslint/no-misused-promises */ this.page.once('close', async () => { await this.onPageClosed(); }); await this.crawlPage(this.baseURL, this.baseURL); const endTime = process.hrtime.bigint(); const completedInSeconds = (Number(endTime - startTime) / 1000000 / 1000).toFixed(2); console.log(this.result); console.log(chalk.magenta(`\nYASSSS, Crawling Done!. 🫡 ${completedInSeconds}s`)); console.log(chalk.magenta(`"Wow! We found ${this.result.length} awesome images! 🎉"`)); await this._close(); } async onPageClosed() { await inquirer .prompt([ { name: 'exportToJSON', type: 'confirm', message: 'Export to JSON file? 💾', default: false } ]) .then((answers) => { if (answers.exportToJSON) { exportToJson(this.result, this.baseURL); } }) .catch((error) => { console.log(error); }); process.exit(); } async crawlPage(baseURL, currentURL) { const baseURLObj = new URL(baseURL); const currentURLObj = new URL(currentURL); if (baseURLObj.hostname !== currentURLObj.hostname) { return; } console.log(chalk.green(`${currentURL}? ${getRandomEmoji()} Let's crawl it!"`)); await this.page.goto(currentURL); await this.autoScroll(this.page); const images = await this.findImages(this.page, currentURL, 0); if (images.length === 0) { console.log(chalk.cyan('No pics, no proof.')); } else { console.log(chalk.cyan(`Boom! Found ${images.length} pics.`)); } this.result.push(...images); const nextURLs = await this.findURLs(this.page); this.stack = [...nextURLs]; const maxDepth = Math.min(this.depth, DEPTH_LIMIT); for (let l = 0; l < maxDepth; l++) { if (l > 4) { console.log(chalk.magenta(`\nWe're diving in deep, we've hit level ${l}!`)); } else { console.log(chalk.magenta(`You're at level ${l}`)); } /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ for (const _ of nextURLs) { if (this.stack.length === 0) return; const url = normalizeURL(this.stack.pop()); if (!this.crawledUrls.has(url)) { this.crawledUrls.add(url); try { console.log(chalk .hex('fff1f3') .underline(`\nGrabbing every link on ${url}! 🌐`)); await this.page.goto(url); const images = await this.findImages(this.page, url, l); if (images.length === 0) { console.log(chalk.cyan('No pics, no proof.')); } else { console.log(chalk.cyan(`Boom! Found ${images.length} ${images.length === 1 ? 'pic' : 'pics'}.`)); } this.result.push(...images); } catch (e) { console.log(chalk.red(e.message)); } const urls = await this.findURLs(this.page); for (const i of urls) { this.stack.push(i); } } } } } async autoScroll(page) { await page.evaluate(async () => { await new Promise((resolve) => { let totalHeight = 0; const distance = 250; const timer = setInterval(() => { const scrollHeight = document.body.scrollHeight; window.scrollBy(0, distance); totalHeight += distance; if (totalHeight >= scrollHeight - window.innerHeight) { clearInterval(timer); resolve(); } }, 100); }); }); } async findImages(page, sourceUrl, depth) { console.log(chalk.italic("Hmm... Let's find some pics on this page! 🧐📷")); const imagesArr = getUnique(await page.evaluate(() => Array.from(document.images, (e) => e.src))); return imagesArr.map((imageUrl) => ({ imageUrl, sourceUrl, depth })); } async findURLs(page) { return getOnlyHostURLs(await page.evaluate(() => Array.from(document.links, (e) => e.href)), this.baseURL); } async _close() { await this.browser.close(); } } export default Crawler;