如何在循环中通过异步IO使用多个选项卡?

时间:2018-04-03 09:37:04

标签: node.js multithreading tabs puppeteer

我正在尝试使用一定数量的标签来打开和操作这些标签,同时等待网络IO加速我对域的抓取过程。

我在Go中通过工作池监听通道解决了同样的问题,但我不确定如何在Node.js和Puppeteer中解决同样的问题。

我的猜测是循环过程

async function nextPage() {
    try {
        for (var link of uncrawledLinks.keys()) {
            if (runningThreads < maxThreads) {
                var page = await browser.newPage();
                console.log("nextPage() # runningThreads: " + runningThreads + " #  uncrawledLinks.size: " + uncrawledLinks.size);
            //debugger;
            crawlPage(page, link);
        }
    }

当我将“maxthreads”增加到1以上时可能会产生问题,但直到现在我都无法解决问题。

当我将其设置为高于1时发生的问题是链接被抓取两次(但不是每个链接,只有约90%的链接),因此我得到重复项,这使得爬虫不可用。

我想过使用像Redis或SQLite这样的数据库,但我想先解决问题而不用它来更好地理解问题(直到现在我没有性能/内存问题所以在内存中这样做是没有的问题)。

可运行的示例代码:

'use strict';
const puppeteer = require('puppeteer');
const url = require('url');

// start URL
const startUrlObj = url.parse("http://example.de/");
const startUrlDomain = startUrlObj.protocol + "//" + startUrlObj.hostname;
const startUrl = url.format(startUrlObj);

let browser;
let pages = [];
let uncrawledLinks = new Map();
let crawledLinks = [];
let runningThreads = 0;
const maxThreads = 1;

start();

async function start() {
    console.log("Starting Crawler");
    browser = await puppeteer.launch();
    console.log("Finished initializing browser object");
    uncrawledLinks.set(startUrl, "");
    nextPage();
};

async function crawlPage(page, link) {
    try {
        console.log("starting crawl for: " + link);
    runningThreads++;

    const response = await page.goto(link, {
        waitUntil: 'networkidle2',
        timeout: 30000
    });

    // find all links in the form <a href="xxx">
    const hrefs = await page.$$eval('a', as => as.map(a => a.href));
    hrefs.forEach(function(foundLink, key) {
        if (foundLink.startsWith(startUrlDomain)) {
            var tempUrl = url.parse(foundLink);
                // remove #asd and ?param1=y values from URL
                tempUrl.hash = null;
                tempUrl.search = null;
               var tempLink = url.format(tempUrl);
                //console.log(url.format(tempLink));
            if (crawledLinks.includes(tempLink) === false) {
                if (tempLink.endsWith(".html") === true) {
                    uncrawledLinks.set(tempLink, "false");
                    //pages.push(tempLink);
                }      
        }
    }},  hrefs)
    //console.log("Found new links: " + i + " # " + link);

    // crawling queues
    uncrawledLinks.delete(link);
    crawledLinks.push(link);

} catch (error) {
    // Log errors
    console.error(error);
} finally {
    runningThreads--;
    await page.close();
    await nextPage();
}

}

async function nextPage() {
    try {
        for (var link of uncrawledLinks.keys()) {
            if (runningThreads < maxThreads) {
                var page = await browser.newPage();
                //console.log("nextPage() # runningThreads: " + runningThreads + " #  uncrawledLinks.size: " + uncrawledLinks.size);
            //debugger;
            crawlPage(page, link);
        }
    }


} catch (error) {
    console.error(error);
} finally {
    if (uncrawledLinks.size === 0 && runningThreads === 0) {
        console.log("Finished crawling");
        console.log(crawledLinks);
        await browser.close();
    }
} 
}

2 个答案:

答案 0 :(得分:0)

首先,不幸的是(或者幸运的是,取决于你的观点)你没有Node.js中的线程(技术上你有,但它们不能从JavaScript代码中获得 - 仅在C ++级别)。这只是为了说清楚,因为它可能是一个无辜的命名错误或误解,可能会给你不合理的期望。

其次,请注意您正在捕捉erorr但正在打印error,因此您可能会因为变量拼写错误而导致某些错误无法显示。

最后,您正在尝试做的事情(异步操作与任何给定时间内有多少未完成的操作并行限制)可以使用Bluebird的eachLimit方法轻松解决:

使用async关键字的简单循环不容易做到这一点,因为通过在循环中使用async,您将停止迭代,直到解析了promise,而不是并行执行其他迭代,并且您必须手动跟踪计数器,但是awaiting对计数器的状态进行更改而不是给定的函数调用。使用async模块更容易,而不是async关键字。

答案 1 :(得分:0)

我发现必须用Promise包装IO内容,因为只有Promise才能提供异步IO。

async function crawlPage(page, link) {
    browser.newPage().then(
        async page => {
            try {
                await page.setRequestInterception(true);
                // catch all requests
                page.on('request', request => {

                })

                await page.goto(uncrawledUrl, {
                    waitUntil: 'networkidle2',
                    timeout: 10000
                }).then(
                    async response => {