UNPKG

@blocklet/crawler

Version:

blocklet crawler lib

342 lines (341 loc) 11.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getPageContent = exports.crawlUrl = exports.crawlBlocklet = exports.cancelCronCrawlBlocklet = void 0; Object.defineProperty(exports, "getRelativePath", { enumerable: true, get: function () { return _utils.getRelativePath; } }); exports.setUrlInfoToCache = exports.initCronCrawlBlocklet = exports.getUrlInfoFromCache = void 0; var PQueue = _interopRequireWildcard(require("p-queue")); var _ufo = require("ufo"); var _utils = require("./utils"); var _cron = _interopRequireDefault(require("@abtnode/cron")); var _debounce = _interopRequireDefault(require("lodash/debounce")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } const formatHtml = htmlString => { if (typeof htmlString !== "string") { return ""; } if (htmlString.includes("<h2>Unexpected Application Error!</h2>")) { return ""; } return htmlString; }; const crawlQueue = new PQueue.default({ concurrency: 1, timeout: 60 * 1e3 }); const getPageContent = async ({ url, formatPageContent }) => { let page = await (0, _utils.initPage)(); let pageContent = null; try { const response = await page.goto(url, { timeout: 60 * 1e3 // 60s }); if (!response) { throw new Error(`Failed to load page: response is null for ${url}`); } const statusCode = response.status(); if (![200, 304].includes(statusCode)) { throw new Error(`Request failed with status ${statusCode}, in ${url}`); } await page.waitForNetworkIdle({ idleTime: 1 * 1e3 // 1s }); if (formatPageContent) { pageContent = await formatPageContent({ page, url }); } else { pageContent = await page.content(); } } catch (error) { _utils.logger.error("Get page content error:", error.message || error); } finally { await page.close(); } return formatHtml(pageContent); }; exports.getPageContent = getPageContent; const getUrlInfoFromCache = async url => { const cache = await _utils.useCache.get((0, _utils.getRelativePath)(url)); return cache; }; exports.getUrlInfoFromCache = getUrlInfoFromCache; function getNextCrawlDate(lastmod) { const now = /* @__PURE__ */new Date(); const lastModTime = lastmod ? new Date(lastmod).getTime() || 0 : 0; const daysDiff = Math.max(0, (now.getTime() - lastModTime) / (24 * 60 * 60 * 1e3)); const CRAWL_INTERVALS = /* @__PURE__ */new Map([[[-1, 0], 1], // 无 lastmod [[0, 3], 1], // 3 天内活跃 [[3, 7], 3], // 7 天内活跃 [[7, 30], 14], // 30 天内活跃 [[30, 90], 30], // 90 天内活跃 [[90, Infinity], 365] // 长期不活跃 ]); const interval = Array.from(CRAWL_INTERVALS.entries()).find(([[min, max]]) => lastmod ? daysDiff > min && daysDiff <= max : min === -1)?.[1] || 90; return new Date(now.getTime() + interval * 24 * 60 * 60 * 1e3).toISOString(); } const setUrlInfoToCache = async ({ url, content, lastmod, nextDate }) => { if (!content || !url) { return; } const lastmodValue = lastmod || ( /* @__PURE__ */new Date()).toISOString(); return await _utils.useCache.set((0, _utils.getRelativePath)(url), { content, lastmod: lastmodValue, updatedAt: ( /* @__PURE__ */new Date()).toISOString(), nextDate: nextDate || getNextCrawlDate(lastmodValue) }); }; exports.setUrlInfoToCache = setUrlInfoToCache; const crawlUrl = async ({ urls, lastmodMap, formatPageContent, autoCloseBrowserCount = 50 }) => { if (typeof urls === "string") { urls = [urls]; } const crawlUrlJob = ({ url, retryCount = 0, index }) => { return async () => { try { if (index % autoCloseBrowserCount === 0) { await (0, _utils.closeBrowser)({ trimCache: false }); } const canCrawl = await (0, _utils.isAcceptCrawler)(url); if (canCrawl) { const pageContent = await getPageContent({ url, formatPageContent }); if (pageContent) { const lastmodValue = lastmodMap?.get(url) || ( /* @__PURE__ */new Date()).toISOString(); await setUrlInfoToCache({ url, content: pageContent, lastmod: lastmodValue, nextDate: getNextCrawlDate(lastmodValue) }); _utils.logger.info(`Crawler[${index}] ${url} success`); } else if (retryCount < 3) { retryCount++; const timeout = 1e3 * 5 * retryCount; _utils.logger.info(`Crawler[${index}] ${url} fail, continue after ${timeout}ms`); await (0, _utils.sleep)(timeout); crawlQueue.add(crawlUrlJob({ url, retryCount, index }), { priority: retryCount // Operations with greater priority will be scheduled first. }); } else { _utils.logger.info(`Crawler[${index}] ${url} fail reach 3 times, skip it`); } } else { _utils.logger.info(`Crawler[${index}] ${url} skip`); } if (global.gc) { global.gc(); } } catch (error) { _utils.logger.info(`Crawler[${index}] ${url} abort by error, skip it: ${error?.message || error?.reason || error}`); } }; }; for (const [index, url] of urls.entries()) { await crawlQueue.add(crawlUrlJob({ url, index: index + 1 })); } }; exports.crawlUrl = crawlUrl; const crawlBlockletRunningMap = /* @__PURE__ */new Map(); const crawlBlocklet = async () => { const { mountPoint, did } = (0, _utils.getComponentInfo)(); if (crawlBlockletRunningMap.has(did) && crawlBlockletRunningMap.get(did)) { _utils.logger.info(`Crawler blocklet ${did} is running, skip it`); return; } try { const browser = await (0, _utils.getBrowser)(); if (!browser) { throw new Error("No Browser can use"); } _utils.logger.info("Crawler blocklet existing can use browser"); } catch (error) { _utils.logger.info(`Crawler blocklet abort by error: ${error?.message || error?.reason || error}`); return; } const { appUrl } = _utils.env; if (!appUrl) { throw new Error("appUrl not found"); } const sitemapList = await (0, _utils.getSitemapList)(appUrl); const matchMountPoint = (0, _ufo.joinURL)(appUrl, !mountPoint || mountPoint === "/" ? "" : mountPoint); const otherMountPointList = _utils.components.filter(item => item.mountPoint && item.mountPoint !== mountPoint).map(item => item.mountPoint); const blockletLocList = sitemapList.filter(item => { if (mountPoint !== "/") { return item?.url?.indexOf(matchMountPoint) > -1; } return otherMountPointList.every(mountPoint2 => item?.url?.indexOf(mountPoint2) === -1); }); const canUseBlockletLocList = []; const lastmodMap = /* @__PURE__ */new Map(); let skipBlockletLocTotal = 0; let blockletLocTotal = 0; await Promise.all(blockletLocList.map(async item => { let tempLocList = []; if (item.url) { tempLocList.push(item.url); } if (item?.links?.length > 0) { tempLocList.push(...item.links?.map(ytem => ytem.url)); } blockletLocTotal += tempLocList.length; tempLocList = (await Promise.all(tempLocList.map(async loc => { try { const { lastmod: cacheLastmod, nextDate } = await _utils.useCache.get((0, _utils.getRelativePath)(loc)); if (item.lastmod && new Date(cacheLastmod).getTime() === new Date(item.lastmod).getTime()) { skipBlockletLocTotal++; return false; } if (nextDate && new Date(nextDate).getTime() >= ( /* @__PURE__ */new Date()).getTime()) { skipBlockletLocTotal++; return false; } return loc; } catch (error) {} return loc; }))).filter(Boolean); tempLocList.forEach(loc => { if (item.lastmod) lastmodMap.set(loc, item.lastmod); }); canUseBlockletLocList.push(...tempLocList); })); const crawlerLogText = (step = "") => [`Crawler sitemap.xml about ${did} ${step}: `, { blockletLocTotal, canUseBlockletLocTotal: canUseBlockletLocList.length, skipBlockletLocTotal, lastmodMapTotal: lastmodMap.size }]; _utils.logger.info(...crawlerLogText("start")); try { crawlBlockletRunningMap.set(did, true); await crawlUrl({ // @ts-ignore urls: canUseBlockletLocList, lastmodMap, formatPageContent: async ({ page, url }) => { const pageContent = await page.evaluate(() => { const removeElements = tagName => { const elements = document.querySelectorAll(tagName); for (let i = elements.length - 1; i >= 0; i--) { try { elements[i]?.parentNode?.removeChild(elements[i]); } catch (error) {} } }; removeElements('[id="uploader-container"]'); removeElements('[class^="uppy-"]'); removeElements('[id="point-up-component"]'); const meta = document.createElement("meta"); meta.name = "blocklet-crawler"; meta.content = "true"; document.head.appendChild(meta); return document.documentElement.outerHTML; }); return pageContent; } }); _utils.logger.info(...crawlerLogText("success")); await (0, _utils.closeBrowser)({ trimCache: true }); } catch (error) { _utils.logger.info(`Crawler blocklet abort by error`, error); } finally { crawlBlockletRunningMap.delete(did); } }; exports.crawlBlocklet = crawlBlocklet; const CRON_CRAWL_BLOCKLET_KEY = "cron-crawl-blocklet"; let cronCrawlBlockletJob = null; const initCronCrawlBlocklet = ({ time = "0 0 */12 * * *", // every 12 hours options } = {}) => { if (!cronCrawlBlockletJob) { cronCrawlBlockletJob = _cron.default.init({ context: {}, jobs: [{ name: CRON_CRAWL_BLOCKLET_KEY, time, fn: (0, _debounce.default)(crawlBlocklet), options: { runOnInit: false, ...options } }], onError: err => { console.error("run job failed", err); } }); } return cronCrawlBlockletJob; }; exports.initCronCrawlBlocklet = initCronCrawlBlocklet; const cancelCronCrawlBlocklet = () => { if (cronCrawlBlockletJob) { cronCrawlBlockletJob.jobs[CRON_CRAWL_BLOCKLET_KEY].stop(); cronCrawlBlockletJob = null; crawlQueue.clear(); _utils.logger.info("Cron crawl blocklet stop, clear crawl queue"); } }; exports.cancelCronCrawlBlocklet = cancelCronCrawlBlocklet;