UNPKG

krawala

Version:

Simple javascript web-crawler with command line interface

211 lines (170 loc) 4.96 kB
import { Tree } from 'jargs'; import * as request from 'request'; import * as _ from 'underscore'; import { Crawled, LinkScriptImageOrHref, Options, Output, OutputLinks, Page, PartiallyCrawled, Progress, RequiredOptions, } from './types'; import { collectData, complete, getFailed, isSameDomain, resolveUrl, updateProgress, validateBaseUrl, } from './utils'; const MATCHES_SUCCESS_STATUS = /^[123]/; const MATCHES_TEXT_HTML = /\btext\s*\/\s*html\b/i; const USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' + '(KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36'; const REQUEST_OPTIONS = { method: 'GET', headers: { 'User-Agent': USER_AGENT, }, }; let options: Options; const crawled: {[index: string]: PartiallyCrawled | undefined} = {}; const queue: PartiallyCrawled[] = []; const progress: Progress = { depth: 0, urlsToCrawl: [], urlsCrawled: [], failed: [], progressMade: false, }; const output: Output = { pages: [], externalPages: [], links: [], scripts: [], images: [], failed: [], }; const enqueue = (node: PartiallyCrawled) => { progress.urlsToCrawl.push(node.resolved); queue.push(node); }; const storeNode = (node: PartiallyCrawled) => { crawled[node.resolved] = node; }; const storeSubNode = (node: PartiallyCrawled, href: LinkScriptImageOrHref, key: keyof OutputLinks) => { const { url, resolved } = href; const subNode = { url, resolved, depth: node.depth + 1, internal: isSameDomain(resolved, options.resolved), linkedFrom: [node.resolved], }; if (progress.urlsToCrawl.indexOf(resolved) < 0) { output[key].push(subNode); enqueue(subNode); } const subCrawledNode = crawled[resolved]; if (typeof subCrawledNode === 'object') { subCrawledNode.linkedFrom.push(node.resolved); } else { storeNode(subNode); } }; let crawlQueue: () => void; const crawlNode = (node: PartiallyCrawled) => { if (node.depth > progress.depth) { progress.depth = node.depth; } updateProgress(options, progress, `Crawling: ${node.resolved}`); request(node.resolved, {...REQUEST_OPTIONS, timeout: options.timeout}, (error, response, body) => { progress.urlsCrawled.push(node.resolved); const status = (response ? response.statusCode : null) || null; const contentType = (response ? response.headers['content-type'] : '') || ''; const type = Array.isArray(contentType) ? `[${contentType.join(', ')}]` : contentType; if (!error && status && MATCHES_SUCCESS_STATUS.test(status.toString())) { node.failed = false; node.type = type; node.status = status; if (node.internal && MATCHES_TEXT_HTML.test(type) && body) { const data = collectData(node as Crawled, body, options.resolved); _.extend(node, data); const page = node as Page; if (node.depth < options.depth) { page.hrefs.internal.forEach((href) => storeSubNode(node, href, 'pages')); page.hrefs.external.forEach((href) => storeSubNode(node, href, 'externalPages')); page.links.forEach((href) => storeSubNode(node, href, 'links')); page.scripts.forEach((href) => storeSubNode(node, href, 'scripts')); page.images.forEach((href) => storeSubNode(node, href, 'images')); } } else { node.failed = false; } } else { progress.failed.push(node.resolved); node.failed = true; } node.type = type || null; node.status = status || null; const crawledNode = crawled[node.resolved]; if (typeof crawledNode === 'object') { crawledNode.failed = node.failed; crawledNode.type = node.type; crawledNode.status = node.status; } updateProgress(options, progress, `Crawled: ${node.resolved}`); if (progress.urlsToCrawl.length === progress.urlsCrawled.length) { complete(options, {...output, failed: getFailed(crawled)}); } else if (options.interval) { setTimeout(crawlQueue, options.interval); } else { crawlQueue(); } }); }; crawlQueue = () => { const node = queue.shift(); if (node) { crawlNode(node); } }; export const crawl = (tree: Tree & RequiredOptions) => { const { kwargs: { url, depth = '10', format = 'json', interval, timeout = '10000', }, flags: { wait = false, }, } = tree; validateBaseUrl(url); options = { url, resolved: resolveUrl(url), depth: parseInt(depth, 10), format, interval: typeof interval === 'string' ? parseInt(interval, 10) : undefined, wait, timeout: parseInt(timeout, 10), }; const node = { url, resolved: resolveUrl(url), depth: 0, internal: true, linkedFrom: [], }; output.pages.push(node); enqueue(node); crawlQueue(); }; export default crawl;