W4J音乐热评一Puppeteer爬虫并发和连接池(2)

  • 上一节我们完成了并发数的控制,这一节完成连接池。
  • 因为单纯只用并发数来控制的话,每次爬取新的页面都会开启新的浏览器,使用完毕后关闭,会造成很大的资源开销。
  • 使用连接池,一次性创建指定个数的浏览器实例,随用随取,减少资源开销。

Puppeteer连接池

  • 使用generic-pool,这是一个基于Promise的通用链接池库。
  • 有了他之后我们就可以 将Puppeteer实例放在我们的链接池中,首先启动指定数量的浏览器实例,如果有请求进来,那么就去池子里面去取一个实例。
  • 更多详细信息可点击这里generic-pool
# 安装generic-pool
npm i generic-pool --save

# 安装完成
+ generic-pool@3.7.1
//genericPool.js
const puppeteer = require('puppeteer')
const genericPool = require('generic-pool')
/**
 * 初始化一个 Puppeteer 池
 * @param {Object} [options={}] 创建池的配置配置
 * @param {Number} [options.max=10] 最多产生多少个 puppeteer 实例 。如果你设置它,请确保 在引用关闭时调用清理池。 pool.drain().then(()=>pool.clear())
 * @param {Number} [options.min=1] 保证池中最少有多少个实例存活
 * @param {Number} [options.maxUses=2048] 每一个 实例 最大可重用次数,超过后将重启实例。0表示不检验
 * @param {Number} [options.testOnBorrow=2048] 在将 实例 提供给用户之前,池应该验证这些实例。
 * @param {Boolean} [options.autostart=false] 是不是需要在 池 初始化时 初始化 实例
 * @param {Number} [options.idleTimeoutMillis=3600000] 如果一个实例 60分钟 都没访问就关掉他
 * @param {Number} [options.evictionRunIntervalMillis=180000] 每 3分钟 检查一次 实例的访问状态
 * @param {Object} [options.puppeteerArgs={}] puppeteer.launch 启动的参数
 * @param {Function} [options.validator=(instance)=>Promise.resolve(true))] 用户自定义校验 参数是 取到的一个实例
 * @param {Object} [options.otherConfig={}] 剩余的其他参数 // For all opts, see opts at https://github.com/coopernurse/node-pool#createpool
 * @return {Object} pool
 */
const initPuppeteerPool = (options = {}) => {
    const {
        max = 10,
        min = 3,//建议连接池最小(min)实例数,应与并发数一致
        maxUses = 2048,
        testOnBorrow = true,
        autostart = false,
        idleTimeoutMillis = 3600000,
        evictionRunIntervalMillis = 180000,
        puppeteerArgs = {},
        validator = () => Promise.resolve(true),
        ...otherConfig
    } = options

    const factory = {
        create: () =>
            puppeteer.launch(puppeteerArgs).then(instance => {
                // 创建一个 puppeteer 实例 ,并且初始化使用次数为 0
                instance.useCount = 0
                return instance
            }),
        destroy: instance => {
            instance.close()
        },
        validate: instance => {
            // 执行一次自定义校验,并且校验校验 实例已使用次数。 当 返回 reject 时 表示实例不可用
            return validator(instance).then(valid => Promise.resolve(valid && (maxUses <= 0 || instance.useCount < maxUses)))
        }
    }
    const config = {
        max,
        min,
        testOnBorrow,
        autostart,
        idleTimeoutMillis,
        evictionRunIntervalMillis,
        ...otherConfig
    }
    const pool = genericPool.createPool(factory, config)
    const genericAcquire = pool.acquire.bind(pool)
    // 重写了原有池的消费实例的方法。添加一个实例使用次数的增加
    pool.acquire = () =>
        genericAcquire().then(instance => {
            instance.useCount += 1
            return instance
        })
    pool.use = fn => {
        let resource
        return pool
            .acquire()
            .then(r => {
                resource = r
                return resource
            })
            .then(fn)
            .then(
                result => {
                    // 不管业务方使用实例成功与后都表示一下实例消费完成
                    pool.release(resource)
                    return result
                },
                err => {
                    pool.release(resource)
                    throw err
                }
            )
    }
    return pool
}
module.exports = initPuppeteerPool
  • 整体流程如下:
    • 在服务启动时启动池。
    • 请求到达 => 从池中取得一个Puppeteer实例 => 打开tab页 => 运行代码 => 关闭tab页 => 返回数据(其他的管理都交给池了)
  • 那么我们之前的代码也要稍加修改.
//index.js
const puppeteer = require('puppeteer');
const mapLimit = require('async/mapLimit');
const initPuppeteerPool = require('./genericPool');
//准备的音乐id数组
let musicId = [1407551413, 1303289043, 1417862046];
// 全局只应该被初始化一次
const pool = initPuppeteerPool({ 
    puppeteerArgs: {
        timeout: 15000,
        ignoreHTTPSErrors: true,
        devtools: true,
        headless: true,
        args: [
            '-–disable-dev-shm-usage',
            '-–disable-setuid-sandbox',
            '-–no-first-run',
            '--no-sandbox',
            '-–no-zygote',
            '-–single-process'
        ]
    }
})
//并发次数为3,建议连接池最小(min)实例数,应与并发数一致
mapLimit(musicId, 3, (item, callback) => {
    (async () => {
        //并发执行爬取代码,返回结果
        let info = await selectInfo(item);
        callback(null, info);
    })();
}, (err, results) => {
    //三首音乐爬取完成再返回结果
    console.log(results);
});
//定义函数,内部返回Promise
async function selectInfo(id) {
    return new Promise(async (resolve, reject) => {
        // 在业务中取出实例使用
        const page = await pool.use(async browser => {
            const page = await browser.newPage();
            //同样的域名不同id
            await page.goto(`https://music.163.com/#/song?id=${id}`);
            return page
        })
        const iframe = await page.frames().find(f => f.name() === 'contentFrame');
        const musicComment = await iframe.$('.cmmts.j-flag');
        let commentList = await iframe.evaluate((e) => {
            let comment = Array.from(e.getElementsByClassName('cnt f-brk'));
            //只获取前五条
            return comment.map((item) => item.innerText).filter((item, index) => index <= 5);
        }, musicComment);
        //关闭标签页!!!!!
        await page.close();
        //返回当前音乐爬取的数据
        resolve(commentList);
    })
}
  • 这次时关闭标签页而不是关闭浏览器.
  • 最终的效果,谁用谁知道,运行速度有很大的提升.
  • 至此,一个比较完整的并发和连接池的爬虫就完成了,下一步该解决IP的问题.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351